I have the below function to print receipts:
const generateOnePDF = async (ticket, orderData) =>
new Promise(async (resolve, reject) => {
let finalString = '';
if (!ticket) {
const err = new Error('missing ticket data');
reject(err);
}
if (!orderData) {
const err = new Error('missing order data');
reject(err);
}
const { payload, sequence, title } = ticket;
if (!payload) {
const err = new Error('missing payload data');
reject(err);
}
if (!sequence) {
const err = new Error('missing ticket sequence');
reject(err);
}
if (!title) {
const err = new Error('missing ticket book title');
reject(err);
}
const doc = new PDFDocument();
PDFDocument.prototype.addSVG = function (svg, x, y, options) {
return SVGtoPDF(this, svg, x, y, options);
};
const stream = doc.pipe(new Base64Encode());
// logo
const logo = await fetchImage(
`${url}/static/media/logo_157.c15ac239.svg`
);
doc.addSVG(logo.toString(), 32, 40, {});
doc.moveUp();
doc.moveUp();
doc.text(`Order: O${orderData.orderId}`, { align: 'right' });
const rectXOffset = 25;
const rectPosition = 32;
doc
.rect(rectXOffset, rectPosition, doc.page.width - rectXOffset * 2, 32)
.stroke();
doc.moveDown();
doc.moveDown();
doc.moveDown();
doc.rect(rectXOffset, 80, doc.page.width - rectXOffset * 2, 680).stroke();
doc.text(orderData.title, { align: 'left' });
doc.moveDown();
doc.text(orderData.venue, { align: 'left' });
doc.text(orderData.address, { align: 'left' });
doc.text(`${orderData.city}, ${orderData.province} ${orderData.zip}`, {
align: 'left'
});
if (orderData.custom) {
doc.moveDown();
doc.text(orderData.customCopy, { align: 'left' });
}
doc.moveDown();
doc.text(`${orderData.date} at ${orderData.show}`, { align: 'left' });
doc.moveDown();
const image = await fetchImage(orderData['image']);
doc.image(image, {
fit: [100, 100],
align: 'right',
valign: 'top'
});
doc.moveDown();
doc.text(
`Order: O${orderData.orderId}. Placed by ${orderData.firstName} ${orderData.lastName} on ${orderData.created}`
);
// right column
doc.moveUp();
doc.moveUp();
doc.moveUp();
doc.moveUp();
doc.moveUp();
doc.moveUp();
doc.moveUp();
doc.moveUp();
doc.moveUp();
doc.moveUp();
doc.moveUp();
doc.moveUp();
doc.moveUp();
doc.moveUp();
doc.moveUp();
doc.moveUp();
doc.moveUp();
doc.moveUp();
doc.moveUp();
doc.moveUp();
doc.text(`Ticket: ${sequence} ${title}`, { align: 'right' });
const qrcode = new QRCode({
content: payload,
width: 200,
height: 200,
xmlDeclaration: false,
join: true
}).svg();
const options = { align: 'right' };
doc.addSVG(qrcode, 400, 150, options);
// finalize/close the PDF file
doc.end();
stream.on('data', (chunk) => {
finalString += chunk;
});
stream.on('end', () => {
// the stream is at its end, so push the resulting base64 string to the response
resolve(finalString);
});
stream.on('error', (err) => {
reject(err);
});
});
It is not the cleanest piece of code in the world, but it works for me, in the meantime.
I added a simple unit test for this code, below:
it('should throw an error if the ticket or order data are invalid', async () => {
await expect(generateOnePDF(null, {})).rejects.toThrowError();
await expect(generateOnePDF({}, null)).rejects.toThrowError();
});
The tests passes, but it writes "garbage" to the console. There are unhandled rejects in the code.
(node:53703) UnhandledPromiseRejectionWarning: TypeError: Cannot destructure property 'payload' of '((cov_2gbfm4phuo(...).s[25]++) , ticket)' as it is null.
(Use `node --trace-warnings ...` to show where the warning was created)
is one such error and another is:
(node:53703) UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'orderId' of null
What I don't understand why if (!ticket) throw()
and if (!orderData) throw
would not prevent the errors from occurring. Where is the "broken promise?"
I am doing reject
inside the promise handler function, so where I am not doing it?
The problem is that your function still continues executing after reject
is called, and so errors occur.
Moreover, you should not create a new Promise
with an async
callback. That is an anti-pattern. Instead promisify the stream "end" event, and then return that promise.
I would also refrain from defining a function on that PDFDocument
prototype, and certainly not on every call of generateOnePDF
. You might as well just call that SVGtoPDF
function directly.
So something like this:
const streamPromise = stream =>
return new Promise((resolve, reject) => {
let finalString = '';
stream.on('data', chunk => finalString += chunk);
stream.on('end', () => resolve(finalString));
stream.on('error', reject);
});
const generateOnePDF = async (ticket, orderData) => {
if (!ticket ) throw new Error('missing ticket data');
if (!orderData) throw new Error('missing order data');
const { payload, sequence, title } = ticket;
if (!payload ) throw new Error('missing payload data');
if (!sequence ) throw new Error('missing ticket sequence');
if (!title ) throw new Error('missing ticket book title');
const doc = new PDFDocument();
const stream = doc.pipe(new Base64Encode());
// logo
const logo = await fetchImage(`${url}/static/media/logo_157.c15ac239.svg`);
SVGtoPDF(doc, logo.toString(), 32, 40, {});
// ... etc ...
// ...
doc.end();
return streamPromise(stream);
});