node.jsfirebasegoogle-cloud-functionsgoogle-cloud-storagedocxtemplater

Firebase Storage + docxtemplater in nodejs


I am having trouble loading a firebase storage document in node js (preferably in binary) so that I can generate a docxtemplater document on it. I'm quite new to docxtemplater and would really like to use it for my webapp

Is this something that can be done?

Below is the code I get but I dont think it's loading the document from my firebase storage properly:

const functions = require('firebase-functions');
const admin = require('firebase-admin');
const {Storage} = require('@google-cloud/storage');
var PizZip = require('pizzip');
var Docxtemplater = require('docxtemplater');
admin.initializeApp();
const BUCKET = 'gs://mpcwapp.appspot.com';

    const https = require('https');
    
    const storage = new Storage({
    projectId: 'myapp' });
    const cors = require('cors')({origin: true});

exports.test2 = functions.https.onCall((data, context) => {
// The error object contains additional information when logged with JSON.stringify (it contains a properties object containing all suberrors).
function replaceErrors(key, value) {
    if (value instanceof Error) {
        return Object.getOwnPropertyNames(value).reduce(function(error, key) {
            error[key] = value[key];
            return error;
        }, {});
    }
    return value;
}

function errorHandler(error) {
    console.log(JSON.stringify({error: error}, replaceErrors));

    if (error.properties && error.properties.errors instanceof Array) {
        const errorMessages = error.properties.errors.map(function (error) {
            return error.properties.explanation;
        }).join("\n");
        console.log('errorMessages', errorMessages);
        // errorMessages is a humanly readable message looking like this :
        // 'The tag beginning with "foobar" is unopened'
    }
    throw error;
}

//Load the docx file as a binary
let file_name = 'input.docx';
const myFile =storage.bucket(BUCKET).file(file_name);
var content =  myFile.createReadStream();

var zip = new PizZip(content);
var doc;
try {
    doc = new Docxtemplater(zip);
} catch(error) {
    // Catch compilation errors (errors caused by the compilation of the template : misplaced tags)
    errorHandler(error);
}

//set the templateVariables
doc.setData({
    first_name: 'John',
    last_name: 'Doe',
    phone: '0652455478',
    description: 'New Website'
});

try {
    // render the document (replace all occurences of {first_name} by John, {last_name} by Doe, ...)
    doc.render();
}
catch (error) {
    // Catch rendering errors (errors relating to the rendering of the template : angularParser throws an error)
    errorHandler(error);
}
var buf = doc.getZip()
             .generate({type: 'nodebuffer'});

buf.pipe(myFile.createWriteStream());

});

Any help will be appreciated I am really stuck.


Solution

  • First, if you're deploying your code logic in Firebase Functions, make sure it's inside your list of exports.

    I tried to reproduce the behavior of your code and noticed that the root cause of the error is because of this part in your code:

    var content =  myFile.createReadStream();
    var zip = new PizZip(content);
    

    pizzip appears to accept a Buffer input according to this documentation. However, createReadStream() returns ReadableStream so there's a mismatch between the required parameters.

    There are two solutions I can think of:

    1. First, download and store the file (in /tmp). Then read the file using fs.readFileSync().
    2. Skip saving the file to the file system and get the buffer of the file object.

    For the 2nd option, you need to understand how streams work. This answer can give you a good head start. As example, you can get the buffer from ReadableStream like this:

    const remoteFile = storage.bucket("bucket-name").file("file-name")
    const readable = remoteFile.createReadStream()
    
    var buffers = [];
    readable.on('data', (buffer) => {
      buffers.push(buffer)
    });
    readable.on('end', () => {
      var buffer = Buffer.concat(buffers);  
      var zip = new PizZip(buffer);
      var doc;
      try {
          doc = new Docxtemplater(zip);
      } catch(error) {
          errorHandler(error);
      }
    
      // ... rest of your code
    });