Search code examples
firebasecallbackecmascript-6promisegoogle-cloud-functions

Firebase Function, turn Sync / Async Mess into clean ES6


I did write a firebase function which manipulates a PDF File on request, does some manipulations to it, saves it back to storage and archives a hash to database.

I did manage to get it to work, but its a total mess since I never really learned how to work with pre ES6 js callbacks. I'm new to all of this and learned to work with arrow functions and promises. But here I need to use packages which are pure JavaScript and its working somehow, but I really need to clean up this sync/async mess to do clean error handling and return a promise to firebase after processing the function.

My function also includes some odd file handlings to get the different files ready for the pdf library: For example i create a QR Code, save it to a tmp file, use another library to create a jpg out of the png and save it again to tmp. I am open to any suggestions or hints if I could do something smarter.

Something is wrong with the chain, because the function is completing in a few ms while it still running.

I did add some comments to my code where I have no clue how to change it to ES6 and I would be very grateful if you could help me clean up this big mess.

const hummus = require('hummus');
const request = require('request');
const fs = require('fs');
const sha1 = require('sha1');
const images = require("images");

exports.handleDocSignRequests = functions.database.ref('/user_writeable/docrequests/{uid}').onWrite(event => {
    var userReq = event.data.val();
    var userRef = event.data.ref;
    if (!userReq) return Promise.resolve();
    if (!userReq.docpath) return Promise.resolve();
    let uid = event.params.uid;
    let filename = userReq.docpath; // File to sign and hash

    return bucket.file(filename).getSignedUrl({  // getting downloadurl from Firebase Storage
        action: 'read'
    }).then(
        (downloadpath) => {
            downloadpath = downloadpath[0];
            //download pdf - how to turn this into a promise?
            download = function (uri, filename, callback) {
                request.head(uri, function (err, res, body) {
                    request(uri).pipe(fs.createWriteStream(filename)).on('close', callback);
                });
            };
            let pdfsourse = LOCAL_TMP_FOLDER + 'downloadedfile.pdf';
            return download(downloadpath, pdfsourse, function () { // download callback turn this into ES6
                console.log('download finished');
                let qrjpg = LOCAL_TMP_FOLDER + 'qrcode.jpg';
                var qrpng = LOCAL_TMP_FOLDER + 'qrcode.png';
                let qrurl = 'https://some.url/' + userReq.docid;
                let pdfdest = LOCAL_TMP_FOLDER + 'newpdf.pdf';
                let logfile = './hummus.log';

                QRCode.toFile(qrpng, qrurl, {  // how to make this a part of the "chain" and go on when finished
                    version: 4, type: "png", scale: 2, margin: 0
                }, function (err) {
                    if (err) throw err;
                    console.log('qr png ready');
                    images(qrpng).save(qrjpg, {operation: 100}); // save png as jpg
                    console.log('qr jpg ready');
                });


                // Doing stuff to PDF with HummusJs
                let pdfWriter = hummus.createWriterToModify(pdfsourse, {
                    modifiedFilePath: pdfdest,
                    log: logfile,
                    userPassword: 'user',
                    ownerPassword: 'owner',
                    userProtectionFlag: 4

                });
                let pdfReader = pdfWriter.getModifiedFileParser();
                let arialFont = pdfWriter.getFontForFile(ariallocal);
                let textOptions = {font: arialFont, size: 5, colorspace: 'gray', color: 0x00};

                for (let i = 0; i < pdfReader.getPagesCount(); ++i) {
                    let pageBox = pdfReader.parsePage(i).getMediaBox();
                    let pageModifier = new hummus.PDFPageModifier(pdfWriter, i, true);
                    let ctx = pageModifier.startContext().getContext();
                    ctx.writeText('Document saved', 5, 110, textOptions);
                    ctx.drawImage(5, 52, qrfile,
                        {
                            transformation: {
                                width: 40,
                                height: 40,
                                fit: 'always'
                            }
                        });
                    pageModifier.endContext().writePage();
                }
                pdfWriter.end();
                // How can I be sure PDF is done and written to tmp file? Or is this given by sync function?
                // Reading finished PDF from file again, to get base64 for hashing - is there a better way?
                let newpdf = fs.readFileSync(pdfdest);
                let base64pdf = newpdf.toString('base64');
                let hash = sha1(base64pdf);
                let signobj = {};
                signobj['hash'] = hash;
                // Check if document already in database, if not write hash to database,
                // upload finished pdf to original place and archive
                // and return remove request
                let sign_ref = docsign_ref.child(userReq.docid);
                return sign_ref.once('value').then(function (snap) {
                    if (!snap.val()) { //Document is new
                        let upload1 = bucket.upload(destcry, {destination: filename}).then
                        (suc => {
                            console.log('uploaded');
                        });
                        //
                        let upload2 = bucket.upload(destcry, {destination: 'signed/' + userReq.docid + '.pdf'}).then
                        (suc => {
                            console.log('uploaded');
                        });
                        return Promise.all([upload1, upload2]).then( // When both uploads are finished go on
                            (suc) => {
                                return sign_ref.set(signobj).then(
                                    (suc) => {
                                        // Remove Request and return Promise
                                        return userRef.remove();
                                    });

                            });
                    }
                    else {
                        //Document already in database, this should never happen, only for seq reasons
                        console.log('doc already in database);
                        return Promise.resolve();
                    }
                });
            });
        });
});

Solution

  • As I wrote in comments, your question is too broad. I will focus here on the question as you put it in the title.

    The main improvement when it comes to ES6 promises, is that you should:

    • turn some callback-based schemes to promises, and
    • flatten the promise chain, avoiding nested then calls

    Finally, when you call the outer function, make sure to treat it as a promise: once you have asynchronous stuff going on, you need to stick with that pattern. So you would call your main function like this:

    handleDocSignRequests().then( .... );
    

    Here is some untested(!) rewrite of your code to address the above two points. The main changes are commented with ***. There might be some mistakes, but the pattern should be clear:

    const hummus = require('hummus');
    const request = require('request');
    const fs = require('fs');
    const sha1 = require('sha1');
    const images = require("images");
    
    exports.handleDocSignRequests = functions.database.ref('/user_writeable/docrequests/{uid}').onWrite(event => {
        var userReq = event.data.val();
        var userRef = event.data.ref;
        if (!userReq) return Promise.resolve();
        if (!userReq.docpath) return Promise.resolve();
        let uid = event.params.uid;
        let filename = userReq.docpath; // File to sign and hash
    
        // *** Define these variables at this level, so they are accessible throughout
        //     the flattened promise-chain:
        let signobj = {};
        let sign_ref = docsign_ref.child(userReq.docid);
    
        return bucket.file(filename).getSignedUrl({  // getting downloadurl from Firebase Storage
            action: 'read'
        }).then((downloadpath) => {
            downloadpath = downloadpath[0];
            let pdfsourse = LOCAL_TMP_FOLDER + 'downloadedfile.pdf';
            // *** turned into promise:
            // *** return the promise to the main promise chain
            return new Promise(resolve => {
                request.head(downloadpath, function (err, res, body) {
                    request(downloadpath)
                        .pipe(fs.createWriteStream(pdfsourse))
                        .on('close', resolve);
                });
            });
        }).then(function () {
            console.log('download finished');
            let qrjpg = LOCAL_TMP_FOLDER + 'qrcode.jpg';
            var qrpng = LOCAL_TMP_FOLDER + 'qrcode.png';
            let qrurl = 'https://some.url/' + userReq.docid;
            // *** Return a promise to make this a part of the "chain" and go on when finished
            return new Promise( (resolve, reject) => {
                QRCode.toFile(qrpng, qrurl, {  
                    version: 4, type: "png", scale: 2, margin: 0
                }, function (err) {
                    if (err) reject(err); // *** reject 
                    console.log('qr png ready');
                    images(qrpng).save(qrjpg, {operation: 100}); // save png as jpg
                    console.log('qr jpg ready');
                    resolve(); // *** resolve the promise now it's done
                });
            });
        }).then(function () {
            // Doing stuff to PDF with HummusJs
            let pdfdest = LOCAL_TMP_FOLDER + 'newpdf.pdf';
            let logfile = './hummus.log';
            let pdfWriter = hummus.createWriterToModify(pdfsourse, {
                modifiedFilePath: pdfdest,
                log: logfile,
                userPassword: 'user',
                ownerPassword: 'owner',
                userProtectionFlag: 4
            });
            let pdfReader = pdfWriter.getModifiedFileParser();
            let arialFont = pdfWriter.getFontForFile(ariallocal);
            let textOptions = {font: arialFont, size: 5, colorspace: 'gray', color: 0x00};
    
            for (let i = 0; i < pdfReader.getPagesCount(); ++i) {
                let pageBox = pdfReader.parsePage(i).getMediaBox();
                let pageModifier = new hummus.PDFPageModifier(pdfWriter, i, true);
                let ctx = pageModifier.startContext().getContext();
                ctx.writeText('Document saved', 5, 110, textOptions);
                ctx.drawImage(5, 52, qrfile, {
                    transformation: {
                        width: 40,
                        height: 40,
                        fit: 'always'
                    }
                });
                pageModifier.endContext().writePage();
            }
            pdfWriter.end();
            // How can I be sure PDF is done and written to tmp file? Or is this given by sync function?
            // Reading finished PDF from file again, to get base64 for hashing - is there a better way?
            let newpdf = fs.readFileSync(pdfdest);
            let base64pdf = newpdf.toString('base64');
            let hash = sha1(base64pdf);
            signobj['hash'] = hash;
            // Check if document already in database, if not write hash to database,
            // upload finished pdf to original place and archive
            // and return remove request
            // *** Only return the basic promise. Perform the `then` on the outer chain.
            return sign_ref.once('value');
        }).then(function (snap) {
            // *** Do simple case first:
            if (snap.val()) { 
                //Document already in database, this should never happen, only for seq reasons
                console.log('doc already in database);
                return Promise.resolve();
            } // *** No need for `else` here. The above `return` is enough
            //Document is new
            // *** use array to replace two blocks of very similar code
            let promises = [filename, 'signed/' + userReq.docid + '.pdf'].map(destination =>
                return bucket.upload(destcry, {destination}).then(suc => {
                    console.log('uploaded');
                });
            });
            // *** Return the basic promise. Perform the `then` on the outer chain
            return Promise.all(promises);// Wait for both uploads to finish
        }).then( suc => {
            // *** I suppose this also has to run when not a new document....
            // *** Again: return the promise and perform the `then` on the outer chain
            return sign_ref.set(signobj);
        }).then( suc => {
            // Remove Request and return Promise
            return userRef.remove();
        });
    });