import db from './db'; // Set this to the max ops per batch listed in the Firestore docs // https://firebase.google.com/docs/firestore/manage-data/transactions export const MAX_BATCH_OPS = 500; // A batch processor that will accept any number of requests. // It processes batches in the maximum number of events allowed. // Does not guarantee atomicity across batches. // export default class DbBatch { private opsCount: number; private totalOpsCount: number; private batchCount: number; private currentBatch; private promises: Array>; constructor() { this.promises = []; this.opsCount = 0; this.totalOpsCount = 0; this.batchCount = 0; } // Add a new document at path create(path: string | Array, data: any) { const batch = this.getBatch(); const id = db.newId(); batch.set(db.path([path, id]), data); return id; } // Add or replace a document at path set(path: string | Array, data: any) { const batch = this.getBatch(); batch.set(db.path(path), data); } // Update properties on a document at path update(path: string | Array, data: any) { const batch = this.getBatch(); batch.update(db.path(path), data); } // Delete a single document at path delete(path: string | Array) { const batch = this.getBatch(); batch.delete(db.path(path)); } // Delete all docs in a collection (but not any child collections) deleteCollection(collectionName: string | Array) { const query = db.path(collectionName).orderBy('__name__'); this.deleteDocsMatchingQuery(query); } // Delete all docs matching the query provided, do not include // limit attributes on the query. Those are added internally. deleteDocsMatchingQuery(query: FirebaseFirestore.Query) { // Unfortunately, there's no synchronous way to update the totalOpsCount // based on number of docs affected, so we count this as a single DB op // We do return the number of docs deleted in the promise. this.totalOpsCount += 1; this.batchCount += 1; // Process batches of MAX_BATCH_OPS until we run out of docs const nextBatch = (resolve, reject, limitQuery, totalDocsDeleted = 0) => { const currentBatch = db.batch(); limitQuery.get().then(snapshot => { totalDocsDeleted += snapshot.size; if (snapshot.size > 0) { snapshot.docs.forEach(doc => { currentBatch.delete(doc.ref); }); currentBatch.commit().then(() => { process.nextTick(() => nextBatch(resolve, reject, limitQuery, totalDocsDeleted)); }); } else { resolve(totalDocsDeleted); } }) }; // For query-based deletes, there are going to be an unknown // number of batches (each making asynchronous calls). This means that // we can't depend on all the promises being collected in this.promises // before commitAll() is invoked. We solve this by using a single // promise and passing the resolve method indefinitely until we run out // of batches. this.promises.push(new Promise((resolve, reject) => { const limitQuery = query.limit(MAX_BATCH_OPS); nextBatch(resolve, reject, limitQuery); })); } // Get the current number of batch operations that have been sent. count() { return this.totalOpsCount; } // Resolves when all batch operations have been completed. commitAll() { if( this.currentBatch ) { this.promises.push(this.currentBatch.commit()); } this.currentBatch = null; return Promise.all(this.promises).then(() => this); } private getBatch() { if( this.opsCount === MAX_BATCH_OPS ) { this.promises.push(this.currentBatch.commit()); this.opsCount = 0; } if( this.opsCount === 0 ) { this.batchCount++; this.currentBatch = db.batch(); } this.opsCount++; this.totalOpsCount++; return this.currentBatch; } static deleteCollections(list: Array) { const batch = new DbBatch(); list.forEach(url => batch.deleteCollection(url)); return batch.commitAll(); } static loadSeedData(data) { const batch = new DbBatch(); Object.keys(data).forEach(docPath => { batch.set(docPath, data[docPath]); }); return batch.commitAll(); } static deleteRecursively(docPath: string|Array) { const batch = new DbBatch(); return this.recurseAndDelete(batch, db.path(docPath)).then(() => batch.commitAll()); } private static recurseAndDelete(batch: DbBatch, docRef: FirebaseFirestore.DocumentReference) { // Delete the current document batch.getBatch().delete(docRef); // Delete subcollections of the doc return docRef.listCollections().then(subcollections => { return Promise.all(subcollections.map(coll => { return coll.listDocuments().then(docs => { // For each document, rinse and repeat return Promise.all(docs.map(doc => { return this.recurseAndDelete(batch, doc); })); }); })); }) } }