multipage document pdf-lib.js with node & express - node.js

Im trying to create a multi-page document from templates on my file system, but I'm getting strange behaviour of the same page title across all pages in the document instead. Any ideas what I'm doing wrong here?
Something I don't quite get, is the way we add pages. Why do we need to reference newDoc in the example below, when we do await newDoc.copyPages(page, [0])? Instead of just newDoc.addPage(page)?
Would it be that the form field named Title is being overwritten because both pages have the same field name during the copying of data streams?
Note: I've been made aware that StackOverflow doesnt have a tag for pdf-lib.js.org, not to be confused with other pdf libraries.
const payload = {
rows: [{
id: 1,
title: 'Foo',
},{
id: 2,
title: 'Bar'
},
formData: {
hello: 'World',
lorum: 'Ipsum'
}
]
}
const makePdf = async (payload) => {
const newDoc = await PDFDocument.create()
newDoc.getForm().acroForm.dict.set(PDFName.of('NeedAppearances'), PDFBool.True)
for (const row of payload.rows) {
await addPage(row, payload.formData, newDoc)
}
return newDoc
}
const addPage = async (dataRow, formData, newDoc) => {
const rowId = dataRow.id
let templateName
switch(true) {
case (rowId === 1):
templateName = 'foo'
break
case (rowId === 2):
templateName = 'bar'
break
}
const templatePath = path.join(__dirname, `../templates/pdfs_/${templateName}.pdf`)
const template = await fs.readFileSync(templatePath)
const page = await PDFDocument.load(template)
const form = page.getForm()
form.acroForm.dict.set(PDFName.of('NeedAppearances'), PDFBool.True)
switch(templateName) {
case 'foo':
foo(form, formData)
break
case 'bar':
bar(form, formData)
}
// dataRow.title logs correct strings ie: 'Foo' & 'Bar'
form.getField('Title').setText(dataRow.title)
const [firstPage] = await newDoc.copyPages(page, [0])
return await newDoc.addPage(firstPage)
}
const bar = (form, formData) => {
form.getField('Lorum').setText(formData.lorum)
}
const foo = (form, payload) => {
form.getField('Hello').setText(formData.hello)
}
return makePdf(payload)
// Produces 2 page pdf with the same title
// [[ title: Foo, Hello: World ], [title: Foo, Lorum: Ipsum ]]

I don't have your template file so I'm not sure excatly what you are trying to achieve.
The copyPage is indeed required.
You should save the PDFDocument before you change it again.
I tried to rewrite your code, I ignored the PDFName and PDFBool line but I'm sure you can get the idea.
const {PDFDocument} = require('pdf-lib');
const fs = require('fs');
const payload = {
rows: [
{ id: 1, title: 'Foo', },
{ id: 2, title: 'Bar', },
],
formData: { hello: 'World', lorum: 'Ipsum', }
}
class PdfSample {
async loadTemplate(templatePath) {
const templateFile = fs.readFileSync(templatePath);
const templateDoc = this.templateDoc = await PDFDocument.load(templateFile);
this.templateForm = templateDoc.getForm();
}
async initDoc() {
this.resultDoc = await PDFDocument.create();
}
fillTextField(fieldName, value) {
const field = this.templateForm.getField(fieldName);
field.setText(value);
}
get formFieldsNames() {
return this.templateForm.getFields().map(field => field.getName());
}
async addDocPage() {
const newPageBytes = await this.templateDoc.save();
const newPage = await PDFDocument.load(newPageBytes);
const [pageToAdd] = await this.resultDoc.copyPages(newPage, [0]);
return this.resultDoc.addPage(pageToAdd);
}
async saveResult(outputPath) {
const pdfBytes = await this.resultDoc.save();
fs.writeFileSync(outputPath, pdfBytes);
}
}
const pdfSample = new PdfSample();
(async () => {
await pdfSample.initDoc();
await pdfSample.loadTemplate('./pdfs_/OoPdfFormExample.pdf');
console.log(pdfSample.formFieldsNames);
for (const row of payload.rows) {
pdfSample.fillTextField('Given Name Text Box', row.title);
pdfSample.fillTextField('Family Name Text Box', payload.formData.hello);
pdfSample.fillTextField('House nr Text Box', payload.formData.lorum);
await pdfSample.addDocPage(pdfSample.templateDoc);
}
await pdfSample.saveResult('./pdfs_/result.pdf');
})();
The sample form is from this site

