Not reading data and executing a function that does not exist - node.js

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.

Related

multipage document pdf-lib.js with node & express

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(...);

Number Matcher Error even though values are of type Number

I am having trouble with the third test case in my describe case. More specifically, I get the error with the line expect(secondResult._body.length).toHaveLength(initalBlogs.length + 1). Here I keep getting a "Matcher error: received value must have a length property whose value must be a number" error. I used the typeof function and it found that both my received and expected values are numbers but I still keep getting this error. Any help would be greatly appreciated.
Code
const supertest = require('supertest')
const mongoose = require('mongoose')
const app = require('../app')
const Blog = require('../models/blog')
const { initial } = require('lodash')
const api = supertest(app)
const initalBlogs = [
{
title: "React patterns",
author: "Michael Chan",
url: "https://reactpatterns.com/",
likes: 7
},
{
title: "Go To Statement Considered Harmful",
author: "Edsger W. Dijkstra",
url: "http://www.u.arizona.edu/~rubinson/copyright_violations/Go_To_Considered_Harmful.html",
likes: 5
}
]
beforeEach(async () => {
await Blog.deleteMany({})
let newBlog = new Blog(initalBlogs[0])
await newBlog.save()
newBlog = new Blog(initalBlogs[1])
await newBlog.save()
})
describe('blog tests', () => {
test('returns blog list length', async () => {
const blogs = await api.get('/api/blogs')
const result = blogs._body
expect(result.length).toBe(2)
})
test('verify unique identifier is named id', async () => {
const blogs = await api.get('/api/blogs')
const result = blogs._body
result.forEach((element) => {
expect(element.id).toBeDefined()
})
})
//ISSUES HERE
test('adds new blog', async () => {
const newBlog = {
title: "Node patterns",
author: "Cool Chan",
url: "https://fregrferfref.com/",
likes: 7
}
const result = await api.post('/api/blogs').send(newBlog).expect(201)
const secondResult = await api.get('/api/blogs')
const filteredArr = secondResult._body.map(a => a.title);
expect(secondResult._body.length).toHaveLength(initalBlogs.length + 1) //Says the length property value should be a number even though it is a number
expect(filteredArr).toContain('Node patterns')
})
})
afterAll(() => {
mongoose.connection.close()
})
In this line
expect(secondResult._body.length).toHaveLength(initalBlogs.length + 1)
you are computing the length twice: once with .length and once with .toHaveLength().
In other words, you are testing the value of secondResult._body.length.length, which throws an error because secondResult._body.length is a number and therefore its length is undefined.
Try
expect(secondResult._body).toHaveLength(initalBlogs.length + 1)
or
expect(secondResult._body.length).toBe(initalBlogs.length + 1)

Asynchronous function in Node.js API not working as intended

As an exercise, I'm creating a simple API that allows users to provide a search term to retrieve links to appropriate news articles across a collection of resources. The relevent function and the route handler that uses the function is as follows:
function GetArticles(searchTerm) {
const articles = [];
//Loop through each resource
resources.forEach(async resource => {
const result = await axios.get(resource.address);
const html = result.data;
//Use Cheerio: load the html document and create Cheerio selector/API
const $ = cheerio.load(html);
//Filter html to retrieve appropriate links
$(`a:contains(${searchTerm})`, html).each((i, el) => {
const title = $(el).text();
let url = $(el).attr('href');
articles.push(
{
title: title,
url: url,
source: resource.name
}
);
})
})
return articles; //Empty array is returned
}
And the route handler that uses the function:
app.get('/news/:searchTerm', async (req, res) => {
const searchTerm = req.params.searchTerm;
const articles = await GetArticles(searchTerm);
res.json(articles);
})
The problem I'm getting is that the returned "articles" array is empty. However, if I'm not "looping over each resource" as commented in the beginning of GetArticles, but instead perform the main logic on just a single "resource", "articles" is returned with the requested data and is not empty. In other words, if the function is the following:
async function GetArticles(searchTerm) {
const articles = [];
const result = await axios.get(resources[0].address);
const html = result.data;
const $ = cheerio.load(html);
$(`a:contains(${searchTerm})`, html).each((i, el) => {
const title = $(el).text();
let url = $(el).attr('href');
articles.push(
{
title: title,
url: url,
source: resources[0].name
}
);
})
return articles; //Populated array
}
Then "articles" is not empty, as intended.
I'm sure this has to do with how I'm dealing with the asynchronous nature of the code. I've tried refreshing my knowledge of asynchronous programming in JS but I still can't quite fix the function. Clearly, the "articles" array is being returned before it's populated, but how?
Could someone please help explain why my GetArticles function works with a single "resource" but not when looping over an array of "resources"?
Try this
function GetArticles(searchTerm) {
return Promise.all(resources.map(resource => axios.get(resource.address))
.then(responses => responses.flatMap(result => {
const html = result.data;
//Use Cheerio: load the html document and create Cheerio selector/API
const $ = cheerio.load(html);
let articles = []
//Filter html to retrieve appropriate links
$(`a:contains(${searchTerm})`, html).each((i, el) => {
const title = $(el).text();
let url = $(el).attr('href');
articles.push(
{
title: title,
url: url,
source: resource.name
}
);
})
return articles;
}))
}
The problem in your implementation was here
resources.forEach(async resource...
You have defined your function async but when result.foreach get executed and launch your async functions it doesn't wait.
So your array will always be empty.

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)

POST request with formidable returning 500 - Node.js

I need to save data and file as a new project to my Mongo. For this I am using formidable.
My POST method looks like this:
exports.create = async (req, res) => {
let form = new formidable.IncomingForm();
form.keepExtensions = true;
form.parse(req, (err, fields, files) => {
if (err) {
return res
.status(400)
.json({ errors: [{ msg: 'Image could not be uploaded' }] });
}
const {
title,
description,
photo,
tags,
git,
demo,
projectType,
} = fields;
//get links object
const projectFields = {};
projectFields.creator = req.user._id;
if (title) projectFields.title = title;
if (title) projectFields.description = description;
if (photo) projectFields.photo = photo;
if (projectType) projectFields.projectType = projectType;
if (tags) {
projectFields.tags = tags.split(',').map((tag) => tag.trim());
}
//get links object
projectFields.links = {};
if (git) projectFields.links.git = git;
if (demo) projectFields.links.demo = demo;
//1kb = 1000
//1mb = 1000000kb
//name 'photo' mus match client side. use photo
if (files.photo) {
if (files.photo.size > 1000000) {
return res.status(400).json({
errors: [{ msg: 'Image could not be uploaded. File to big.' }],
});
}
//this relates to data in schema product
project.photo.data = fs.readFileSync(files.photo.path);
project.photo.contentType = files.photo.type;
}
});
I want to use async/await so I am using try{}catch(err){} for my project.save(). I am initializing all my fields where I have also nested links. Unfortunately this is not working as I thought it will work. Right now my POST is returning 500. I am sitting on this and right now I am at the point that this can get a bit messy and not even close to any solution.

Resources