PHP dev here, I'm wrestling with NodeJS for a while and I still can't wrap my head around the idea of asynchrony is JS/Node.
Look at this (ExpressJS):
router.get('/:id', function (req, res, next) {
var id = req.params.id;
var db = new Firebase('some_firebase_db');
db.child("users/"+id).once('value', function (snapshot) {
var user = snapshot.val();
if (user) {
db.child("messages/"+id).once('value', function (snapshot) {
res.render('user/messages', {
'user': user,
'messages': snapshot.val()
});
});
} else {
res.render('404');
}
});
});
To make database value accessible for a res object I need to render views inside the fetching data callbacks.
So when I need, say, make 6 requests to my DB I will have to embed my res object in 6 callbacks?
Or is there different approach that will make the code more readable keeping it asynchronous?
Ultimately, I need a way to fetch data from db multiple times in one request that will not make my code look like Christmas tree.
You can make it more readable even without using async or Promise:
router.get('/:id', function(req, res, next) {
var id = req.params.id;
var db = new Firebase('some_firebase_db');
db.child("users/" + id).once('value', userResult);
function userResult(snapshot) {
var user = snapshot.val();
if (user) {
db.child("messages/" + id).once('value', messageResult);
} else {
res.render('404');
}
}
function messageResult(snapshot) {
res.render('user/messages', {
'user': user,
'messages': snapshot.val()
});
}
});
But using async or Promise would be a better solution.
Related
In multiple functions I'm running more than one database action. When one of these fails I want to revert the ran actions. Therefore I'm using a transaction session from Mongoose.
First I create a session with the startSession function. I've added the session to the different Model.create functions. At the end of the function I'm committing and ending the session.
Since I work with an asyncHandler wrapper on all my function I'm not retyping the try/catch pattern inside my function. Is there a way to get the session into the asyncHandler of a different wrapper to abort the transaction when one or more of these functions fail?
Register function example
import { startSession } from 'mongoose';
import Company from '../models/Company';
import Person from '../models/Person';
import User from '../models/User';
import Mandate from '../models/Mandate';
import asyncHandler from '../middleware/asyncHandler';
export const register = asyncHandler(async (req, res, next) => {
const session = await startSession();
let entity;
if(req.body.profile_type === 'company') {
entity = await Company.create([{ ...req.body }], { session });
} else {
entity = await Person.create([{ ...req.body }], { session });
}
// Create user
const user = await User.create([{
entity,
...req.body
}], { session });
// Create mandate
await Mandate.create([{
entity,
status: 'unsigned'
}], { session });
// Generate user account verification token
const verification_token = user.generateVerificationToken();
// Send verification mail
await sendAccountVerificationMail(user.email, user.first_name, user.language, verification_token);
await session.commitTransaction();
session.endSession();
res.json({
message: 'User succesfully registered. Check your mailbox to verify your account and continue the onboarding.',
})
});
asyncHandler helper
const asyncHandler = fn => ( req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
export default asyncHandler;
EDIT 1
Let me rephrase the question. I'm looking for a way (one or more wrapper functions or a different method) to avoid rewriting the lines with // ~ repetitive code behind it. A try/catch block and handling the start and abort function of a database transaction.
export const register = async (req, res, next) => {
const session = await startSession(); // ~ repetitive code
session.startTransaction(); // ~ repetitive code
try { // ~ repetitive code
let entity;
if(req.body.profile_type === 'company') {
entity = await Company.create([{ ...req.body }], { session });
} else {
entity = await Person.create([{ ...req.body }], { session });
}
const mandate = await Mandate.create([{ entity, status: 'unsigned' }], { session });
const user = await User.create([{ entity, ...req.body }], { session });
const verification_token = user.generateVerificationToken();
await sendAccountVerificationMail(user.email, user.first_name, user.language, verification_token);
await session.commitTransaction(); // ~ repetitive
session.endSession(); // ~ repetitive
res.json({
message: 'User succesfully registered. Check your mailbox to verify your account and continue the onboarding.',
});
} catch(error) { // ~ repetitive
session.abortTransaction(); // ~ repetitive
next(error) // ~ repetitive
} // ~ repetitive
};
If you put the repetitive code in a class
class Transaction {
async middleware(req, res, next) {
const session = await startSession();
session.startTransaction();
try {
await this.execute(req, session);
await session.commitTransaction();
session.endSession();
this.message(res);
} catch (error) {
session.abortTransaction();
next(error);
}
}
async execute(req, session) { }
message(res) { }
}
then you can inherit from that class to put in the non-repetitive parts:
class Register extends Transaction {
async execute(req, session) {
let entity;
if (req.body.profile_type === 'company') {
entity = await Company.create([{ ...req.body }], { session });
} else {
entity = await Person.create([{ ...req.body }], { session });
}
const mandate = await Mandate.create([{ entity, status: 'unsigned' }], { session });
const user = await User.create([{ entity, ...req.body }], { session });
const verification_token = user.generateVerificationToken();
await sendAccountVerificationMail(user.email, user.first_name, user.language, verification_token);
}
message(res) {
res.json({
message: 'User succesfully registered. Check your mailbox to verify your account and continue the onboarding.',
});
}
}
export const register = async (req, res, next) => {
new Register().middleware(req, res, next);
}
I don't know where you got your asyncHandler logic, but it is very similar to what is used here and if it's not from there, I believe that article combined with this one about res.locals should answer your question.
By the way, the usage of express is assumed from your code and if I'm right, this question has way more to do with express than anything else and in that case I'd edit the tags to only include javascript and express.
Why I didn't I mark this as a duplicate though?
Well, after searching for answers I also bumped into Express 5 and I thought it would be interesting to mention that Starting with Express 5, route handlers and middleware that return a Promise will call next(value) automatically when they reject or throw an error
Which means that with Express 5, you can just do something like:
app.get('/user/:id', async (req, res, next) => {
const user = await getUserById(req.params.id)
res.send(user)
})
And any errors will be implicitly handled behind the scenes by Express, meaning that if await getUserById would somewhy fail, express would automatically call next for you, passing the flow to e.g. some error handler:
app.use((err, req, res, next) => {
console.log(err);
});
Edit for OP's revision
This is a programming patterns issue. My opinion is that you should definitely explicitly write all of the try..catch, startSession and abortTransaction blocks inside every database function such as register like you have done.
What you could do instead is to implement shared error handling between all of these database functions.
There are multiple reasons for why I am suggesting this:
It is generally a bad idea to have very large try...catch blocks, which you will technically have, if all of the database functions are under the same try...catch. Large try...catch blocks make debugging harder and can result into unexpected situations. They will also prevent fine tuning of handling of exceptions, should the need arise (and it often will). Think of this as your program just saying "error", no matter what the error is; that's not good!
Don't use transactions if you don't need to, as they can introduce unnecessary performance overhead and if you just "blindly" wrap everything into a transaction, it could accidentally result into a database deadlock. If you really really want to, you could create some kind of utility function as shown below, as that too would at least scope / restrict the transaction to prevent the transaction logic "leaking"
Example:
// Commented out code is what you'd actually have
(async () => {
const inTransaction = async (fn, params) => {
//const session = await startSession();
const session = "session";
let result = await fn(session, ...params);
//await session.commitTransaction();
//session.endSession();
return result;
};
let req = 0;
console.log(req);
const transactionResult = await inTransaction(async (session, req) => {
//return Company.create([{ ...req.body }], { session });
return new Promise(resolve => setTimeout(() => { resolve(req) }, 500));
}, [10]);
req += transactionResult;
console.log(req);
})();
So eventhough e.g. putting all code into one try...catch does prevent "duplicate code", the matter is not as black and white as "all duplicate code is bad!". Every so often when programming, you will stumble upon situations where it is a perfectly valid solution to repeat yourself and have some dreaded duplicate code (👻 Oooo-oo-ooo!).
I have the following model in teamMembers.js:
const { Schema, model } = require('mongoose');
const teamMembersSchema = new Schema({
uid: String,
name: String,
hours: Number
})
const TeamMembers = model('teamMembers', TeamMembersSchema);
module.exports = TeamMembers;
I've created the following endpoints in teamMemberRoute.js:
const TeamMembers = require('./models/teamMembers');
module.exports = (app) => {
app.get('/api/pods/teamMembers/:uid', async (req, res) => {
let teamMember = await TeamMembers.find( {'uid': req.params.uid } );
return res.status(200).send(teamMember);
});
app.get('/api/pods/teamMembers', async (req, res) => {
let teamMembers = await TeamMembers.find();
return res.status(200).send(teamMembers);
});
}
The first endpoint (/api/pods/teamMembers/:uid) works just fine - when I pass a uid it returns documents specific to that uid in the TeamMember collection.
The second endpoint should return all documents from the TeamMember collection since no parameters are passed. However, when the request is executed, only [] is returned. We know for a fact that documents exist in the TeamMember collection, since the first endpoint returns data from that collection based on the uid parameter that is passed.
I'm stumped on this. Any ideas? I don't think there is anything wrong with my model since I am able to execute the first endpoint with no issues.
Express executes code from top to button, and that is the reason for this issue. It will match your first endpoint and assume that uid is null. Just change the order of defined endpoints, like this:
module.exports = (app) => {
app.get('/api/pods/teamMembers', async (req, res) => {
let teamMembers = await TeamMembers.find();
return res.status(200).send(teamMembers);
});
app.get('/api/pods/teamMembers/:uid', async (req, res) => {
let teamMember = await TeamMembers.find( {'uid': req.params.uid } );
return res.status(200).send(teamMember);
});
}
I'm trying to figure out how to pass req.params around using Express in the context of MVC.
I know how to properly reference req.params but when I split my app.js up into models and controllers I'm quite lost.
Code for reference:
routes.js
app.get('/category/:category', descriptor.getSingleCategory)
model.js
let getSingleCat = (cb) => {
let queryString = 'SELECT * FROM categories WHERE category_id = $1'
let queryValue = [req.params.category]
db.query(queryString, queryValue, cb)
}
controller.js
const getSingleCategory = (req, response) => {
console.log(req.params.category);
db.desc.getSingleCat((err, queryRes) => {
if (err) {
//render something went wrong
response.send('something went wrong')
} else {
response.send(queryRes.rows)
}
})
}
I've checked all requires and they are working correctly. Is there a vanilla way of passing req.params around without using middleware?
The only way to use the req.params in the model, is by sending it as parameters as the following example:
model.js
let getSingleCat = (params, cb) => {
let queryString = 'SELECT * FROM categories WHERE category_id = $1'
let queryValue = params.category
db.query(queryString, queryValue, cb)
}
controller.js
const getSingleCategory = (req, response) => {
console.log(req.params.category);
db.desc.getSingleCat(req.params, (err, queryRes) => {
if (err) {
//render something went wrong
response.send('something went wrong')
} else {
response.send(queryRes.rows)
}
})
}
You can't use global vars, so this is the only way to do it. Also, this variable (req) can only be accessed in the functions bound to an endpoint that will receive an actual request.
I'm learning node.js. I have a client form, from which I send to my server some data and refactor it somehow in a module makeRequest. The module returns a promise, which I want to resolve with response to the client.
So I might think that I could do something like this:
let post = '';
const server = http.createServer(function (req, res) {
const readHtml = fs.createReadStream(__dirname + '/view/form.html', 'utf8');
readHtml.pipe(res);
});
server.on('request', processPost);
function processPost(request, response) {
let body = '';
if (request.method === 'POST') {
request.on('data', function (data) {
body += data;
if (body.length > 1e6) {
request.connection.destroy();
}
});
request.on('end', function () {
post = JSON.parse(body);
makeRequest(post.text)
.then(data => {
response.end(data);
})
.catch(err => {
console.log(err);
});
});
}
}
server.listen(3000, 'localhost');
This for me means that as soon as I retreive the refactored data I immediately send it back to client with response.end(data).
But suddenly the data does not return to the client if I do like this. It does, if I write response.end(data) after request.on('end'). But this is synchronous way and therefore when I run such code, there will be no data anyhow.
What can I do to make it work as I want to?
If, as said in a discussion below the question, you just want a default page accessed in a browser, make it conditional
const server = http.createServer(function(req, res) {
if ( req.method == "GET" ) {
const readHtml = fs.createReadStream(__dirname + '/view/form.html', 'utf8');
readHtml.pipe(res);
}
});
This way both GETs and POSTs are handled correctly.
My advice would be to use a more organized approach using any framework, Express should do fine. This way you could be a more clean approach to define different routes and request types.
I'm trying to have a total message count for a user's inbox displayed within my layout. I was thinking that I needed to use Express' dynamicHelpers to do this, but in Express <= 2.x, these are not async calls, and I need to do some async processing within them: in this case, a database call with a callback.
I'm trying the following to place the count within my session, which itself is put in a dynamicHelper accessible to the views. However, due to the asynchronous nature of these callbacks, session.unreadMessages is always undefined.
messageCount: function(req, res) {
var Messages = require('../controllers/messages'),
messages = new Messages(app.set('client'));
if(req.session.type === 'company') {
messages.getCompanyUnreadCount(req.session.uid, function(err, result) {
req.session.unreadMessages = result[0].unread;
});
} else if(req.session.type === 'coder') {
messages.getCoderUnreadCount(req.session.uid, function(err, result) {
req.session.unreadMessages = result[0].unread;
});
}
return;
}
Is there another or better way to perform this task?
It should be noted that req.session.unreadMessages is defined (at least within that callback), but undefined when session is called using the helper.
Not sure, it it would be a 'best way', but I'm used to using a filter (or a so called middleware) to load data before it reaches the actual destiny, like in:
filters.setReqView = function(req,res,next) {
req.viewVars = {
crumb: [],
flash_notice: augument,
}
somethingAsync(function(err,data){
req.viewVars.someData = data
next()
})
}
app.all('*', filters.setReqView )
// then on my request:
...
res.render('auth/win', req.viewVars )
Refactoring your code you would have:
app.all('*', function(req, res, next) {
if(req.session && req.session.type){
var Messages = require('./controllers/messages'),
messages = new Messages(app.set('client'));
if(req.session.type === 'company') {
messages.getCompanyUnreadCount(req.session.uid, function(err, result) {
req.session.messageCount = result[0].unread;
next();
});
} else if(req.session.type === 'coder') {
messages.getCoderUnreadCount(req.session.uid, function(err, result) {
req.session.messageCount = result[0].unread;
next();
});
}
} else {
next()
}
});