You are trying to create a document with two form fields named Title, one on each page. This does not work, both are the same field and show the same value.
Rather then "copy" existing fields from a template, you must use something like
var newField = form.createTextField('Title' + dataRow.id);
newField.addToPage(firstPage, {x: ..., y: ...});
newField.setText(...);

Related

How can I efficiently filter multiple values in mongodb

I'm trying to filter some data by themes and am puzzled as to how I can go about doing it. Say I have two items with the themes ['People', 'Technology', 'Culture'] and ['Economy', 'Technology', 'Culture'], if the query is Technology, I am able to see both of these items appearing. But if the query is Technology and Culture, I'm not able to see either of them because ["Technology", "Culture"] =/= ['People', 'Technology', 'Culture'] and vice versa. My code searches for the exact list, not it's components, if the query was Technology and Culture then I want both of those items to show up since it is inside that list.
I'm not sure how to do this, I'm using MERN stack and here is my backend code:
const Project = require('../models/projectModel')
const mongoose = require('mongoose')
// get all projects
const getProjects = async (req, res) => {
const projects = await Project.find().sort({ createdAt: -1 })
// const test = await Project.find({assignment_type: 1 || 2}).sort({ createdAt: -1 })
// console.log(test)
res.status(200).json(projects)
}
// get filtered project
const getFilteredProjects = async (req, res) => {
var request = {}
console.log(req.query.sdg)
console.log('t' + req.query.theme)
console.log('a' + req.query.assignment_type)
// var t = ["Economical", "Technological"]
// const test = await Project.find({theme: ("Technological" && "Economical")}).sort({ createdAt: -1 })
// const test = await Project.find({
// $and:
// }).sort({ createdAt: -1 })
// console.log(test)
// Function to separate commas from string
function separateCommas(str) {
let t = []
for (let i = 0; i < str.length; i++) {
if (str[i] === ',') {
console.log(i)
t.push(i)
}
}
let themeArray = []
if (t.length === 1) {
let theme1 = str.slice(0, t[0])
let theme2 = str.slice(t[0]+1)
themeArray.push(theme1)
themeArray.push(theme2)
}
if (t.length === 2) {
let theme1 = str.slice(0, t[0])
let theme2 = str.slice(t[0]+1, t[1])
let theme3 = str.slice(t[1]+1)
themeArray.push(theme1)
themeArray.push(theme2)
themeArray.push(theme3)
}
request["theme"] = themeArray.sort()
}
// See if sdg selected
if (req.query.sdg !== '') {
request["sdg"] = req.query.sdg
}
// See if assignment type selected
if (req.query.assignment_type !== '') {
request["assignment_type"] = req.query.assignment_type
}
// See if theme selected
if (req.query.theme !== '') {
if (req.query.theme.length > 14) {
separateCommas(req.query.theme)
}
else {
request["theme"] = req.query.theme
}
}
console.log(request)
const projects = await Project.find(request).sort({ createdAt: -1 })
res.status(200).json(projects)
}
module.exports = {
getProjects,
getProject,
createProject,
deleteProject,
updateProject,
getFilteredProjects
}
This is how my backend code receives the data from the database, it sends it in this format where there can be multiple theme's:
{
sdg: 'SDG 2: Zero Hunger',
assignment_type: 'Discussion Topics',
theme: 'Economy'
}

Not reading data and executing a function that does not exist

