Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cloud Function http request with multiple batch read & writes

I have a cloud function triggered by an http request which intends to do the following:

  1. Get a certain amount of documents based on a query.
  2. For each document of the query perform a read operation.
  3. After getting the new document from (2), perform some read/write operations (delete from a subcollection, add the document to another subcollection, and update a document on a root collection).

Therefore I need something that waits for the looping of (2) and (3) and then performs a batch operation.

Below is the code that I have at the moment and is kind of working when I test the function locally. However I cannot deploy it to Firebase since it has promises errors such as "every then must return a promise" and "avoid nesting promises".

exports.finishEvents =  functions.https.onRequest((req, res) => {
  const eventsRef = admin.firestore().collection('events');
  var currentTime = new Date().getTime();
  var currentTimeMinus1h = currentTime - 3600000;

  console.log('----- finishEvents started -----')

  const queryRef = eventsRef.where('finished', '==', false).where('date', '<=', new Date(currentTimeMinus1h)).get().then(function(querySnapshot){
    if (querySnapshot.size > 0) {
        querySnapshot.forEach(function(doc) {

          var owner_id = doc.data().owner_id;
          var event_id = doc.id;
          console.log(owner_id, event_id);

          var userEventOwnerGoingRef = admin.firestore().collection("user_events").doc(owner_id).collection('going').doc(event_id);
          userEventOwnerGoingRef.get().then(doc2 => {
            if (!doc2.exists) {
              console.log('No such document!');
            } else {
              console.log('Document data:', doc2.data());
              var goingIds = doc.data().going_ids;
              console.log('GOING IDS', goingIds);
              var batch = admin.firestore().batch();
              for (var userId in goingIds) {
                if (goingIds.hasOwnProperty(userId)) {
                  console.log(userId + " -> " + goingIds[userId]);
                  var eventRef = admin.firestore().collection("events").doc(event_id);
                  var userEventGoingRef = admin.firestore().collection("user_events").doc(userId).collection('going').doc(doc2.id);
                  var userEventAttendedRef = admin.firestore().collection("user_events").doc(userId).collection('attended').doc(doc2.id);
                  batch.set(userEventAttendedRef, doc2.data());
                  batch.delete(userEventGoingRef)
                  if (userId == doc2.data().owner_id) batch.update(eventRef, {finished: true});
                }
              }
              batch.commit().then(function () {
                return res.status(200).send("Done.");
              });
            }
          })
         .catch(err => {
           console.log('Error getting userEventOwnerGoingRef', err);
           return res.status(200).send("Finished.");
         });
       });
    } else {
        console.log("No events found");
        return res.status(200).send("Finished.");
    }
  })
  .catch(err => {
      console.log('Error getting events', err);
      return res.status(200).send("Finished.");
  });
});

When I test it locally, even though the job is completed, I get an error stating that

UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: Can't set headers after they are sent.

I can see that I am sending the result for each document of the original query, and I would only need to send the result once to finish the cloud function.

I guess I need to return promises and then after steps (2) and (3) are completed perform my batch transaction of everything. However is the first time I am using javascript and I am struggling with this. Any help would be appreciated.

like image 811
b-fg Avatar asked May 30 '18 15:05

b-fg


1 Answers

The Unhandled promise rejection error is encountered when your HTTPS function has sent a response but forgot to return a promise that resolves before the timeout limit has been reached. This means that you aren't returning all of your promises in the HTTPS function. Your code should look something like this:

exports.finishEvents =  functions.https.onRequest((req, res) => {
  const eventsRef = admin.firestore().collection('events')
  const currentTime = new Date().getTime()
  const currentTimeMinus1h = currentTime - 3600000

  console.log('----- finishEvents started -----')

  const queryRef = eventsRef
    .where('finished', '==', false)
    .where('date', '<=', new Date(currentTimeMinus1h))

  return queryRef.get().then((querySnapshot) => {
    // Use Promise.all with snapshot.docs.map to combine+return Promise context
    return Promise.all(querySnapshot.docs.map((doc) => {
      const owner_id = doc.get('owner_id')
      const event_id = doc.id
      console.log(owner_id, event_id)

      const userEventOwnerGoingRef = admin.firestore()
        .collection("user_events").doc(owner_id)
        .collection('going').doc(event_id)
      return userEventOwnerGoingRef.get().then((doc2) => {
        if (!doc2.exists) {
          console.log('No such document!')
          return
        } else {
          console.log('Document data:', doc2.data())
          const goingIds = doc.get('going_ids')
          console.log('GOING IDS', goingIds)
          const batch = admin.firestore().batch()
          for (const userId in goingIds) {
            if (goingIds.hasOwnProperty(userId)) {
              console.log(userId + " -> " + goingIds[userId])
              const eventRef = admin.firestore().collection("events").doc(event_id)
              const userEventGoingRef = admin.firestore()
                .collection("user_events").doc(userId).collection('going').doc(doc2.id)
              const userEventAttendedRef = admin.firestore()
                .collection("user_events").doc(userId).collection('attended').doc(doc2.id)
              batch.set(userEventAttendedRef, doc2.data())
              batch.delete(userEventGoingRef)
              if (userId == doc2.get('owner_id')) {
                batch.update(eventRef, {finished: true})
              }
            }
          }
          return batch.commit()
        }
      })
    }))
  })
  .then(() => {
    return res.status(200).send('Done.')
  })
  .catch((err) => {
    console.error(err)
    return res.status(200).send('Finished.')
  })
})

The important thing to take from this is to not strand any of your promises. Always keep a handle on them whether it be by adding them to an array and waiting for them all to resolve/reject, or by returning them from their scopes/functions. I hope this helps.

like image 136
Bryan Massoth Avatar answered Oct 26 '22 16:10

Bryan Massoth