In a blog made in express/nodejs I am trying to display in the single article page both the article (working fine) and a set of 2 recommended articles. Unfortunately as you can see in the commented bit of the code it doesn't work (can't render the same template twice)
What would be the best approach in this case?
<!-- language: lang-js -->
router.get('/*', function(req, res, next) {
var slug=req.url.replace(/^\//,'').replace(/\/$/,'');
var bg = getRandomInt(5);
if(slug==''){
connection.query('SELECT * FROM `pages` WHERE NOT slug = "about"', function (error, results, fields) {
res.render('index', { title: title, year: year, bg:bg, pages: results });
});
}else{
connection.query('SELECT * FROM `pages` where slug=?', [slug], function (error, results, fields)
{
if(results.length){
res.render('page', { title: results[0].title, page:results[0] });
}else{
console.log(req);
res.render('error',{url: 'http://'+req.headers.host+req.url});
}
});
/* not working apparently you can't send the header of the template twice umhh
connection.query('SELECT * FROM `pages` ORDER by RAND () LIMIT 2', function (error, random, fields)
{
res.render('page', { pages:random});
});
*/
}
});
The way you have it now
Both queries will complete (and call their callback) at unrelated times
res.render will be called multiple times, which does not work because it assumes all of the data is being sent in a single call. So it sends the HTTP headers, which cannot be sent twice.
Updated according to what it looks like you intended. Note this makes the order of the queries sequential, which may not be desirable. You'll want to use the async lib to help manage running them both at the same time and still consolidate the results:
router.get('/*', (req, res, next) => {
const slug = req.url.replace(/^\//, '').replace(/\/$/, '');
const bg = getRandomInt(5);
if (slug == '') {
return connection.query('SELECT * FROM `pages` WHERE NOT slug = "about"', (error, results, fields) => {
res.render('index', { title: title, year: year, bg: bg, pages: results });
});
} else {
return connection.query('SELECT * FROM `pages` where slug=?', [slug], (error, results, fields) => {
if (results.length) {
return connection.query('SELECT * FROM `pages` ORDER by RAND () LIMIT 2', (error, random, fields) => {
if (error) {
// handle error
}
// consolidate renders into a single call
// adjust the template file accordingly
return res.render('page', { title: results[0].title, page: results[0], pages: random });
});
} else {
console.log(req);
return res.render('error', { url: 'http://' + req.headers.host + req.url });
}
});
}
});
Alternatively, consider using bluebird & async/await, this is just another style - to give you options that are new based on node 8+. In this one the queries are kicked off at the same time again.
const bluebird = require('bluebird');
router.get('/*', async (req, res, next) => {
try {
const slug = req.url.replace(/^\//, '').replace(/\/$/, '');
const bg = getRandomInt(5);
if (slug == '') {
const results = await bluebird.fromCallback(cb => connection.query('SELECT * FROM `pages` WHERE NOT slug = "about"', cb));
return res.render('index', { title: title, year: year, bg: bg, pages: results });
} else {
const [results, random] = await Promise.all([
bluebird.fromCallback(cb => connection.query('SELECT * FROM `pages` where slug=?', [slug], cb)),
bluebird.fromCallback(cb => connection.query('SELECT * FROM `pages` ORDER by RAND () LIMIT 2', cb))
]);
if (results && results.length) {
return res.render('page', { title: results[0].title, page: results[0], pages: random });
} else {
return res.render('error', { url: 'http://' + req.headers.host + req.url });
}
}
} catch (e) {
return res.render('error', { url: 'http://' + req.headers.host + req.url });
}
});
You can't render a page twice, otherwise you'll get Error: Can't set headers after they are sent to the client
What you need to do, is fetch the current article and the recommended pages, and render the page once you have the results from both queries.
In order to achieve that I used: Promise.all, and then performed a single res.render
router.get('/*', async (req, res, next) => {
const slug = req.url.replace(/^\//, '').replace(/\/$/, '');
const bg = getRandomInt(5);
if (slug == '') {
const results = await query('SELECT * FROM `pages` WHERE NOT slug = "about"');
return res.render('index', {
title: title,
year: year,
bg: bg,
pages: results
});
}
// Get current article and recommended pages
// Promise.all returns an array where each entry has
// the resolved value of the promise passed at that index
const [article, recommended] = await Promise.all([
query('SELECT * FROM `pages` where slug=?', [slug]),
query('SELECT * FROM `pages` ORDER by RAND () LIMIT 2')
]);
if (article.length) {
// Render the article & recommended pages at once.
res.render('page', {
title: article[0].title,
page: article[0],
pages: recommended
});
} else {
console.log(req);
res.render('error', {
url: 'http://' + req.headers.host + req.url
});
}
});
// Query helper
// You can use promisify...
function query(statement, placeholders = []) {
return new Promise((resolve, reject) => {
connection.query(query, placeholders, (error, results) => {
if(err)
return reject(err);
resolve(results);
});
});
}
Related
I'm working with the airtable API, node js
I have a table called mentors
I have a second table called jobs
I use a find function which will return the job name for each mentor
I return records so I could pass it to my view
I got promise pending. r
router.get('/',function(req, res, next) {
const title= 'test'
const getRecords = async () => {
const records = await mentors
.select({maxRecords: 6, view: "Grid view"})
.eachPage(function page(records, fetchNextPage) {
records.forEach(function(records) {
jobs.find(records.fields["Quel est votre métier actuel"], function(err, record) {
if (err) { console.error("Big Error",err); return; }
records.fields.job = record.fields.Job;
return records
});
});
});
fetchNextPage();
}
getRecords()
.then(function(records) {
console.log(records)
res.render('index',{title: title, records});
});
});
I have a function in a utils file that I want to call and assign the exported result to a variable.
Currently, the variable is defined then I try to assign the return result but I am getting undefined as the result when I console.log it.
Here is my utils/consul file
var consul = require("consul")({host: config.consul.host'});
var consulBase = [];
var options;
module.exports = {
consulQuery: function(service){
consul.catalog.service.nodes(service, function(err, results) {
if(err) {console.log(err); throw err;}
if(results.length <= 0) return {message: `Error could not find any service of ${service} registered with consul,`, errorCode: 500};
if(results.length > 0) consulBase = [];
results.forEach((result) => {
consulBase.push(result.ServiceAddress+ ':' +result.ServicePort);
});
var serviceURL = 'http://' + consulBase[Math.floor(Math.random()*consulBase.length)];
return options = {
baseUrl : serviceURL,
form: {'':''},
headers: {authorization: ''}
};
});
}
Then in another file, I am calling like this and then trying to assign the value to 'options' but am getting undefined.
var consulQuery = require("../utils/consul").consulQuery;
// Get options array right away
var options = consulQuery('auth');
// Get options array every 5 seconds
setInterval(() => {
options = consulQuery('auth');
console.log(options);
}, 5 * 1000);
OK you have a couple issues.
First, is conceptual about what you are trying to do. Second is what do you actually need to change in your code to make it work.
I will not talk about the first part because, and there are plenty of good resources to learn about async with examples better then I can do here.
For the actual problems with your code:
You are missing a callback for consulQuery()
It should be something like this (notice the cb i added):
module.exports = {
consulQuery: function (service, cb) {
consul.catalog.service.nodes(service, function (err, results) {
if (err) {
console.log(err);
cb(err, null)
throw err;
}
if (results.length <= 0) return {
message: `Error could not find any service of ${service} registered with consul,`,
errorCode: 500
};
if (results.length > 0) consulBase = [];
results.forEach((result) => {
consulBase.push(result.ServiceAddress + ':' + result.ServicePort);
});
var serviceURL = 'http://' + consulBase[Math.floor(Math.random() * consulBase.length)];
cb(null, {
baseUrl: serviceURL,
form: {'': ''},
headers: {authorization: ''}
});
});
}
}
Second, in the other file in which you invoke the function, you will have to now pass a callback function.
options = consulQuery('auth', (err, response) => {
if(err){
console.log(err)
}
console.log(response)
});
So I have a route with a database call in a separate file:
module.exports = (req, res) => {
const sql = `SELECT Topic FROM surveys WHERE ID="${req.params.id}"`;
model.connection.query(sql, (err, result) => {
if (err) res.status(500).send(err);
res.send(result);
});
};
In my test file, I create fake req and res objects to test different inputs:
const req = {
body: {},
params: {
id: '',
}
};
const res = {
message: '',
stat: 200,
send(arg) {
this.message = arg;
},
status(number) {
this.stat = number;
return this;
}
};
And call it in every test:
it('Should return status 200 if parameter is a number string', () => {
req.params.id = '1';
getTopic(req, res);
chai.expect(res.stat).to.equal(200);
});
My question is, how can I test it asynchronously, without affecting my route?
I've seen Mocha documentation explaining this, but they require a callback in a function.
it('Should return status 200 if parameter is a number string', (done) => {
req.params.id = '1';
getTopic(req, res); // Cannot give any callback
chai.expect(res.stat).to.equal(200);
done(); //Doesn't work
});
I also tried using async/await, but it didn't work (I lack knowledge of how to use it, maybe that's why)
it('Should return status 200 if parameter is a number string', async () => {
req.params.id = '1';
await getTopic(req, res);
chai.expect(res.stat).to.equal(200);
});
And I've seen this question, and didn't help at all: unit testing express route with async callback
I want to display chatbot and facebook data at the same time. how to display it? because when I try to run in the browser but it does not appear anything. I've tried to look it up on stackoverflow but did not get the right reference
route.js
app.get('/cpanel/facebook', function(req, res) {
if (req.session.user == null) {
res.redirect('/cpanel/login');
} else {
CB.getAllRecords( function(e, chatbot) {
res.render('cpanel/facebook', { udata : req.session.user, chatbot : chatbot });
});
FBM.getAllRecords( function(e, facebook) {
res.render('cpanel/facebook', { udata : req.session.user, facebook : facebook });
});
}
});
facebook.js
var facebook = db.collection('facebook');
exports.addNewFacebook = function(newData, callback) {
facebook.findOne({accesstoken:newData.accesstoken}, function(e, o) {
if (o) {
callback('accesstoken-taken');
} else {
facebook.insert(newData, {safe: true}, callback);
}
});
}
exports.getAllRecords = function(callback) {
facebook.find().toArray(
function(e, res) {
if (e) callback(e)
else callback(null, res)
}
);
}
chatbot.js
var chatbot = db.collection('chatbot');
exports.addNewChatBot = function(newData, callback) {
chatbot.insert(newData, {safe: true}, callback);
}
exports.getAllRecords = function(callback) {
chatbot.find().toArray(
function(e, res) {
if (e) callback(e)
else callback(null, res)
}
);
}
The easier way to manage asynchronous operations in node.js, especially when you have more than one operation that you want to coordinate is to use Promises instead of plain callbacks. And, fortunately, mongodb supports a promise-based interface for all its asynchronous operations now.
So, first change your methods to return a promise instead of taking a callback:
var chatbot = db.collection('chatbot');
exports.getAllRecords = function() {
return chatbot.find().toArray();
}
var facebook = db.collection('facebook');
exports.getAllRecords = function() {
return facebook.find().toArray();
}
Then, you can use those promises with Promise.all() to coordinate:
app.get('/cpanel/facebook', function(req, res) {
if (req.session.user == null) {
res.redirect('/cpanel/login');
} else {
Promise.all([CB.getAllRecords(), FBM.getAllRecords()]).then(results => {
res.render('cpanel/facebook', { udata : req.session.user, chatbot : results[0], facebook: results[1]});
}).catch(err => {
// render some error page here
res.sendStatus(500);
});
}
});
For a call to just a single function that returns a promise, you can use .then():
app.get('/cpanel/facebook', function(req, res) {
if (req.session.user == null) {
res.redirect('/cpanel/login');
} else {
FBM.getAllRecords().then(results => {
res.render('cpanel/facebook', { udata : req.session.user, facebook: results});
}).catch(err => {
// render some error page here
res.sendStatus(500);
});
}
});
I want to make a backend call to an external api's and populate my page with the results. What is the best way to do this?
The "request.get" call is asynchronous, so I understand the code below is erroneous. However, I have written it in that fashion so that I can explain what I want to actually do.
Further, I may have 5-6 external api, is there a way to make this asynchronous for every api but synchronous get call?
This is how my current code looks like:
var express = require('express');
var request = require('request');
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
var body = getRawApiResponse("someURL");
console.log("Index >" + body);
res.render('index', { title: 'Express', api: "some", body: body});
});
function getRawApiResponse(api){
request.get({
uri: api,
},
function(error, response, body){
if (!error && response.statusCode === 200) {
console.log("Index > Raw Api Response: " + body);
} else {
console.log(error);
}
});
}
You can wrap getRawApiResponse() in a promise
function getRawApiResponse(api){
return new Promise(function(resolve, reject){
request.get({
uri: api,
},
function(error, response, body){
if (!error && response.statusCode === 200) {
resolve(body)
} else {
reject(error)
}
});
});
}
which resolves on success and rejects in case of an error then you can chain it inside the get request like
router.get('/', function(req, res, next) {
getRawApiResponse("someURL")
.then(function(body){
res.render('index', { title: 'Express', api: "some", body: body});
})
.catch(err){
// do something
}
});
Look into Promises or async/await. You can use them to call your async apis and wait for the response making the call synchronous.
http://bluebirdjs.com/docs/getting-started.html
A Sample code of async/ await which u can modify for ur purpose is as below:
try{
let orderDetails = await MongoHelper.findOneByCriteria(MongoCollections.ORDER,searchCriteria);
}catch(err){
return err;
}
MongoHelper.findOneByCriteria = (collectionName, criteria) => {
return new Promise((resolve, reject) => {
db.collection(collectionName).find(criteria).toArray()
.then((results) => {
if (results.length > 0) {
resolve(results[0]);
} else {
resolve(null);
}
});
});
}
The best way is to use Promises to avoid callbacks hell. If you can use node.js v7.6 or higher, it could be much easier with async/await.
router.get('/', function(req, res, next) {
getRawApiResponse("someURL")
.then(body => {
console.log("Index >" + body);
res.render('index', { title: 'Express', api: "some", body: body});
});
});
function getRawApiResponse(uri) {
return new Promise((resolve, reject) => {
request.get({ uri }, (error, response, body) => {
if (err) {
reject(err);
}
resolve(body);
});
});
}
In the example about I use promisification to return a promise from getRawApiResponse, but there is already a module which do the same https://github.com/request/request-promise.