1

I understand this type of question has been asked many times, but I haven't found an example that makes enough sense to execute on, so I'm bringing my specific case to the table to receive clarity on.

Summary of problem: This problem came about from working with the Vue router. In the project, we have a user who can make their own teams and add team members to that team. Initially I was getting the results I wanted in that the data flow I was looking for worked as expected using props in the router link to supply the detail views with data like so

this.members = this.$route.params.data

The team list would show in fine and clicking the button to see the members of a specific team also worked. The start of my issues is that the data from a router link is not persisted and upon refresh or changing pages to add a new team member, the team member list isn't updated when I use the this.$router.back() method nor does it show with the router.push({ path: 'home' }) method either as expected.

After researching more about how router links function with Vue and reading some SO posts from users with similar circumstances, I realized I needed a better way to retrieve the data I wanted simply from using the :to="{ name: 'PageThatShowsTeamMembers', params: { id: id } } method and that's where I've been running into what's seemingly a dead end.

What my current data structure looks like: In Firestore I am using a top level collections to store the teams. the document id is the current users uid and inside that document contains fields with the team information and an array that stores the team members.

How I've tried adding the data into Firestore: I've explored a handful of ways to go about store the team member information for retrieval

let ref = db.collection("Teams").doc(firebase.auth().currentUser.uid).collection("Team Members");

ref.set({
  // team member info
})

let ref = db.collection("Teams").where("userId", "==", firebase.auth().currentUser.uid);
ref.get().then(querySnapshot => {
  querySnapshot.forEach(doc => {
    db.collection("Teams").doc(doc.id)
      .update({
        teamMembers: firebase.firestore.FieldValue.arrayUnion({
          // team member info
        })
      })
  });
})

let ref = db.collection("Teams").doc(this.$route.params.id);
ref.update({
  teamMembers: firebase.firestore.FieldValue.arrayUnion({
    // team member info
  })
})      

As expected, neither of those ways are getting quite to where I want to be.

How I've tried to retrieve the information: Having control over the overall structure of the data with Firestore, I've tried the following ways to get the same results I was seeing with the :to="{ name: 'PageThatShowsTeamMembers', params: { id: id, data: teamMemberData } } way that worked before to no avail.

let ref = db.collection("Teams").where("userId", "==", this.$route.params.id)
ref
  .get()
  .then(querySnapshot => {
    querySnapshot.forEach(doc => {
      this.members.push(...doc.data().teamMembers)
    });
  })
  .catch(err => {
    console.log(err);
  })

The code block above gets every team associated with the id in the route parameters which is the current users uid. This isn't the desired result as all members will show for every team instead of the specific members to any team that's clicked.

let ref = db.collection("Teams").doc(this.$route.params.id).collection("Team Members")

ref.get().then((querySnapshot) => {
  querySnapshot.forEach((doc) => {
    this.members.push(doc.data());
  });
});

This code block is gives me results closer to the :to="{ name: 'PageThatShowsTeamMembers', params: { id: id, data: teamMemberData } } way, but since that data requires the set() method to be used when adding new team members as opposed to the add() method, I feel like I'll run into the 1mb limit for my document size rather quickly in addition to not being able to query for a list of all team members in the collection as easily when I need to for a different page.

Where things are now: This brings me to the present situation. I have researched extensively on ways I can start to restructure the data in Firestore. I have already read through the following posts: Many to Many relationship in Firebase Firebase Cloud Firestore Many to Many Relationships Firebase query if child of child contains a value and many others besides these and I am still confused about how to store the uids for team members of a specific team to only get the specific data I want. I've seen in some of those posts with the suggested answers will have the uids store as field keys to be retrieved, but I don't see how that will list out the data once I query on them. I have also grown increasingly skeptical of store the team member information itself in an array or map field since that data is comprised of more than 7 fields itself.

After watching the Firebase videos that discuss data modeling and denormalization and thinking on it some more, I had the idea that it may be better to make the Team Members its own top level collection altogether and possibly storing the name of the team any one member belongs to in some way, but I'm not sure if that should be the document id for the team member or a field within the document that i query on. In addition to that I'm confused about how that would work with routing in Vue. If I made the team name the document id, how would I go about setting that properly in a router link to get the results I want. Another option could be to make the team members a subcollection, but then I lose some query capabilities and add to the number of reads I have to make.

