MongoError: No transaction started - javascript

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
}

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

Retrieve an array from a Firestore document and store it to Node.Js then use it as tokens to send notifications

I've been trying to figure this out for hours and I just can't. I'm still a beginner with Node.js and Firebase. I need your help to be able to retrieve the tokens array in my "userdata" collection to Node.js and be able to use it to send notifications in the Cloud Function. So far this is what I've been working on. Here is what my database looks like:
The receiverId is gathered from when I have an onCreate function whenever a user sends a new message. Then I used it to access the userdata of a specific user which uses the receiverId as their uid.
In the cloud function, I was able to start the function and retrieve the receiverId and print the userToken[key]. However, when I try to push the token it doesnt go through and it results in an error that says that the token is empty. See the image:
Your help would mean a lot. Thank you!
newData = snapshot.data();
console.log("Retrieving Receiver Id");
console.log(newData.receiverId); //uid of the user
const tokens = [];
const docRef = db.collection('userdata').doc(newData.receiverId);
docRef.get().then((doc) => {
if (doc.exists) {
console.log("DocRef exist");
const userToken = doc.data().tokens;
for(var key in userToken){
console.log(userToken[key]);
tokens.push(userToken[key]);
}
} else {
// doc.data() will be undefined in this case
console.log("No such document!");
}
}).catch((error) => {
console.log("Error getting document:", error);
});
//Notification Payload
var payload = {
notification: {
title: newData.sendBy,
body: 'Sent you a message',
sound: 'default',
},
data: {
click_action : 'FLUTTER_NOTIFICATION_CLICK',
route: '/telconsultinbox',
}
};
console.log("Sending Notification now.");
console.log(tokens);
try{
//send to device
const response = await admin.messaging().sendToDevice(tokens, payload);
console.log('Notification sent successfully');
console.log(newData.sendBy);
}catch(err){
console.log(err);
}
I think you should avoid using for..in to iterate through an array (you can read more about it in this answer). Try one of these 2 options:
You could use forEach(), which is more elegant:
userToken.forEach((token) => {
console.log(token);
tokens.push(token);
});
for-of statement:
for(const token of userToken){
console.log(token);
tokens.push(token);
}
Also, I would consider renaming userToken to userTokens, since it should contain multiple values. Makes the code a bit more readable.

Firebase Firestore is not creating a doc - any idea how to debug?

I've been struggling with a weird error. Can't create a doc in firebase. There are no security rules to speak of, just:
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write;
}
}
}
Firestore is initialised the normal way and is active:
import { Firebase } from "../db";
let firebase = Firebase();
let firestore = firebase.firestore();
But nothing happens after this is run other than printing "here1", the other consoles aren't doing anything and the userid doc is not being created and no collection and doc under it.
export const addEnquiry = async (data) => {
let user = await firebase.auth().currentUser;
data.uid = user.uid;
console.log("here1");
const enquiry = await firestore.collection("users").doc(data.uid).collection("enquiries").doc();
return await enquiry
.set(data)
.then((doc) => {
console.log("here2");
return true;
})
.catch((err) => {
console.log("here3");
console.log(err);
return false;
});
};
The above doesn't print anything other than "here1" and gets stuck on the setting of the doc. The doc isn't created in Firestore either.
Any idea what might be wrong and how to debug it? Wasted a good 4 hours on trying to figure it out and worried if Firestore is so buggy that it's unsafe to use it in production.
First of all, I assure you Firebase is not buggy at all, we have it running on several production applications and they're running fantastic.
Second, I think your issue here is that you're passing a function as the second argument in the set() method, which is nowhere that I can find in the API reference. Instead, it returns a promise. Your code should look like this:
firebase.firestore()
.collection("users")
.doc(uid)
.set({ uid: uid })
.then((doc) => { console.log(doc.id) })
.catch((err) => { console.log(err) })
Cheers.
Here is an example which will work for you:
file test.mjs
import { Firestore } from '#google-cloud/firestore';
const firestore = new Firestore()
export default (uid) => firestore.collection("users")
.doc(uid)
.set({ uid })
.then(() => console.log('success')) // documentReference.set() returns: Promise < void >
.catch(err => console.error(err))
It's super weird, but what solved the issue for me is adding an unnecessary doc.get() like so:
export const addEnquiry = async (data) => {
let user = await firebase.auth().currentUser;
data.uid = user.uid;
console.log("here1");
const enquiry = await firestore.collection("users").doc(data.uid).collection("enquiries").doc();
const x = await firestore.collection("users").doc(data.uid).get();
// ^^^ added the above line I don't actually need, but somehow it
// establishes a connection to firebase or something which allows the
// promise below to resolve, rather than just hang!
// Doesn't resolve without it for whatever reason!
return await enquiry
.set(data)
.then((doc) => {
console.log("here2");
return true;
})
.catch((err) => {
console.log("here3");
console.log(err);
return false;
});
};
When removing the line, the function hangs again. So have to keep it in for now!
A bit worrying that we have to use such workaround hacks to make a simple write to the firestore, to work, Firebase!
Nonetheless, hope it helps someone facing this undebuggable problem.