So I'm building an API prototype and I have code that reads data from a Google Sheets (serving as a CMS) but the problem is when calling the route that I defined in Express.js it is not reading data from the sheet. It works for sheet 1 but not for sheet 2.
To read the data I use this package: https://www.npmjs.com/package/google-spreadsheet
Link to a copy of the Google Sheet: https://docs.google.com/spreadsheets/d/1yx0iRPPho2H1OGrvTAspkTr2b1zwzbkxeb7hRuqqNwc/edit?usp=sharing
Relevant Code:
router.get('/getallcontent', async function (req, res) {
const doc = new GoogleSpreadsheet('sheetId');
await doc.useServiceAccountAuth({
client_email: creds.client_email,
private_key: creds.private_key
});
const info = await doc.loadInfo();
const sheet = doc.sheetsByIndex[1];
const rows = await sheet.getRows()
// console.log(rows)
let contents = []
function Content(title, content, sku, author, lesson) {
this.title = title;
this.content = content;
this.sku = sku;
this.author = author;
this.lesson = lesson;
}
await rows.forEach(row => {
let content = new Content(
row._rawData[0],
row._rawData[1],
row._rawData[2],
row._rawData[3],
row._rawData[4]
)
contents.push(content)
})
res.json(Response.success(req, { content: contents }))
})
Response when calling the route:
{"request":{"result":"success","message":""},"body":{"lessons":[]}}
Expected response:
{"request":{"result":"success","message":""},"body":{"content":[{"title": "Requesting Clearance","content": "some html markup text", "sku": "requesting-clearance", "author": "John D", "lesson": "test-lesson"}]}}
Test Script does work:
async function getLessons() {
const doc = new GoogleSpreadsheet('1T8-rIN4w-T1OuYRuQ-JYI-15l9RqOXqDQ2KehWyp44E');
await doc.useServiceAccountAuth({
client_email: creds.client_email,
private_key: creds.private_key
});
const info = await doc.loadInfo();
const sheet = doc.sheetsByIndex[1];
const rows = await sheet.getRows()
rows.forEach(row => {
printContent(row)
})
}
async function printContent(rows) {
let lesson = {
title: rows._rawData[0],
content: rows._rawData[1],
sku: rows._rawData[2],
author: rows._rawData[3],
lesson: rows._rawData[4]
};
console.log(lesson)
}
Found my own answer. For some reason the contents array was being cleared after the .push and thus returned an empty array.

Iterate dataArray to create an object is not happening

How can I parse the incoming nomData data array and store those values into an object along with userEmail ? Somehow below code is not working, could someone pleas advise the issue here.
Expected database columns values:
var data = { useremail: userEmail, nomineeemail: email, nomineename: name, nomineeteam: team, reason: reason }
server.js
app.post('/service/nominateperson', async (req, res) => {
try {
const userEmail = req.body.userEmail;
const nomData = req.body.nomRegister;
const formData = {
useremail: userEmail,
nomineeemail: {},
nomineename: {},
nomineeteam: {},
reason: {}
}
const newArray = nomData.map(item => {
formData.nomineeemail = item.email;
formData.nomineename = item.name;
formData.nomineeteam = item.team;
formData.reason = item.reason;
});
var data = { useremail: userEmail, nomineeemail: email, nomineename: name, nomineeteam: team, reason: reason }
// Ideally I should get nomData
//items parsed create an data object and pass that into bulkCreat() method:
const numberOfNominations = await NominationModel.count({where: {useremail: userEmail}});
if (numberOfNominations <= 3) {
const nominationData = await NominationModel.bulkCreate(data);
res.status(200).json({message: "Nomination submitted successfully !"});
} else {
res.status(202).json({message: "Nomination limit exceeded, please try next week !"});
}
} catch (e) {
res.status(500).json({fail: e.message});
}
});
So i assume nomData is an array containing multiple nominations. So if you want to bulkCreate with data you should pass an array instead of an object.
const data = nomData.map(item => ({
useremail: userEmail,
nomineeemail: item.email,
nomineename: item.name,
nomineeteam: item.team,
reason: item.reason
}));
...
await NominationModel.bulkCreate(data)

