0

I'm trying to collect all the documents from a specific file and transform them into a Sendgrid attachment object with a content and filename property.

Step 1 and 2 are working. After step 2 I have an array of objects with some document data like the file name, type, storage URL etc.

In step 3 I want to fetch the actual file based on the storage URL and create a Sendgrid attachment object. It requires content and filename as properties.

However, with my current code, the attachments array stays empty when I'm logging the variable.

My code:

export const onStatusChanged = functions.database.ref(`files/{fileID}/general/status`).onUpdate(async (change, context) => {
    const prevStatus = change.before.val();
    const currentStatus = change.after.val();

    if (prevStatus === currentStatus) return null;

    if (currentStatus === 'email') {
        // 1. Get file data
        const snapshot = await change.after.ref.parent.parent.once('value');
        const file = snapshot.val();

        // 2. Get documents
        const documents = getDocuments(file.documents);
        console.log(documents);

        // 3. Create attachments
        const attachments = [];

        documents.forEach(document => {
            axios.get(document.url, { responseType: 'arraybuffer' }).then(image => {
                attachments.push({ content: Buffer.from(image.data).toString('base64'), filename: document.name });
            }).catch(error => {
                console.log(error)
            })
        });

        console.log(attachments) // []

        // 4. Create email object

        // 5. Send email
    }

    return null;
})

I thought by using a promise my code is synchronous?

EDIT: first I had this code

    // 3. Create attachments
    const attachments = documents.map(document => {
        const image = await axios.get(document.url, { responseType: 'arraybuffer' });
        return attachments.push({ content: Buffer.from(image.data).toString('base64'), filename: document.name });
    })

    console.log(attachments) // [ Promise { <pending> } ]
Thore
  • 1,918
  • 2
  • 25
  • 50
  • "I thought by using a promise my code is synchronous?" -> as you discovered, that's not the case. There are different ways to sequentialize async code -- we used to do `async.waterfall` and maybe this is still done. You might be thinking about using `await`. This new keyword makes async code _look_ synchronous, though it is still definitely async. Have you tried `await` in your `forEach`? – Ray Toal Mar 07 '19 at 03:30
  • @RayToal: First I did use await with a `map()` function. Added this code – Thore Mar 07 '19 at 03:41

2 Answers2

1

I think you misunderstand the use of promises specifically with Cloud Functions. With Cloud Functions background triggers, you are obliged to return a promise that resolves when all the async work is fully complete, else the work will be shut down. Right now you are returning null, which isn't correct.

Also, attachements doesn't seem to be accumulating promises. It's accumulating other objects that are not useful for async programming.

Read more in the documentation: https://firebase.google.com/docs/functions/terminate-functions

Doug Stevenson
  • 297,357
  • 32
  • 422
  • 441
  • I will check the documentation and come back afterward. Thanks for the url. – Thore Mar 07 '19 at 03:44
  • Changed the code to async/await and wrapped the loop in `Promise.all()`. At the end of the if-condition (step 5) I will return a promise when sending the actual email. – Thore Mar 07 '19 at 04:21
0

Changed the code back to async/await and added Promise.all(). It's working like I want now.

The code:

    const attachments = await Promise.all(documents.map(async document => {
        const image = await axios.get(document.url, { responseType: 'arraybuffer' });
        return { content: Buffer.from(image.data).toString('base64'), filename: document.name };
    }));
Thore
  • 1,918
  • 2
  • 25
  • 50