Firestore Many-to-Many: Part 2 - array-contains-all
Tags: firestore, firebase, angular, rxjs
Published: 2021-08-27 01:01:26

## Update 3/24/24
This post is out-of-date. Please see my latest post on the `array-contains-all` workaround.

https://code.build/p/firestore-many-to-many-array-contains-all-dyFZgf
___
## Original Post
___


This is Part 2 of the [Firestore Many-to-Many](https://dev.to/jdgamble555/firestore-many-to-many-mf8/) Series.  Before I cover the map many-to-many situations, there is two more situation in arrays that you need to think about: 

## array-contains-any / array-contains-all

**Get all students taking either Class A or Class B**

```typescript
this.afs.collection('students')
.where('classes', 'array-contains-any', [classA_ID, classB_ID]);
```

As you can see, you can easily accomplish this with [array-contains-any](https://firebase.google.com/docs/firestore/query-data/queries#array-contains-any).

**Get all classes either Student A is taking or Student B is taking**

```typescript
this.afs.collection('classes')
.where('students', 'array-contains-any', [studentA_ID, studentB_ID]);
```

OR

Just to give you another route, you can still use pipe with **in** to accomplish the rxjs frontend join:

```typescript
this.afs.collection('students', ref =>
  ref.where(
    firebase.firestore.FieldPath.documentId(),
    'in',
    [studentA_ID, studentB_ID]
  )
).valueChanges().pipe(
  switchMap((r: any[]) => {
    let ids = r.map((m: any) => m.classes);
    ids = Array.prototype.concat.apply([], ids);
    const diff = ids.filter(
      (v: any, i: number, a: any[]) =>
        a.indexOf(v) === i
    ).sort();
    const docs: Observable<any>[] = diff.map(
      (id: number) => this.afs.doc('classes/' + id).valueChanges()
    );
    return combineLatest(docs);
  })
);
```
Basically here, you use the **in** where clause to look for several students. You then reduce the array and filter for only unique documents. Again, you don't want to filter after the reads, but you want to avoid reading duplicates in the first place.

**Note:** **in**, **array-contains**, and **array-contains-any** all only allow up to 10 instances.

array-contains - JOIN "="
array-contains-any - JOIN "OR"
array-contains-all - JOIN "AND"

But how do you do **array-contains-all**? You can't natively.  I foresee Firestore adding this feature before any other real features. However, there is a hack.

Basically you create your own index using **__** between every combination of items in the array. This would give you search options. You could do this on the front end, or on the backend in Firebase Functions. Here I am only covering the frontend, although I may one day add this ability to my [adv-firestore-functions](https://github.com/jdgamble555/adv-firestore-functions) package.

### Array-contains-all - ADD

```typescript
function createArrays(arr: any[]) {
  function getSubArrays(a: any[]) {
    if (a.length === 1) return [a];
    else {
      const subarr: any[] = getSubArrays(a.slice(1));
      return subarr.concat(
        subarr.map((e: any[]) => e.concat(a[0])), [[a[0]]]
      );
    }
  }
  return getSubArrays(arr).map((a: any[]) => a.sort().join('__'));
}
```

### array-contains-all - UPDATE

```typescript
function getArray(arr: any[]) {
  return arr.filter((f: string) => !f.includes('__')).sort();
}
```

### array-contains-all - Query

```typescript
function createSearch(...s: any) {
  return typeof s === 'string'
    ? s
    : s.sort().join('__');
}
```

You need to use these three functions in order to add, update, and query a doc in your firestore collection.  So, using our example:

**ADD**

```typescript
this.afs.collection('students').add({
  classes: createArrays([
    class1,
    class2,
    class3
  ])
});
```

**UPDATE**

```typescript
const q = this.afs.doc('students/' + studentID).valueChanges();
const x = (await q.pipe(take(1)).toPromise() as any).classes;
const classes = getArray(x);

this.afs.collection('students').set({
  classes: createArrays([
    ...classes,
    add_new_class_here
  ])
});
```
You will basically be storing ids alphabetically like:

```id1__id2, id1__id3, id2__id3,... etc```

**QUERY**

Then you can query the doc like this:

**Get all students taking both Class A AND Class B**

```typescript
this.afs.collection('students)
.where(
  'classes',
  'array-contains',
  createSearch(class1_ID, class2_ID)
);
```
OR
```typescript
this.afs.collection('classes',
  ref =>
    ref.where(
      firebase.firestore.FieldPath.documentId(),
      'in',
      [classID_1, classID_2])
).valueChanges().pipe(
  switchMap((r: any[]) => {
    const ids = r.map((m: any) => m.students);
    const common = ids.reduce(
      (a: number[], b: number[]) => a.filter(
        (c: number) => b.includes(c)
      )
    ).sort();
    const docs: Observable<any>[] = common.map(
      (id: number) => this.afs.doc('students/' + id).valueChanges()
    );
    return combineLatest(docs);
  })
);
```
The reverse version of this would be getting both class documents using **IN**, filtering the students array to what is different, grabbing documents in common using combineLatest, and sorting. 

And you get **array-contains-all**!

Next up: Using map for Many-to-Many... coming soon!

J