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!).
Related
I am building a fitness tracker through a class, it gives me built in tests to use as well. I am having an issue with passing this one in specific. I shortened the test specs for convenience.
Expected[{"activities": [{"activityId": 3,
Received {"publicRoutines": [{"activities": [{"activityId": 3,
1. Gets a list of public routines for a particular user.
2. Gets a list of all routines for the logged in user
I understand that the publicRoutines are sent in the res.send() but without the curly brackets, it sends over a failed test that is in my catch. Is there a way to send over both of these functions in my code to match the expected result?
usersRouter.get(`/:username/routines`,async(req,res) =>{
const username = req.params.username
try{
if(username){
const userRoutines = await getAllRoutinesByUser({username});
const publicRoutines = await getPublicRoutinesByUser({username})
console.log(publicRoutines, userRoutines)
res.send({publicRoutines, userRoutines})
}else{
return null;
}
}catch(error){
throw Error('Failed to get', error)
}
})
Yes, you can modify your code to send the expected result format by combining the two objects into a single object:
usersRouter.get(`/:username/routines`, async (req, res) => {
const username = req.params.username;
try {
if (username) {
const userRoutines = await getAllRoutinesByUser({ username });
const publicRoutines = await getPublicRoutinesByUser({ username });
console.log(publicRoutines, userRoutines);
res.send({ activities: [...publicRoutines.activities, ...userRoutines.activities] });
} else {
return null;
}
} catch (error) {
throw Error("Failed to get", error);
}
});
This way, you are combining the arrays of activities from both publicRoutines and userRoutines and returning it in the format that the test is expecting.
I tried to find the solutions over here but unable to get success while using $pull as the array values I have does not contain `mongo_id'.
So the scenario is that , I am trying to delete the specific comment of the particular user which I am passing through query params. M
My mongo data looks like this:
Now I am making API Delete request like this : http://localhost:8000/api/articles/learn-react/delete-comment?q=1 on my localhost .
ANd finally my code looks like this:
import express from "express";
import bodyParser from "body-parser";
import { MongoClient } from "MongoDB";
const withDB = async (operations, res) => {
try {
const client = await MongoClient.connect(
"mongodb://localhost:27017",
{ useNewUrlParser: true },
{ useUnifiedTopology: true }
);
const db = client.db("my-blog");
await operations(db);
client.close();
} catch (error) {
res.status(500).json({ message: "Error connecting to db", error });
}
};
app.delete("/api/articles/:name/delete-comment", (req, res) => {
const articleName = req.params.name;
const commentIndex = req.query.q;
withDB(async(db) => {
try{
const articleInfo = await db.collection('articles').findOne({name:articleName});
let articleAllComment = articleInfo.comments;
console.log("before =",articleAllComment)
const commentToBeDeleted = articleInfo.comments[commentIndex];
//console.log(commentToBeDeleted)
// articleAllComment.update({
// $pull: { 'comments':{username: commentToBeDeleted.username }}
// });
articleAllComment = articleAllComment.filter( (item) => item != commentToBeDeleted );
await articleAllComment.save();
console.log("after - ",articleAllComment);
//yaha per index chahiye per kaise milega pta nhi?
//articleInfo.comments = gives artcle comment
res.status(200).send(articleAllComment);
}
catch(err)
{
res.status(500).send("Error occurred")
}
},res);
});
I have used the filter function but it is not showing any error in terminal but also getting 500 status at postman.
Unable to figure out the error?
I believe you'll find a good answer here:
https://stackoverflow.com/a/4588909/9951599
Something to consider...
You can use MongoDB's built-in projection methods to simplify your code.
https://docs.mongodb.com/manual/reference/operator/projection/positional/#mongodb-projection-proj.-
By assigning a "unique ID" to each of your comments, you can find/modify the comment quickly using an update command instead of pulling out the comment by order in the array. This is more efficient, and much simpler. Plus, multiple read/writes at once won't interfere with this logic during busy times, ensuring that you're always deleting the right comment.
Solution #1: The recommended way, with atomic operators
Here is how you can let MongoDB pull it for you if you give each of your comments an ID.
await db.collection('articles').updateOne({ name:articleName },
{
$pull:{ "comments.id":commentID }
});
// Or
await db.collection('articles').updateOne({ name:articleName, "comments.id":commentID },
{
$unset:{ "comments.$":0 }
});
Solution #2 - Not recommended
Alternatively, you could remove it by index:
// I'm using "3" here staticly, put the index of your comment there instead.
db.collection('articles').updateOne({ name:articleName }, {
$unset : { "comments.3":0 }
})
I do not know why your filter is erroring, but I would recommend bypassing the filter altogether and try to utilize MongoDB's atomic system for you.
I have a route like http://localhost:3000/admin/video/edit/5 and the controller looks like this
albumEdit: async (req, res) => {
const editInfoId = req.params.id;
await Movie.findOne({ where: { id: editInfoId } }).then((movie) => {
if (movie) {
res.render('admin/movies/edit', { title: 'Edit Movie On Page One', movie });
}
});
},
for the testing purpose when I type the wrong id after edit/ then the process is freezing after some time I am getting 500 errors.
how to prevent this if someone tries to break my app with the wrong id in the URL? I want something like if anyone tries to do this application redirect to an error page.
I am new in node js express js I need some info.
Your route will freeze if movie is falsy or if fineOne results in an error because for both of these cases you don't send any response.
after some time I am getting 500 errors.
If you run your node server behind a web server then this 500 is due to a timeout because your router does not send a response.
how to prevent this if someone tries to break my app with the wrong id in the URL? I want something like if anyone tries to do this application redirect to an error page.
As with any programming language or code, make sure you handle all control flows and possible exceptions.
Besides that, if you use await you in most of the cases don't want to use .then.
albumEdit: async (req, res) => {
const editInfoId = req.params.id;
try {
let movie = await Movie.findOne({
where: {
id: editInfoId
}
})
if (movie) {
res.render('admin/movies/edit', {
title: 'Edit Movie On Page One',
movie
});
} else {
// either the if is not necessary or you have to also handle the else cases
// send some error response
res.send('error')
}
} catch (err) {
// send some error response
res.send('error')
}
}
For completeness, this is how where you would need to do changes in your code, but as said above don't mix await and then:
albumEdit: async (req, res) => {
const editInfoId = req.params.id;
try {
await Movie.findOne({
where: {
id: editInfoId
}
}).then((movie) => {
if (movie) {
res.render('admin/movies/edit', {
title: 'Edit Movie On Page One',
movie
});
} else {
// either the if is not necessary or you have to also handle the else cases
// send some error response
res.send('error')
}
});
} catch (err) {
// send some error response
res.send('error')
}
}
I want users to pay a fee before a POST request from a front end form is processed. I have a Stripe webhook that works fine on the backend, but I'm not sure how to delay the front end posting of the form until after the payment confirmation is received.
In the code below, right now, createTour and createTourPay run at the same time. I would like for createTourPay to execute first, and the createTour only triggers after Stripe posts to my application from the webhook. How can I achieve this?
Controller File (webhook):
exports.webhookCheckout = (req, res, next) => {
const signature = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
signature,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
return res.status(400).send(`Webhook error: ${err.message}`);
}
if (
event.type === 'checkout.session.completed' &&
event.line_items.name === 'New Job Purchase'
) {
res.status(200).json({ recieved: true });
// Somehow, I want this to trigger the execution of the POST request in my front end JS file.
} else {
if (event.type === 'checkout.session.completed')
createBookingCheckout(event.data.object);
res.status(200).json({ recieved: true });
}
};
Front end JS file:
export const createTourPay = async myForm => {
try {
// 1) Get the checkout session from API response
const session = await axios(`/api/v1/tours/tour-pay`);
const complete = 1;
// console.log(session);
// 2) Create checkout form + charge the credit card
await stripe.redirectToCheckout({
sessionId: session.data.session.id
});
} catch (err) {
// console.log(err);
showAlert('error', err);
}
};
export const createTour = async myForm => {
try {
const startLocation = {
type: 'Point',
coordinates: [-10.185942, 95.774772],
address: '123 Main Street',
description: 'Candy Land'
};
const res = await axios({
method: 'POST',
headers: {
'Content-Type': `multipart/form-data; boundary=${myForm._boundary}`
},
url: '/api/v1/tours',
data: myForm
});
if (res.data.status === 'success') {
showAlert('success', 'NEW TOUR CREATED!');
window.setTimeout(() => {
location.assign('/');
}, 1500);
}
} catch (err) {
showAlert('error', err.response.data.message);
}
};
Broadly: don't do this. Instead, you in fact should create some pending/unpaid version of the "tour" (or any other product/service) in your system, then attach the unique id (eg: tour_123) to the Checkout session when you create it, either using the client_reference_id (doc) or metadata (doc):
const session = await stripe.checkout.sessions.create({
// ... other params
client_reference_id: 'tour_123',
metadata: { tour_id: 'tour_123' },
});
Then you'd use the webhook to inspect those values, and update your own database to indicate the payment has been made and that you can fulfill the order to the customer (ship product, send codes, allow access to service etc).
If you really want to proceed with a more synchronous flow, you can use separate auth and capture to sequence your customer experience and capture the funds later after authorizing and creating your tour entity.
Edit: a note about security
You should never trust client-side logic for restricted operations like creating a "paid" tour. A motivated user could, for example, simply call your /api/v1/tours create endpoint without ever going through your payment flow. Unless you validate a payment and track that state on your server you won't be able to know which of these had actually paid you.
I am using mongoose transactions for the first time. Following the documentation and some articles, I was able to get it running using run-rs for local replicas. However, I encountered two issues,
Even though the transaction reflects on the replica sets, mongoose always throws the error MongoError: No transaction started. I have tried checking for solutions but can't find any to solve this problem.
Upon successful completion of the transaction, there's another async function that is meant to send notification emails. However, I realized that somehow, this notification email function runs before the transaction occurs, then the transaction function runs second. I am guessing this might have to do with promises, correct me if I am wrong.
Here's what the two functions look like.
await transactionDb
.handleMoneyTransfer({
senderId,
receiverId: paymentInfo.getReceiver(),
amount: paymentInfo.getAmountToPay(),
ref
})
return await sendNotificationEmail({ ref, user })
The handleMoneyTransfer function is meant to run first, then the sendNotificationEmail is meant to run next, but that's not the case here.
Here is the code that handles the mongoose transaction listed below.
async function handleMoneyTransfer({ senderId, receiverId, amount, ref }) {
const session = await mongoose.startSession()
try {
const sender = await User.findOne({ _id: senderId }).session(session)
sender.balance -= amount
await sender.save({ session })
const receiver = await User.findOne({ _id: receiverId }).session(session)
// receiver.balance += amount
const transactionInfo = await Transaction.findOne({
reference: ref
}).session(session)
const newEscrow = await new Escrow({
amount,
reference: ref,
buyerInfo: {
buyerId: sender._id,
email: sender.email
},
sellerInfo: {
sellerId: receiverId,
email: receiver.email
},
currentTransaction: {
transaction: transactionInfo
}
})
await newEscrow.save({ session })
await session.commitTransaction()
} catch (error) {
await session.abortTransaction()
} finally {
session.endSession()
}
}
Here is how I connect using mongoose
const setupDB = async (uri, dbUrl) => {
try {
await mongoose.connect(`${uri}/${dbUrl}`, {
useUnifiedTopology: true,
useNewUrlParser: true,
replicaSet: 'rs'
})
console.log('Connected')
} catch (e) {
return console.log(e)
}
}
which is translated to this
setupDB(
'mongodb://DESKTOP-SNA1HQK:27017,DESKTOP-SNA1HQK:27018,DESKTOP-SNA1HQK:27019',
'escrow?replicaSet=rs'
)
Now, I am stuck on fixing the No transaction started error and also getting these functions to run in the order they are placed.
Help will be very much appreciated, thank you in advance.
You seem to be missing the actual start of a transaction. Adding the following to your code should fix the issue:
async function handleMoneyTransfer({ senderId, receiverId, amount, ref }) {
const session = await mongoose.startSession()
session.startTransaction();
// rest of your code
}