How to make sure all data is written into Firestore in a sequential order?

I'm trying to create a user profile that states that that profile is from one of the business owners in my app. It is supposed to create the profile and then merge info such as the 'roles' array with 'businessOwner' in it and also add the 'businessId'.
Sometimes, the code will work seamlessly. At other times, only the roles and the businessId will be passed to the created user (and all of the other information won't!).
async function writeToFirebase(values) {
authService.createUserWithEmailAndPassword(values.user.email, values.user.senha).then(
async function (user) {
userService.createUserProfileDocument(values.user)
const uid = user.user.uid
const userRef = await userService.doc(uid)
console.log('userRef', userRef)
try {
values.user.uid = uid
const { id } = await businessPendingApprovalService.collection().add(values)
await userRef.set({ roles: ['businessOwner'], businessId: id }, { merge: true })
} catch (error) {
console.error('error merging info')
}
},
function (error) {
var errorCode = error.code
var errorMessage = error.message
console.log(errorCode, errorMessage)
},
)
}
This is createUserWithEmailAndPassword:
async createUserProfileDocument(user, additionalData) {
if (!user) return
const userRef = this.firestore.doc(`users/${user.uid}`)
const snapshot = await userRef.get()
if (!snapshot.exists) {
const { displayName, email, photoURL, providerData } = user
try {
await userRef.set({
displayName,
email,
photoURL,
...additionalData,
providerData: providerData[0].providerId,
})
} catch (error) {
console.error('error creating user: ', error)
}
}
return this.getUserDocument(user.uid)
}
I think that the issue is on this line const snapshot = await userRef.get().
As stated in documentation you should fetch the snapshot using then() function in order to return the promise first.
I think you need to await on the below as well:-
await userService.createUserProfileDocument(values.user)
Since you are setting the user info here(await userRef.set), if you will not wait for the promise, then sometimes, your next block of code(await userRef.set({ roles: ['businessOwner'],) executes and after then your promise might get resolved. Because of this, you might not get the other information sometimes.
You also need to handle the error case of createUserProfileDocument.

firebase/firestore save user to collection

I am trying to save a user to a collection in my Firestore. It seems that the users are being created, I can see them in the Authentication tab, however they are not saving to my collection inside my firestore. There doesn't seem to be any errors inside my console either. I've been struggling with this for a couple hours now and i'm not even sure how to debug this.
export const authMethods = {
signup: (email, password, setErrors, setToken) => {
firebase
.auth()
.createUserWithEmailAndPassword(email, password)
// make res asynchronous so that we can make grab the token before saving it.
.then(async res => {
const token = await Object.entries(res.user)[5][1].b
// set token to localStorage
await localStorage.setItem('token', token)
// grab token from local storage and set to state.
setToken(window.localStorage.token)
const userUid = firebase.auth().currentUser.uid
const db = firebase.firestore()
db.collection('/users')
.doc(userUid)
.set({
email,
password,
})
console.log(res)
})
.catch(err => {
setErrors(prev => [...prev, err.message])
})
},
....
}
Any ideas?
Remove await from localStorage.setItem it isn't an asynchronous function.
You'll also need to add await to db.collection("/users").doc(userUid)
This is another approach that you could do. Let a cloud function handle that for you.
Whenever a user is created the following function is triggered.
export const onUserCreate = functions.auth
.user()
.onCreate(async (user, context) => {
await admin.firestore().collection("users").doc(user.uid).set({
id: user.uid,
emailAddress: user.email,
verified: user.emailVerified,
});
});
If you need more information about cloud functions read the following.
https://firebase.google.com/docs/functions/get-started

Categories