2

I am not too confident working with Firestore and have trouble with more complex API calls to get data. Usually I use SQL backends in my apps.

For the section that I am working on, I would like to combine three collections to get an array of ToDos with the involved users and the category the current user labelled this ToDo with. Every involved person can label the ToDo like they prefer, which makes things a little more complicated. Broken down the collections are structured as follows.

todo: Firestore Database Document

{
   title: string,
   involved: string[], //user ids
   involvedCategory: string[] //category ids mapped by index to involved
}

(I tried to have an array of objects here instead of the two arrays, but it seems I would not be able to query the array for the current user´s ID, like mentioned here, so this is a workaround)

category: Firestore Database Document

{
   title: string,
   color: string
}

user: Firebase Authentication User

{
   uid: string,
   displayName: string,
   photoURL: string,
   ...
}

THE GOAL An array of ToDo items like this:

{
   id: string,
   title: string,
   involved: User[],
   category?: {
      title: string,
      color: string
   }
}

As I am working with TypeScript, I created an interface to use a converter with. My code looks like this so far:

import {
    DocumentData,
    FirestoreDataConverter,
    WithFieldValue,
    QueryDocumentSnapshot,
    SnapshotOptions,
    query,
    collection,
    where,
} from 'firebase/firestore'
import { store } from '../firebase'
import { useCollectionData } from 'react-firebase-hooks/firestore'
import { User } from 'firebase/auth'
import { useCategories } from './categories'
import { useAuth } from '../contexts/AuthContext'

interface ToDo {
    id: string
    title: string
    involved: User[]
    category?: {
        title: string
        color: string
    }
}

const converter: FirestoreDataConverter<ToDo> = {
    toFirestore(todo: WithFieldValue<ToDo>): DocumentData {
        return {} //not implemented yet
    },
    fromFirestore(
        snapshot: QueryDocumentSnapshot,
        options: SnapshotOptions
    ): ToDo {
        const data = snapshot.data(options)

        return {
            id: snapshot.id,
            title: data.title,
            category: undefined, //?
            involved: [], //?
        }
    },
}

export function useToDos() {
    const { currentUser } = useAuth()
    const { categories } = useCategories() //needed in converter

    const ref = query(
        collection(store, 'habits'),
        where('involved', 'array-contains', currentUser.uid)
    ).withConverter(converter)
    const [data] = useCollectionData(ref)

    return {
        todos: data,
    }
}

Is there any way I can do this? I have a Hook that returns all of the user´s categories, but I obviously can´t call that outside the useToDos-Hook. And creating the const in the hook does not help, either, as it results in an infinite re-render.

I know this is a long one, but does anyone have tips how I could approach this? Thanks in advance ^^

UPDATE: I had to make two small adjustments to @ErnestoC ´s solution in case anyone is doing something similar:

First, I changed the calls for currentUser.id to currentUser.uid.

Afterwards I got the very missleading Firestore Error: PERMISSION_DENIED: Missing or insufficient permissions, which made me experiment a lot with my security rules. But that is not where the error originated. Debugging the code line by line, I noticed the category objects resolved by the promise where not correct and had a weird path with multiple spaces at the beginning and the end of their ids. When I removed them before saving them in the promises array, it worked. Although I do not see where the spaces came from in the first place.

promises.push(
    getDoc(
        doc(
             store,
             'categories',
             docSnap.data().involvedCategory[userCatIndex].replaceAll(' ', '')
        )
    )
)
Bexy-Lyn
  • 137
  • 1
  • 9

1 Answers1

1

The general approach, given that Firestore is a NoSQL database that does not support server-side JOINS, is to perform all the data combinations on the client side or in the backend with a Cloud Function.

For your scenario, one approach is to first query the ToDo documents by the array membership of the current user's ID in the involved array.

Afterwards, you fetch the corresponding category document the current user assigned to that ToDo (going by index mapping between the two arrays). Finally, you should be able to construct your ToDo objects with the data.

const toDoArray = [];
const promises = [];

//Querying the ToDo collection
const q = query(collection(firestoreDB, 'habits'), where('involved', 'array-contains', currentUser.id));
const querySnap = await getDocs(q);

querySnap.forEach((docSnap) => {
  //Uses index mapping 
  const userCatIndex = docSnap.data().involved.indexOf(currentUser.id);
  //For each matching ToDo, get the corresponding category from the categories collection
  promises.push(getDoc(doc(firestoreDB, 'categories', docSnap.data().involvedCategory[userCatIndex])));
  //Pushes object to ToDo class/interface
  toDoArray.push(new ToDo(docSnap.id, docSnap.data().title, docSnap.data().involved))
});

//Resolves all promises of category documents, then adds the data to the existing ToDo objects.
await Promise.all(promises).then(categoryDocs => {
  categoryDocs.forEach((userCategory, i) => {
    toDoArray[i].category = userCategory.data();
  });  
});
console.log(toDoArray);

Using the FirestoreDataConverter interface would not be that different, as you would need to still perform an additional query for the category data, and then add the data to your custom objects. Let me know if this was helpful.

ErnestoC
  • 2,660
  • 1
  • 6
  • 19
  • @Bexy-Lyn did you need more help in combining your Firestore collections? – ErnestoC Jul 11 '22 at 15:43
  • I get an `Unhandled Rejection (FirebaseError): Function where() called with invalid data. Unsupported field value: undefined` in the 3rd line of code. I will work on that a bit more. – Bexy-Lyn Jul 24 '22 at 08:10
  • Okay I made it work. Thanks Ernesto :) For anyone who wants to do something similar: See my edit. – Bexy-Lyn Jul 24 '22 at 09:25