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
};
CodePudding user response:
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);
}
CodePudding user response:
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 sametry...catch
. Largetry...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 (