Reputation: 333
I have been stuck with this error for the past 3 days and i have tried literally everything, tried to structure the promises in a 1000 ways but nothing seems to work. Maybe I am losing the "big picture" so hopefully new eyes will help. Thanks for reading:
I have a scheduled function running in Firebase Cloud Functions. What the code tries to accomplish is
In my latest attempt (copied below) I check if there is a document in the snapshot (which would mean that there is another document of the same type, therefore the doc doesnt have to be deleted). Then if res!== true, I would delete the document.
The problem is that for some reason, res is never true.... maybe the "res" promise resolves before the "snapshot" promise?
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
exports.scheduledFunction = functions.pubsub
.schedule('0 23 * * *').timeZone('Europe/Madrid')
.onRun( async (context) => {
const expiredDocs = await admin.firestore().collection('PROMOTIONS_INFO')
.where('active','==',true)
.where('expiration', '<=', new Date())
.get()
.then(async (snapshot) => {
await Promise.all(snapshot.docs.map( async (doc) => {
doc.ref.update({active: false})
const type = doc.data().business.type
const id = doc.data().id
const exists = await admin.firestore().collection('PROMOTIONS_INFO')
.where('active','==',true)
.where('business.type','==', type)
.where('id', '!=', id)
.limit(1)
.get()
.then((snapshot) => {
snapshot.docs.map((doc)=>{return true})
}).then(async (res) => {
res===true ? null :
(await admin.firestore().collection('PROMOTIONS_INFO').doc('types')
.update('types', admin.firestore.FieldValue.arrayRemove(type)))
})
}))
});
});
Upvotes: 2
Views: 415
Reputation: 26296
To achieve your desired result, you might want to consider making use of Batched Writes and splitting your code into distinct steps.
One possible set of steps is:
In the above steps, step 3 can make use of Batched Writes and step 6 can make use of the arrayRemove()
field transform which can remove multiple elements at once to ease the burden on your database.
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
exports.scheduledFunction = functions.pubsub
.schedule('0 23 * * *').timeZone('Europe/Madrid')
.onRun( async (context) => {
// get instance of Firestore to use below
const db = admin.firestore();
// this is reused often, so initialize it once.
const promotionsInfoColRef = db.collection('PROMOTIONS_INFO');
// find all documents that are active and have expired.
const expiredDocsQuerySnapshot = await promotionsInfoColRef
.where('active','==',true)
.where('expiration', '<=', new Date())
.get();
if (expiredDocsQuerySnapshot.empty) {
// no expired documents, log the result
console.log(`No documents have expired recently.`);
return; // done
}
// initialize an object to store all the types to be checked
// this helps ensure each type is checked only once
const typesToCheckObj = {};
// initialize a batched write to make changes all at once, rather than call out to Firestore multiple times
// note: batches are limited to 500 read/write operations in a single batch
const makeDocsInactiveBatch = db.batch();
// for each snapshot, add their type to typesToCheckObj and update them to inactive
expiredDocsQuerySnapshot.forEach(doc => {
const type = doc.get("business.type"); // rather than use data(), parse only the property you need.
typesToCheckObj[type] = true; // add this type to the ones to check
makeDocsInactiveBatch.update(doc.ref, { active: false }); // add the "update to inactive" operation to the batch
});
// update database for all the now inactive documents all at once.
// we update these documents first, so that the type check are done against actual "active" documents.
await makeDocsInactiveBatch.commit();
// this is a unique array of the types encountered above
// this can now be used to check each type ONCE, instead of multiple times
const typesToCheckArray = Object.keys(typesToCheckObj);
// check each type and return types that have no active promotions
const typesToRemoveArray = (await Promise.all(
typesToCheckArray.map((type) => {
return promotionsInfoColRef
.where('active','==',true)
.where('business.type','==', type)
.limit(1)
.get()
.then((querySnapshot) => querySnapshot.empty ? type : null) // if empty, include the type for removal
})
))
.filter((type) => type !== null); // filter out the null values that represent types that don't need removal
// typesToRemoveArray is now a unique list of strings, containing each type that needs to be removed
if (typesToRemoveArray.length == 0) {
// no types need removing, log the result
console.log(`Updated ${expiredDocsQuerySnapshot.size} expired documents to "inactive" and none of the ${typesToCheckArray.length} unique types encountered needed to be removed.`);
return; // done
}
// get the types document reference
const typesDocRef = promotionsInfoColRef.doc('types');
// use the arrayRemove field transform to remove all the given types at once
await typesDocRef.update({types: admin.firestore.FieldValue.arrayRemove(...typesToRemoveArray) });
// log the result
console.log(`Updated ${expiredDocsQuerySnapshot.size} expired documents to "inactive" and ${typesToRemoveArray.length}/${typesToCheckArray.length} unique types encountered needed to be removed.\n\nThe types removed: ${typesToRemoveArray.sort().join(", ")}`);
Note: Error checking is omitted and should be implemented.
If you ever expect to hit the 500 operations per batch limit, you can add a wrapper around batches so they automatically get split up as required. One possible wrapper is included here:
class MultiBatch {
constructor(dbRef) {
this.dbRef = dbRef;
this.batchOperations = [];
this.batches = [this.dbRef.batch()];
this.currentBatch = this.batches[0];
this.currentBatchOpCount = 0;
this.committed = false;
}
/** Used when for basic update operations */
update(ref, changesObj) {
if (this.committed) throw new Error('MultiBatch already committed.');
if (this.currentBatchOpCount + 1 > 500) {
// operation limit exceeded, start a new batch
this.currentBatch = this.dbRef.batch();
this.currentBatchOpCount = 0;
this.batches.push(this.currentBatch);
}
this.currentBatch.update(ref, changesObj);
this.currentBatchOpCount++;
}
/** Used when an update contains serverTimestamp, arrayUnion, arrayRemove, increment or decrement (which all need to be counted as 2 operations) */
transformUpdate(ref, changesObj) {
if (this.committed) throw new Error('MultiBatch already committed.');
if (this.currentBatchOpCount + 2 > 500) {
// operation limit exceeded, start a new batch
this.currentBatch = this.dbRef.batch();
this.currentBatchOpCount = 0;
this.batches.push(this.currentBatch);
}
this.currentBatch.update(ref, changesObj);
this.currentBatchOpCount += 2;
}
commit() {
this.committed = true;
return Promise.all(this.batches.map(batch => batch.commit()));
}
}
To use this, swap out db.batch()
in the original code for new MultiBatch(db)
. If an update in the batch (like someBatch.update(ref, { ... })
) contains a field transform (such as FieldValue.arrayRemove()
), make sure to use someMultiBatch.transformUpdate(ref, { ... })
instead so that single update is correctly counted as 2 operations (a read and a write).
Upvotes: 1
Reputation: 158
snapshot.docs.map((doc)=>{return true})
returns Array like [true, false]
not boolean like true
.
So .then( async (res) => { res===true ? null : await admin.firestore(...
cannot work.
and
Maybe you should modify like the following.
.then((snapshot) =>
snapshot.docs.length > 0 ? null :
await admin.firestore(...
Upvotes: 0
Reputation: 1050
I'm assuming in this case that res
is undefined and is evaluating as falsey.
Before your .then
promise with the res
parameter, you have a previous .then
promise that returns void:
//...
.then((snapshot) => {
snapshot.docs.map((doc)=>{return true}) // <--- This is not actually returning a resolved value
}).then(async (res) => {
res===true ? null :
(await admin.firestore().collection('PROMOTIONS_INFO').doc('types')
.update('types', admin.firestore.FieldValue.arrayRemove(type)))
})
//...
Depending on your intent, you'll need to return a value in this previous promise. It looks like you're creating an array of boolean values that is the length of the number of snapshot.docs
that you have, so if you put a return statement simply in the previous .then
clause, res
would be something like, [true, true, true, true, /* ... */]
//...
.then((snapshot) => {
return snapshot.docs.map((doc)=>{return true})
}).then(async (res) => {
res===true ? null :
(await admin.firestore().collection('PROMOTIONS_INFO').doc('types')
.update('types', admin.firestore.FieldValue.arrayRemove(type)))
})
//...
Upvotes: 1