Migrating to AngularFire v7 (Firebase v9) - CRUD operations

Viewed 644

I'm really challenging myself into migrating slowly to Firebase v9. For now I just replaced the paths with the /compat version, but I started working on it on another file to test few things.

I understand that this method of work for Firebase can increase performances and weight of the libs but I find it pretty difficult to use it related to Firebase v8.

In my app I have a file with few CRUD operations. Let me show it to you:

import { Timestamp } from './../../auth/models/timestamp.model';
import { map, take } from 'rxjs/operators';
import { Observable } from 'rxjs';
import {
    AngularFirestore, AngularFirestoreDocument, AngularFirestoreCollection, DocumentChangeAction
} from '@angular/fire/compat/firestore';
import { Injectable } from '@angular/core';
import firebase from 'firebase/compat/app';

// Custom Types
type CollectionPredicate<T> = string | AngularFirestoreCollection<T>;
type DocPredicate<T> = string | AngularFirestoreDocument<T>;


@Injectable({
  providedIn: 'root'
})
export class FirestoreExtendedService {

  constructor(private afs: AngularFirestore) { }

  /**
   * Get data from document.
   *
   * @param ref Reference of the document.
   *
   * @returns Observable of the requested document.
   */
  doc$<T>(ref: DocPredicate<T>): Observable <T> {
    return this.doc(ref)
      .snapshotChanges()
      .pipe(
        map((doc) => doc.payload.data() as T),
      );
  }

  /**
   * Get data from collection.
   *
   * @param ref Reference of the collection.
   * @param queryFn Filters to query collection items.
   *
   * @returns Observable of the requested collection.
   */
  col$<T>(ref: CollectionPredicate <T> , queryFn?: any ): Observable <T[]> {
    return this.col(ref, queryFn)
      .snapshotChanges()
      .pipe(
        map((docs: DocumentChangeAction<T>[]) => docs.map((a: DocumentChangeAction<T>) => a.payload.doc.data()) as T[]),
      );
  }

  /**
   * Set data into Firestore.
   * It will delete existing data.
   *
   * // Adds "createdAt" field.
   *
   * @param ref Reference of the document.
   * @param data Data to insert in the document.
   */
  set<T>(ref: DocPredicate<T>, data: any): Promise<void> {
    const timestamp = this.timestamp;
    return this.doc(ref).set({
      ...data,
      updatedAt: timestamp,
      createdAt: timestamp,
    });
  }

  /**
   * Update data into Firestore.
   *
   * // Adds "updatedAt" field.
   *
   * @param ref Reference of the document.
   * @param data Data to upsert in the document.
   */
  update<T>(ref: DocPredicate<T>, data: any): Promise<void> {
    return this.doc(ref).update({
      ...data,
      updatedAt: this.timestamp,
    });
  }

  /**
   * Deletes a document from Firestore.
   *
   * @param ref Reference of the document.
   */
  delete<T>(ref: DocPredicate<T>): Promise<any> {
    return this.doc(ref).delete();
  }

  /**
   * Add data into Firestore.
   *
   * // Adds "createdAt" field.
   *
   * @param ref Reference of the collection.
   * @param data Data to insert in the collection.
   */
  add<T>(ref: CollectionPredicate<T>, data: any): Promise<firebase.firestore.DocumentReference> {
    const timestamp = this.timestamp;
    return this.col(ref).add({
      ...data,
      updatedAt: timestamp,
      createdAt: timestamp,
    });
  }

  /**
   * Upsert data into Firestore.
   * If data exists, it will update
   * it. Else it will set it.
   *
   * // Adds "createdAt" field.
   * // or "updatedAt" field.
   *
   * @param ref Reference of the document.
   * @param data Data to upsert in the document.
   */
  async upsert<T>(ref: DocPredicate<T>, data: any): Promise<void> {
    const doc = this.doc(ref)
      .snapshotChanges()
      .pipe(take(1))
      .toPromise();

    return doc.then((snap) => snap?.payload.exists ? this.update(ref, data) : this.set(ref, data));
  }

  /**
   * It returns an entire collection
   * of documents with ids.
   *
   * @param ref Collection Reference.
   * @param queryFn Filters to query the collection.
   */
  colWithIds$<T>(ref: CollectionPredicate<T>, queryFn?: any): Observable<T[]> {
    return this.col(ref, queryFn)
      .snapshotChanges()
      .pipe(
        map((actions: DocumentChangeAction<T>[]) => actions.map((a: DocumentChangeAction<T>) => {
            const data = a.payload.doc.data() as T;
            const thisId = a.payload.doc.id;
            return { id: thisId, ...data };
          })),
      );
  }

  /**
   * @returns Unique id in the database.
   */
  generateId<T>(): string {
    return this.afs.createId();
  }

  /**
   * @returns Firebase Server Timestamp.
   */
  get timestamp(): Timestamp {
    return firebase.firestore.FieldValue.serverTimestamp() as unknown as Timestamp;
  }

  /**
   * Sanityze CollectionReference and return Collection.
   *
   * @param ref CollectionPredicate
   * @param queryFn Filters to query the collection
   */
  private col<T>(ref: CollectionPredicate <T> , queryFn?: any ): AngularFirestoreCollection <T> {
    return typeof ref === 'string' ? this.afs.collection<T>(ref, queryFn) : ref;
  }