How to properly order async functions in node.js?

i got 3 adaptive cards that should display correctly one after the other, depending on which button is pushed. Unfortunately they just show randomly till the wished one appears. My function is pretty miserable and consumes too much in testing. Any suggestion is welcomed.
InputCard works fine cos is attached at the beginning in the index.js file, the mess starts later with the three ones exposed here. InputCard is repeated cos of the context.sendActivity('Back to menu').
async processCards (context, luisResult, next) {
this.logger.log('process');
// Retrieve LUIS result for Process Automation.
const result = luisResult.connectedServiceResult;
const intent = result.topScoringIntent.intent;
let InputCard = require('../resources/FaqCard.json');
let InputCard1 = require('../resources/InputCard.json');
let InputCard3 = require('../resources/UserPsw.json');
let CARDS = [
InputCard,
InputCard1,
InputCard3
];
let reply = { type: ActivityTypes.Message };
let buttons = [
{ type: ActionTypes.Submit, title: 'F.A.Q', value: '1' },
{ type: ActionTypes.Submit, title: 'Back to menu', value: '2' },
{ type: ActionTypes.Submit, title: 'login', value: '3' }
];
let card = CardFactory.heroCard('say what???', undefined,
buttons, { text: 'You can upload an image or select one of the following choices.' });
reply.attachments = [card];
let SelectedCard = CARDS[Math.floor((Math.random() * CARDS.length - 1) + 1)];
if (await context.sendActivity(`F.A.Q`))
return context.sendActivity({
attachments: [CardFactory.adaptiveCard(SelectedCard)]
});
if (await context.sendActivity('Back to menu'))
return context.sendActivity({
attachments: [CardFactory.adaptiveCard(SelectedCard) = this.CARDS(InputCard)]
});
if (await context.sendActivity('login'))
return context.sendActivity({
attachments: [CardFactory.adaptiveCard(SelectedCard) = this.CARDS(InputCard3)]
});
await context.sendActivity(`processCards top intent ${intent}.`);
await context.sendActivity(`processCards intents detected: ${luisResult.intents.map((intentObj) => intentObj.intent).join('\n\n')}.`);
if (await context.sendActivity(`myName ${intent}.` == 'myName')) {
return context.sendActivity(`ok, anything else????`);
}
if (luisResult.entities.length > 0) {
await context.sendActivity(`processCards entities were found in the message: ${luisResult.entities.map((entityObj) => entityObj.entity).join('\n\n')}.`);
await context.sendActivity(`ok, anything else?`);
}
await next();
}
After InputCard,should be possible to choose between FaqCard.json or UserPsw.json and after that,if wished, returning to InputCard too.

How to make Mongoose update work with await?

