Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Firestore pagination - Is there any query compatible with firebase's limitToLast?

Is there a way to implement back pagination with firestore? I am struggling to implement pagination with firestore, and there are limited firestore queries for it. Forward pagination can be made by startAt and limit method, that is ok. But back pagination can't be easily done, because we only have endBefore, and endAt method, and how can we get last n elements from given document? I know realtime database have method limitToLast. Is there any query like this for firestore? (Also I need to implement multiple sorting, so getting last documents with "ASC" or "DESC" sorting will not work) Help much appreciated.

Thanks!

like image 885
Tiago Romao Avatar asked Jan 29 '23 20:01

Tiago Romao


2 Answers

The equivalent to the limitToLast(...) operation from the Firebase Realtime Database in Cloud Firestore is to order the data descending (which is possible in Firestore) and then just limit(...). If you're having problems implement this, update your question to show what you've done.

I agree that this is a sub-optimal API for back-pagination, since you're receiving the items in reverse order.

like image 115
Frank van Puffelen Avatar answered Feb 08 '23 15:02

Frank van Puffelen


Simpler answer: Firestore now has .limitToLast(), which works exactly as you think it does. Used in my own (guess I need to publish it soon) Firestore Wrapper:

//////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////
// *** Paginate API ***

export const PAGINATE_INIT = 0;
export const PAGINATE_PENDING = -1;
export const PAGINATE_UPDATED = 1;
export const PAGINATE_DEFAULT = 10;
export const PAGINATE_CHOICES = [10, 25, 50, 100, 250, 500];

/**
 * @classdesc
 * An object to allow for paginating a table read from Firestore. REQUIRES a sorting choice
 * @property {Query} Query that forms basis for the table read
 * @property {number} limit page size
 * @property {QuerySnapshot} snapshot last successful snapshot/page fetched
 * @property {enum} status status of pagination object
 * @method PageForward pages the fetch forward
 * @method PageBack pages the fetch backward
 */

export class PaginateFetch {
  Query = null;
  limit = PAGINATE_DEFAULT;
  snapshot = null;
  status = null; // -1 pending; 0 uninitialize; 1 updated;
  /**
   * ----------------------------------------------------------------------
   * @constructs PaginateFetch constructs an object to paginate through large
   * Firestore Tables
   * @param {string} table a properly formatted string representing the requested collection
   * - always an ODD number of elements
   * @param {array} filterArray an (optional) 3xn array of filter(i.e. "where") conditions
   * @param {array} sortArray a 2xn array of sort (i.e. "orderBy") conditions
   * @param {ref} ref (optional) allows "table" parameter to reference a sub-collection
   * of an existing document reference (I use a LOT of structered collections)
   *
   * The array is assumed to be sorted in the correct order -
   * i.e. filterArray[0] is added first; filterArray[length-1] last
   * returns data as an array of objects (not dissimilar to Redux State objects)
   * with both the documentID and documentReference added as fields.
   * @param {number} limit (optional)
   * @returns {PaginateFetchObject}
   **********************************************************************/

  constructor(
    table,
    filterArray = null,
    sortArray = null,
    ref = null,
    limit = PAGINATE_DEFAULT
  ) {
    const db = ref ? ref : fdb;

    this.limit = limit;
    this.Query = sortQuery(
      filterQuery(db.collection(table), filterArray),
      sortArray
    );
    this.status = PAGINATE_INIT;
  }

  /**
   * @method Page
   * @returns Promise of a QuerySnapshot
   */
  PageForward = () => {
    const runQuery = this.snapshot
      ? this.Query.startAfter(_.last(this.snapshot.docs))
      : this.Query;

    this.status = PAGINATE_PENDING;

    return runQuery
      .limit(this.limit)
      .get()
      .then((QuerySnapshot) => {
        this.status = PAGINATE_UPDATED;
        //*IF* documents (i.e. haven't gone beyond start)
        if (!QuerySnapshot.empty) {
          //then update document set, and execute callback
          //return Promise.resolve(QuerySnapshot);
          this.snapshot = QuerySnapshot;
        }
        return this.snapshot.docs.map((doc) => {
          return {
            ...doc.data(),
            Id: doc.id,
            ref: doc.ref
          };
        });
      });
  };

  PageBack = () => {
    const runQuery = this.snapshot
      ? this.Query.endBefore(this.snapshot.docs[0])
      : this.Query;

    this.status = PAGINATE_PENDING;

    return runQuery
      .limitToLast(this.limit)
      .get()
      .then((QuerySnapshot) => {
        this.status = PAGINATE_UPDATED;
        //*IF* documents (i.e. haven't gone back ebfore start)
        if (!QuerySnapshot.empty) {
          //then update document set, and execute callback
          this.snapshot = QuerySnapshot;
        }
        return this.snapshot.docs.map((doc) => {
          return {
            ...doc.data(),
            Id: doc.id,
            ref: doc.ref
          };
        });
      });
  };
}

/**
 * ----------------------------------------------------------------------
 * @function filterQuery
 * builds and returns a query built from an array of filter (i.e. "where")
 * consitions
 * @param {Query} query collectionReference or Query to build filter upong
 * @param {array} filterArray an (optional) 3xn array of filter(i.e. "where") conditions
 * @returns Firestor Query object
 */
export const filterQuery = (query, filterArray = null) => {
  return filterArray
    ? filterArray.reduce((accQuery, filter) => {
        return accQuery.where(filter.fieldRef, filter.opStr, filter.value);
      }, query)
    : query;
};

/**
 * ----------------------------------------------------------------------
 * @function sortQuery
 * builds and returns a query built from an array of filter (i.e. "where")
 * consitions
 * @param {Query} query collectionReference or Query to build filter upong
 * @param {array} sortArray an (optional) 2xn array of sort (i.e. "orderBy") conditions
 * @returns Firestor Query object
 */
export const sortQuery = (query, sortArray = null) => {
  return sortArray
    ? sortArray.reduce((accQuery, sortEntry) => {
        return accQuery.orderBy(sortEntry.fieldRef, sortEntry.dirStr || "asc");
        //note "||" - if dirStr is not present(i.e. falsy) default to "asc"
      }, query)
    : query;
};

I also have the equivalent for CollectionGroup queries, and listeners for each as well.

like image 43
LeadDreamer Avatar answered Feb 08 '23 15:02

LeadDreamer