  /**
   * Sanityze DocumentReference and return Document.
   *
   * @param ref DocumentPredicate
   */
  private doc<T>(ref: DocPredicate<T>): AngularFirestoreDocument <T> {
    return typeof ref === 'string' ? this.afs.doc<T>(ref) : ref;
  }

}

Thanks to this I can easily access some common functions I use without repeating code in my App.

I tried implementing the same thing with AngularFire v7 (Firebase v9) and this is where i'm at:

import { Injectable } from '@angular/core';
import {
    collection, deleteDoc, doc, DocumentSnapshot, Firestore, onSnapshot,
    query, QuerySnapshot, setDoc, Timestamp as fTimestamp, Unsubscribe
} from '@angular/fire/firestore';
import { Observable, Subject } from 'rxjs';
import { take } from 'rxjs/operators';

export interface FirestoreExtDoc<T> {
    data: Observable<T>;
    unsubscribe: Unsubscribe;
}
export interface FirestoreExtCol<T> {
    data: Observable<T[]>;
    unsubscribe: Unsubscribe;
}

@Injectable({
  providedIn: 'root'
})
export class FirestoreExtendedService {

  constructor(private db: Firestore) { }

  /**
   * Deletes a document from Firestore.
   *
   * @param ref Reference of the document.
   */
    delete(ref: string): Promise<any> {
        const docRef = doc(this.db, ref);
    return deleteDoc(docRef);
  }

    /**
     * Upsert data into Firestore.
     * If data exists, it will update
     * it. Else it will set it.
     *
     * // Adds "createdAt" field.
     * // or "updatedAt" field.
     *
     * @param ref Reference of the document.
     * @param data Data to upsert in the document.
     */
    upsert<DocumentData>(ref: string, data: any): Promise<void> {
        const docRef = doc(this.db, ref);
        const timestamp = fTimestamp.now();
        const newData = {
            ...data,
            updatedAt: timestamp,
            createdAt: timestamp,
        };
        const updatedData = {
            ...data,
            updatedAt: timestamp,
        };
        const snapshot = this.getDoc<DocumentData>(ref).data.pipe(take(1)).toPromise();
        return snapshot.then(
            snap => (snap as any).exists ?
                setDoc(docRef, updatedData, { merge: true }) :
                setDoc(docRef, newData, { merge: true })
            );
    };

  /**
   * It returns an entire collection
   * of documents with ids.
   *
   * @param ref Collection reference.
   * @param where Filters to query the collection.
   * @param orderBy Order or sort the collection.
   */
    col$<DocumentData>(ref: string, where?: any, orderBy?: any): FirestoreExtCol<DocumentData> {
        const colRef = collection(this.db, ref);

        if (where || orderBy) {
            return this.getCols<DocumentData>(
                query(colRef, where, orderBy)
            );
        }

        return this.getCols<DocumentData>(colRef);
    }

  /**
   * It returns an entire collection
   * of documents with ids.
   *
   * @param ref Collection reference.
   * @param where Filters to query the collection.
   * @param orderBy Order or sort the collection.
   */
    doc$<DocumentData>(ref: string): FirestoreExtDoc<DocumentData> {
        const docRef = doc(this.db, ref);
        return this.getDoc<DocumentData>(docRef);
    }

    private getDoc<DocumentData>(ref: any): FirestoreExtDoc<DocumentData> {
        const res = new Subject<DocumentData>();
        const snapshot = onSnapshot<DocumentData>(ref,
            (snap: DocumentSnapshot<DocumentData>) => {
                res.next({
                    id: snap.id,
                    exists: snap.exists(),
                    ...snap.data() as DocumentData
                });
            });

        return {
            data: res.asObservable(),
            unsubscribe: snapshot,
        };
    }

    private getCols<DocumentData>(ref: any): FirestoreExtCol<DocumentData> {
        const res = new Subject<DocumentData[]>();
        const snapshot = onSnapshot<DocumentData>(ref,
            (snap: QuerySnapshot<DocumentData>) => {
                const dataArray: DocumentData[] = [];
                snap.forEach(item => {
                    dataArray.push({
                        id: item.id,
                        exists: item.exists(),
                        ...item.data() as DocumentData
                    });
                });
                res.next(dataArray);
            });

        return {
            data: res.asObservable(),
            unsubscribe: snapshot,
        };
    }

}

You can see that now, as a result of FirestoreExtendedService.col$ and FirestoreExtendedService.doc$ I have an object like this:

{ data: Observable, unsubscribe: Unsubscribe }

I kinda don't like this approach. I would like to return the same results as I was with Firebase v8. I don't understand why they destroyed Observables. I don't like this new onSnapshot() method. It makes it difficult to me how should I manage the unsubscription.

I don't see this solution as a good approach. If I return an Observable and an Unsubscribe I should then unsubscribe both from the Observable and the Unsubscribe from the component that use this service. In addition, I should refactor the entire components to manage { data }.

Any advice on this?

0 Answers
Related