I'm creating a NodeJS backend where a process reads in data from a source, checks for changes compared to the current data, makes those updates to MongoDB and reports the changes made. Everything works, except I can't get the changes reported, because I can't get the Mongoose update action to await.
The returned array from this function is then displayed by a Koa server. It shows an empty array, and in the server logs, the correct values appear after the server has returned the empty response.
I've digged through Mongoose docs and Stack Overflow questions – quite a few questions about the topic – but with no success. None of the solutions provided seem to help. I've isolated the issue to this part: if I remove the Mongoose part, everything works as expected.
const parseJSON = async xmlData => {
const changes = []
const games = await Game.find({})
const gameObjects = games.map(game => {
return new GameObject(game.name, game.id, game)
})
let jsonObj = require("../sample.json")
Object.keys(jsonObj.items.item).forEach(async item => {
const game = jsonObj.items.item[item]
const gameID = game["#_objectid"]
const rating = game.stats.rating["#_value"]
if (rating === "N/A") return
const gameObject = await gameObjects.find(
game => game.bgg === parseInt(gameID)
)
if (gameObject && gameObject.rating !== parseInt(rating)) {
try {
const updated = await Game.findOneAndUpdate(
{ _id: gameObject.id },
{ rating: rating },
{ new: true }
).exec()
changes.push(
`${updated.name}: ${gameObject.rating} -> ${updated.rating}`
)
} catch (error) {
console.log(error)
}
}
})
return changes
}
Everything works – the changes are found and the database is updated, but the reported changes are returned too late, because the execution doesn't wait for Mongoose.
I've also tried this instead of findOneAndUpdate():
const updated = await Game.findOne()
.where("_id")
.in([gameObject.id])
.exec()
updated.rating = rating
await updated.save()
The same results here: everything else works, but the async doesn't.
As #Puneet Sharma mentioned, you'll have to map instead of forEach to get an array of promises, then await on the promises (using Promise.all for convenience) before returning changes that will then have been populated:
const parseJSON = async xmlData => {
const changes = []
const games = await Game.find({})
const gameObjects = games.map(game => {
return new GameObject(game.name, game.id, game)
})
const jsonObj = require("../sample.json")
const promises = Object.keys(jsonObj.items.item).map(async item => {
const game = jsonObj.items.item[item]
const gameID = game["#_objectid"]
const rating = game.stats.rating["#_value"]
if (rating === "N/A") return
const gameObject = await gameObjects.find(
game => game.bgg === parseInt(gameID)
)
if (gameObject && gameObject.rating !== parseInt(rating)) {
try {
const updated = await Game.findOneAndUpdate(
{ _id: gameObject.id },
{ rating: rating },
{ new: true }
).exec()
changes.push(
`${updated.name}: ${gameObject.rating} -> ${updated.rating}`
)
} catch (error) {
console.log(error)
}
}
})
await Promise.all(promises)
return changes
}
(The diff, for convenience:
9,10c9,10
< let jsonObj = require("../sample.json")
< Object.keys(jsonObj.items.item).forEach(async item => {
---
> const jsonObj = require("../sample.json")
> const promises = Object.keys(jsonObj.items.item).map(async item => {
33a34
> await Promise.all(promises)
)
EDIT: a further refactoring would be to use that array of promises for the change descriptions themselves. Basically changePromises is an array of Promises that resolve to a string or null (if there was no change), so a .filter with the identity function will filter out the falsy values.
This method also has the advantage that changes will be in the same order as the keys were iterated over; with the original code, there's no guarantee of order. That may or may not matter for your use case.
I also flipped the if/elses within the map function to reduce nesting; it's a matter of taste really.
Ps. That await Game.find({}) will be a problem when you have a large collection of games.
const parseJSON = async xmlData => {
const games = await Game.find({});
const gameObjects = games.map(game => new GameObject(game.name, game.id, game));
const jsonGames = require("../sample.json").items.item;
const changePromises = Object.keys(jsonGames).map(async item => {
const game = jsonGames[item];
const gameID = game["#_objectid"];
const rating = game.stats.rating["#_value"];
if (rating === "N/A") {
// Rating from data is N/A, we don't need to update anything.
return null;
}
const gameObject = await gameObjects.find(game => game.bgg === parseInt(gameID));
if (!(gameObject && gameObject.rating !== parseInt(rating))) {
// Game not found or its rating is already correct; no change.
return null;
}
try {
const updated = await Game.findOneAndUpdate(
{ _id: gameObject.id },
{ rating: rating },
{ new: true },
).exec();
return `${updated.name}: ${gameObject.rating} -> ${updated.rating}`;
} catch (error) {
console.log(error);
}
});
// Await for the change promises to resolve, then filter out the `null`s.
return (await Promise.all(changePromises)).filter(c => c);
};

Resources