I understand there isn't one specific 'right' way to go about it and there will be tradeoffs no matter what, I'd just like a little more clarity about how the code would actually look. I'd also like to know if using the current users uid as the document id for anything outside of the user collection is sensible? My initial thinking for doing so was that a team belongs to a specific user, but these bumps in the road has me rethinking my approach.

Example of a query I'd like to run:

let ref = db.collection("Team Members").where("teamId", "==", this.$route.params.id) // the id here could possibly be the team name or the document id or something along those lines
ref
  .get()
  .then(querySnapshot => {
    querySnapshot.forEach(doc => {
      this.members.push(doc.data())
    });
  })
  .catch(err => {
    console.log(err);
  })

Based on the comment by Ernesto I wrote out an example query I'd like to write that stems from the idea of having a top level Team Members collection that keeps track of the team that member is apart of. My thinking is that for a query like this, the id in the route parameters would be maybe the team name or the document id of the team that member is in inside of the Team collection that currently exists. My only hesitation with an approach like this is how to set that id properly in the data prop for vue.

AdamBrashear
  • 131
  • 12
  • From what I have understood, it seems like making a top level collection with information about each user or team member is a good starting point. In this way you are simplifying the structure as you do not have subcollections in each team, since it could lead to duplicate team member documents in case a team member is part of multiple teams. Can you add just an example of the queries you want to run? Just to understand the issue better. – ErnestoC Dec 14 '21 at 21:53
  • @ErnestoContrerasPinon Thanks for replying, I edited my post to show an example query as asked. – AdamBrashear Dec 14 '21 at 23:15

1 Answers1

1

Based from this article, I made a simple Firestore database with the following structure:

TeamMembers (Collection)
    tm1 (Doc)
        “FullName”: “John Smith”,
        “MemberOf”: [“team1”, “team2”]
    ...

Teams (Collection)
    team1 (Doc)
        “Owner”: “tm1”,
        “Members”: [“tm1”, “tm2”, tm3”]
    ...

For this use case, in which a team can have multiple members, and each member is able to be part of multiple teams (M:N), this could work. As you said in the question, there are multiple approaches that can be taken here. Useful operators for queries against this structure are included in the array membership doc. For example the below query would give me all Team members who are part of “team1”:

  let teamRef = firestoreDB.collection("TeamMembers").where("MemberOf", "array-contains", "team1");
  teamRef.get()
    .then((qSnap) => {
      qSnap.forEach((doc) => {
        console.log(doc.data().FullName);
      });
    })
    .catch((err) => {
      console.log("Caught error: ", err);
    });

We could edit this query to work the opposite way, finding the Teams that a TeamMember as a part of:

  let teamRef = firestoreDB.collection("Teams").where("Members", "array-contains", "tm1");
  teamRef.get()
    .then((qSnap) => {
      qSnap.forEach((doc) => {
        console.log(doc.id); // Since the team name is the doc ID, it can be obtained from the DocumentSnapshot object
      });
    })
    .catch((err) => {
      console.log("Caught error: ", err);
    });

You have to use Batch Writes to perform add or remove operations, so you can update the relevant arrays for a Team and TeamMember documents in an atomic operation that cannot be half-completed. As for how to obtain the Document ID to use with a Vuejs data prop, I found this related question and the relevant reference for Vuejs.

ErnestoC
  • 2,660
  • 1
  • 6
  • 19
  • 2
    It's cool that you referenced that specific article, I read it recently too and liked the structure. The overall idea makes sense with using the docRefId of a newly created document, the only issue is the navigation in our project removes it as an option. The user starts at the team view and drills down to the members list a drills down page to add members and navigates back to the updated member list view. My initial thinking is that the id in the `:to="{ name: ' ', params: { id: id }`separate router links would be in conflict with that drill down navigation, but I'll test and see. – AdamBrashear Dec 16 '21 at 00:37