Skip to content

Instantly share code, notes, and snippets.

@samthecodingman
Last active April 7, 2021 04:22
Show Gist options
  • Save samthecodingman/aea3bc9481bbab0a7fbc72069940e527 to your computer and use it in GitHub Desktop.
Save samthecodingman/aea3bc9481bbab0a7fbc72069940e527 to your computer and use it in GitHub Desktop.
Drop-in TypeScript & JavaScript helper functions to help fetch a list of Cloud Firestore documents by their IDs.
/*! firestore-fetch-documents-by-id.js | Samuel Jones 2021 | MIT License | gist.github.com/samthecodingman */
// on server:
import * as firebase from "firebase-admin";
// on client:
// import firebase from "firebase/app";
// import "firebase/firestore";
/** splits array `arr` into chunks of max size `n` */
function chunkArr(arr, n) {
if (n <= 0) throw new Error("n must be greater than 0");
return Array
.from({length: Math.ceil(arr.length/n)})
.map((_, i) => arr.slice(n*i, n*(i+1)))
}
/**
* Fetch documents with the given IDs from the given collection query or
* collection group query.
*
* **Note:** If a particular document ID is not found, it is silently omitted
* from results.
*
* **Note:** When used on a collection group query, you must specify the full document
* path as the ID as required by the Firestore API, or an error will be thrown.
* As an example, searching for the users who have a particular message ID in their
* data is not possible with this function
* (like `fetchDocumentsWithId(userMessagesCollectionGroup, ["someMessageId", ...])`)
* but searching specific users is, as long as you pass in the expected document path
* (like `fetchDocumentsWithId(userMessagesCollectionGroup, ["users/userA/userMessages/someMessageId", ...])`).
* For that functionality, you would be better off embedding the document ID in
* each document and using `where("_id", "in", ids)`.
*
* This utility function has two modes:
* * If `forEachCallback` is omitted, this function returns an array containing
* all found documents.
* * If `forEachCallback` is given, this function returns undefined, but found
* document is passed to `forEachCallback`.
*
* @param {firebase.firestore.Query} collectionQueryRef A collection-based query
* to search for documents
* @param {string[]} arrayOfIds An array of document IDs to retrieve
* @param {((firebase.firestore.QueryDocumentSnapshot) => void) | undefined} [forEachCallback=undefined]
* A callback to be called with a `QueryDocumentSnapshot` for each document found.
* @param {*} [forEachThisArg=null] The `this` binding for the callback.
* @return {*} an array of `QueryDocumentSnapshot` objects (in no callback mode) or `undefined` (in callback mode)
*
* @author Samuel Jones 2021 (samthecodingman) [MIT License]
*/
async function fetchDocumentsWithId(collectionQueryRef, arrayOfIds, forEachCallback = undefined, forEachThisArg = null) {
// in batches of 10, fetch the documents with the given ID from the collection
const fetchDocsPromises = chunkArr(arrayOfIds, 10)
.map((idsInChunk) => (
collectionQueryRef
.where(firebase.firestore.FieldPath.documentId(), "in", idsInChunk)
.get()
))
// after all documents have been retrieved:
// - if forEachCallback has been given, call it for each document and return nothing
// - if forEachCallback hasn't been given, return all of the QueryDocumentSnapshot objects collected into one array
return Promise.all(fetchDocsPromises)
.then((querySnapshotArray) => {
if (forEachCallback !== void 0) {
// callback mode: call the callback for each document returned
for (let querySnapshot of querySnapshotArray) {
querySnapshot.forEach(forEachCallback, forEachThisArg)
}
return;
}
// no callback mode: get all documents as an array
const allDocumentSnapshotsArray = [];
for (let querySnapshot of querySnapshotArray) {
querySnapshot.forEach(doc => allDocumentSnapshotsArray.push(doc))
}
return allDocumentSnapshotsArray;
});
}
/*! firestore-fetch-documents-by-id.ts | Samuel Jones 2021 | MIT License | gist.github.com/samthecodingman */
// on server:
// import * as firebase from "firebase-admin";
// on client:
// import firebase from "firebase/app";
// import "firebase/firestore";
/**
* Splits the given array into chunks with a maximum size of `n`.
*
* @param {T[]} arr The original array to split into chunks
* @param {number} n The maximum length of a chunk
* @return {T[][]} The array split into chunks
*
* @author Samuel Jones 2021 (samthecodingman) [MIT License]
*/
function chunkArr<T>(arr: T[], n: number): T[][] {
if (n <= 0) throw new Error("n must be greater than 0");
return Array
.from({length: Math.ceil(arr.length/n)})
.map((_, i) => arr.slice(n*i, n*(i+1)))
}
/**
* Fetch documents with the given IDs from the given collection query or
* collection group query.
*
* **Note:** If a particular document ID is not found, it is silently omitted
* from results.
*
* **Note:** When used on a collection group query, you must specify the full document
* path as the ID as required by the Firestore API, or an error will be thrown.
* As an example, searching for the users who have a particular message ID in their
* data is not possible with this function
* (like `fetchDocumentsWithId(userMessagesCollectionGroup, ["someMessageId", ...])`)
* but searching specific users is, as long as you pass in the expected document path
* (like `fetchDocumentsWithId(userMessagesCollectionGroup, ["users/userA/userMessages/someMessageId", ...])`).
* For that functionality, you would be better off embedding the document ID in
* each document and using `where("_id", "in", ids)`.
*
* This utility function has two modes:
* * If `forEachCallback` is omitted, this function returns an array containing
* all found documents.
* * If `forEachCallback` is given, this function returns undefined, but found
* document is passed to `forEachCallback`.
*
* @param {firebase.firestore.Query} collectionQueryRef A collection-based query
* to search for documents
* @param {string[]} arrayOfIds An array of document IDs to retrieve
* @param {((firebase.firestore.QueryDocumentSnapshot) => void) | undefined} [forEachCallback=undefined]
* A callback to be called with a `QueryDocumentSnapshot` for each document found.
* @param {*} [forEachThisArg=null] The `this` binding for the callback.
* @return {*} an array of `QueryDocumentSnapshot` objects (in no callback mode) or `undefined` (in callback mode)
*
* @author Samuel Jones 2021 (samthecodingman) [MIT License]
*/
async function fetchDocumentsWithId(collectionQueryRef: firebase.firestore.Query, arrayOfIds: string[], forEachCallback?: (() => void), forEachThisArg: any = null) {
// in batches of 10, fetch the documents with the given ID from the collection
const fetchDocsPromises = chunkArr(arrayOfIds, 10)
.map((idsInChunk) => (
collectionQueryRef
.where(firebase.firestore.FieldPath.documentId(), "in", idsInChunk)
.get()
))
// after all documents have been retrieved:
// - if forEachCallback has been given, call it for each document and return nothing
// - if forEachCallback hasn't been given, return all of the QueryDocumentSnapshot objects collected into one array
return Promise.all(fetchDocsPromises)
.then((querySnapshotArray) => {
if (forEachCallback !== void 0) {
// callback mode: call the callback for each document returned
for (let querySnapshot of querySnapshotArray) {
querySnapshot.forEach(forEachCallback, forEachThisArg)
}
return;
}
// no callback mode: get all documents as an array
const allDocumentSnapshotsArray: firebase.firestore.QueryDocumentSnapshot[] = [];
for (let querySnapshot of querySnapshotArray) {
querySnapshot.forEach(doc => allDocumentSnapshotsArray.push(doc))
}
return allDocumentSnapshotsArray;
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment