Seperation of concerns (service and router layer) - javascript

I would like to improve my skills in Node JS. Right now I'm interested in how to cleanly separate the service and router layers of an application so I can avoid something like code duplication.
The create method of a user scheme shall serve as an example.
In UserService.Js I store the following method for this:
function createUser(req, res, next) {
let user = new User({
userID: req.body.userID,
userName: req.body.userName,
password: req.body.password,
isAdministrator: req.body.isAdministrator
});
user.save().then(function() {
res.send(user);
}).catch(err => {
console.log(err);
});
}
The following code is stored in UserRouter.Js:
router.post('/publicUser', userService.createUser)
The code works, but the separation of concerns is not respected. How do I write the create function now with a callback function ?
My attempt looks like this:
UserService.js
function createUser() {
let user = new User
return user;
}
UserRoute.js
router.post('/publicUser',function(req,res,next){
let newOne=userService.createUser()
newOne.userID=req.body.userID
newOne.userName=req.body.userName
newOne.password=req.body.password
newOne.isAdministrator=req.body.isAdministrator
newOne.save().then(function() {
res.send(newOne);
}).catch(err => {
console.log(err);
})})
It works. But does a more elegant way exist ?

Below is the code to give you an idea of implementation. You can further enhance as per the complexity and requirements.
UserRouter.Js
// import service here
router.post('/publicUser', createUser)
async function createUser(req, res, next) {
try {
const response = await UserService.createUser(req.body);
res.send(response); // Enhance 'res' object here and return as per your requirement.
} catch (error) {
// log error
res.status(500).send(''); // Enhance 'res' object here with error and statuscode and return as per your requirement.
}
}
UserService.Js
async function createUser(body) {
// check emptiness if any for body and throw proper errors.
let userModelData = UserModel.getUserInsertPayload(body);
return UserRepository.save(userModelData);
}
UserRepository.js
// all code related to persistance. This is separate layer.
async function save(user) {
// Do any enhancement you need over the payload.
return User.save(user);
}
UserModel.js
// import User here. Create a payload related to User specific requirements.
function getUserInsertPayload(body) {
return new User({
userID: req.body.userID,
userName: req.body.userName,
password: req.body.password,
isAdministrator: req.body.isAdministrator
});
}

Related

Use transaction session in try/catch function from wrapper

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!).

Separating Mongoose code from Express Router

So basically, I'm trying to separate my code that handles data (mongoose) from my express Router code, since I might want to use it elsewhere too.
The first thing I did was, I got rid of the res.json() calls, since I don't want the code to only work returning a http response. I want it to return data, so I can then return that data from my router as a http response, but still use it as regular data elsewhere.
Here is a function I wrote to get data from mongoose.
module.exports.user_login = data => {
console.log(data);
ModelUser.findOne({email: data.email}).then(user => {
if(!user){
console.log({email: 'E-mail address not found'});
return {
status: response_code.HTTP_404,
response: {email: 'E-mail address not found'}
}
}
bcrypt.compare(data.password, user.password).then(isMatch => {
if(!isMatch){
console.log({password: 'Invalid password'});
return {
status: response_code.HTTP_400,
response: {password: 'Invalid password'}
}
}
const payload = {
id: user.id,
email: user.email
};
jwt.sign(
payload,
config.PASSPORT_SECRET,
{
expiresIn: "1h"
},
(err, token) => {
console.log({
status: response_code.HTTP_200,
response: {
success: true,
token: token
}
});
return {
status: response_code.HTTP_200,
response: {
success: true,
token: token
}
}
}
);
});
});
};
When this code gets executed in my route like so:
router.post("/login", (req, res) => {
const { errors, isValid } = validateLogin(req.body);
if(!isValid) return res.status(400).json(errors);
console.log("ret", dm_user.user_login(req.body));
});
The log says the return value of user_login() is undefined, even though right before the return statement in user_login() I am logging the exact same values and they are getting logged.
Before I changed it to a log, I tried to store the return value in a variable, but obviously that remained undefined as well, and I got the error: 'Cannot read propery 'status' of undefined' when trying to use the value.
I am definitely missing something..
Well you have an small callback hell here. It might be a good idea to go with async / await and splitting up your code into smaller chunks instead of putting everyhing in 1 file.
I rewrote your user_login function:
const { generateToken } = require("./token.js");
module.exports.user_login = async data => {
let user = await ModelUser.findOne({ email: data.email });
if (!user) {
console.log({ email: "E-mail address not found" });
return {
status: response_code.HTTP_404,
response: { email: "E-mail address not found" }
};
}
let isMatch = await bcrypt.compare(data.password, user.password);
if (!isMatch) {
console.log({ password: "Invalid password" });
return {
status: response_code.HTTP_400,
response: { password: "Invalid password" }
};
}
const payload = {
id: user.id,
email: user.email
};
let response = await generateToken(
payload,
config.PASSPORT_SECRET,
response_code
);
return response;
};
I have moved your token signing method into another file and promisfied it:
module.exports.generateToken = (payload, secret, response_code) => {
return new Promise((res, rej) => {
jwt.sign(
payload,
secret,
{
expiresIn: "1h"
},
(err, token) => {
if (err) {
rej(err);
}
res({
status: response_code.HTTP_200,
response: {
success: true,
token: token
}
});
}
);
});
};
Now you need to change your router function into an async:
router.post("/login", async (req, res) => {
const { errors, isValid } = validateLogin(req.body);
if(!isValid) return res.status(400).json(errors);
let result = await dm_user.user_login(req.body);
console.log(result);
});
In addition: You get undefined because you return your value to an callback function
I also would seperate your routes from your controllers instead of writing your code inside an anonymous function
Please notice that whenever you are trying to return any value you are always present in the callback function and that is definitely not going to return any value to its intended place.
There are a couple of things you can improve about your code :
1.Donot use jwt inside your code where you are making database calls, instead move it where your routes are defined or make a separate file.
2.If you are intending to re-use the code, I would suggest you either use async-await as shown in the answer above by Ifaruki or you can use something like async.js. But the above shown approach is better.
Also always use 'error' field when you are making db calls like this:
ModelUser.findOne({email: data.email}).then((error,user) => {

Firebase Cloud Functions - How to call another function and return object

I've created a callable function from my Angular component. My angular component calls the createUser function and successfully returns the userRecord value.
However, what I would like to do is call another cloud function called createUserRecord. I'm not overly familiar with promises and what needs to be returned where in this particular scenario.
Below are my two cloud functions. How would I go about calling createUserRecord upon the success of createUser?
export const createUser = functions.https.onCall(async (data, context) => {
console.log('data = ', data);
return admin.auth().createUser({
email: data.email,
password: data.password,
}).then(function (userRecord) {
return userRecord
})
.catch(function (error) {
return error;
console.log('Error creating new user:', error);
});
});
export const createUserRecord = functions.auth.user().onCreate((user, context) => {
const userRef = db.doc(`users/${user.uid}`);
return userRef.set({
email: user.displayName,
createdAt: context.timestamp,
nickname: 'bubba',
})
});
Update
This is a version I produced where I merged the two functions together. This does produce the expected result of creating and account and then writing to firestore. However, it does feel a little 'off' due to the fact that it doesn't return a value to the client.
export const createUser = functions.https.onCall(async (data, context) => {
console.log('data = ', data);
return admin.auth().createUser({
email: data.email,
password: data.password,
}).then(function (userRecord) {
const userRef = db.doc(`users/${userRecord.uid}`);
return userRef.set({
email: data.email,
name: data.name,
})
})
.catch(function (error) {
return error;
console.log('Error creating new user:', error);
});
});
Angular Callable Function
The sanitizedMessage console log will return undefined.
addUser() {
const createUser = firebase.functions().httpsCallable('createUser');
const uniquePassword = this.afs.createId();
createUser({
email: this.userForm.value.email,
password: uniquePassword,
name: this.userForm.value.name,
}).then((result) => {
// Read result of the Cloud Function.
var sanitizedMessage = result.data.text;
console.log('sanitizedMessage = ', sanitizedMessage);
}).catch((error) => {
var code = error.code;
var message = error.message;
var details = error.details;
console.log('error = ', error);
});
}
If you want to create a record in Firestore upon user creation you can very well do that within a unique Cloud Function. The following code will do the trick, making the assumption that you want to write to the users Firestore collection.
const FieldValue = require('firebase-admin').firestore.FieldValue;
...
export const createUser = functions.https.onCall((data, context) => {
console.log('data = ', data);
return admin
.auth()
.createUser({
email: data.email,
password: data.password
})
.then(userRecord => {
return admin.firestore().collection('users')
.doc(userRecord.uid)
.set({
email: userRecord.displayName,
createdAt: FieldValue.serverTimestamp(),
nickname: 'bubba'
});
})
.then(() => {
return {
result: 'Success'
};
})
.catch(error => {
//Look at the documentation for Callable Cloud Functions to adapt this part:
//https://firebase.google.com/docs/functions/callable?authuser=0
});
});
"Is there any particular reason not to chain functions in CF's?" ?
As explained in the documentation, "Cloud Functions can be associated with a specific trigger". You can "chain" Cloud Functions by creating the corresponding triggers, for example, creating a doc in Firestore in one CF (Callable Function for example), that would trigger another CF that respond to a Firestore trigger). Having said that, in most cases you can probably cover a lot of needs in a unique Cloud Function, by chaining promises, instead of chaining Cloud Functions.
Finally, I would not recommend to call an HTTP Cloud Function from within a Cloud Function because (IMHO) HTTP Cloud Functions are more designed to be called by an external consumer (I even don't know if this would work).
It would be interesting to have Firebasers opinion on that!

ExpressJS variable undefined

I have an ExpressJS app that when a user makes a POST request to a route, it should lookup the ID in the MongoDB using req.params.formId
I have some console.log statements tfor debugging and so I can see what info is being returned.
The route should lookup the ID passed and when it finds it, use the req.body data and also a field from the MongoDB document but this just seems to return as undefined
Here is the code for the route:
app.post("/api/v1/forms/:formId", (req, res) => {
const { name, email, message } = req.body;
console.log(req.body);
Form.findById(req.params.formId, Form.recipient, err => {
if (err) {
res.send(err);
} else {
const formRecipient = Form.recipient;
const newForm = {
name,
email,
message,
recipient: formRecipient
};
console.log(newForm);
const mailer = new Mailer(newForm, contactFormTemplate(newForm));
try {
mailer.send();
res.send(req.body);
} catch (err) {
res.send(err);
}
}
});
});
So an example, if I make a POST request to localhost:5000/api/v1/forms/5ad90544883a6e34ec738c19 the console.log of newForm shows { name: ' Mr Tester',
email: 'person#example.com',
message: 'Hi there',
recipient: undefined }
The forms Mongoose schema has a field named recipient
the correct way is to provide the fields you want to get as the second argument:
Form.findById(req.params.formId, 'recipient', (err, form) => {
if (err) {
// error handling code
} else {
const formRecipient = form.recipient;
}
...
});
here's the Docs

How do I pass arguments to a passport strategy callback using a bluebird promise?

I’m trying to promisify passport.js’s local strategy. I'm new to both promises and passport, and I’m relying heavily on this comment thread, which deals with passing additional arguments to passport’s done() callback using bluebird’s Promise library. This comment resulted in a new bluebird implementation that handles additional callback parameters, but I can’t get it working in my own code:
const passport = require('passport');
const User = require('../models/user');
const LocalStrategy = require('passport-local');
const NoMatchedUserError = require('../helpers/error_helper').NoMatchedUserError;
const NotActivatedError = require('../helpers/error_helper').NotActivatedError;
const localOptions = { usernameField: 'email' };
const localLogin = new LocalStrategy(localOptions, function(email, password, done) {
let user;
User.findOne({ email: email }).exec()
.then((existingUser) => {
if (!existingUser) { throw new NoMatchedUserError('This is not a valid email address.'); }
user = existingUser;
return user.comparePassword(password, user.password);
})
.then((isMatch) => {
if (!isMatch) { throw new NoMatchedUserError('This is not a valid password.'); }
return user.isActivated();
})
.then((isActivated) => {
if (!isActivated) { throw new NotActivatedError('Your account has not been activated.'); }
return user;
})
.asCallback(done, { spread: true });
});
The user is able to validate without a problem. It’s authentication failure that I’m having an issue with: done(null, false, { message: ‘message’} obviously isn’t being called in the .asCallback method. I’m pretty sure it has to do with throwing an error, so instead I tried using this:
if (!existingUser) { return [ false, { message: 'This is not a valid email address.' } ]; }
But returning an array also doesn't work since it gets passed down the promise chain and breaks the code.
Any thoughts?

Categories