mongoose how to send transaction multiple collections - javascript

Side-Note I connect to DB with the following code:
const mongoose = require('mongoose');
const connectDB = (url) => {
return mongoose.connect(url);
}
Problem Description:
I have two different Collections. Both Operations, findByIdAndUpdate and create must run as an atomic operation. This should be possible with mongoose Transactions.
const registerCustomer = async (req, res) => {
await CustomerRegistrationCode.findByIdAndUpdate(req.body._id, { used: true });
const customer = await Customer.create({firstName: req.body.firstName});
}
What I tried:
const registerCustomer = async (req, res) => {
const session = await mongoose.startSession();
await session.startTransaction();
try {
await CustomerRegistrationCode.findByIdAndUpdate(req.body._id, { used: true }); //updates even though
const customer = await Customer.create({ firstName: req.body.firstName });// this line will throw error
await session.commitTransaction();
session.endSession();
} catch (error) {
console.error('abort transaction');
await session.abortTransaction();
session.endSession();
throw error;
}
}
Problem The CustomerRegistrationCode Collection gets updated even though the Customer.create method throws an error. How can this be solved?
New approach to understand MongoDB Transactions fails, but this is official code from https://mongoosejs.com/docs/transactions.html
const mongoose = require('mongoose');
const debugMongo = async () => {
const db = await mongoose.createConnection("mongodb://localhost:27017/mongotest");
const Customer = db.model('Customer', new mongoose.Schema({ name: String }));
const session = await db.startSession();
session.startTransaction();
await Customer.create([{ name: 'Test' }], { session: session }); //(node:20416) UnhandledPromiseRejectionWarning: MongoServerError: Transaction numbers are only allowed on a replica set member or mongos
let doc = await Customer.findOne({ name: 'Test' });
assert.ok(!doc);
doc = await Customer.findOne({ name: 'Test' }).session(session);
assert.ok(doc);
await session.commitTransaction();
doc = await Customer.findOne({ name: 'Test' });
assert.ok(doc);
session.endSession();
}
debugMongo();
At Customer.create an error gets thrown and i don't know why. Does somebody have an minimal working example?

You are using the transaction in a wrong way, that is why it does not work.
You need to pass the session object to your operations.
const registerCustomer = async (req, res) => {
const session = await mongoose.startSession();
session.startTransaction();
try {
await CustomerRegistrationCode.findByIdAndUpdate(req.body._id, { used: true }, { session });
const customer = await Customer.create({ firstName: req.body.firstName }, { session });
await session.commitTransaction();
} catch (error) {
console.error('abort transaction');
await session.abortTransaction();
} finally {
session.endSession();
}
}
Also, I have refactored your code a bit.
You can read more about transactions here

Related

I want to save a data from api to mongodb compass by mongose?

am trying to save a data from fetch api to my database using mongoose so
the data never come.
could anyone help? and thank you,
this is my code
`
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/Quran')
const SurahSchema = mongoose.Schema({
ayahs:[{
number:Number,
numberInSurah:Number,
text:String
}],
englishName:String,
englishNameTranslation:String,
name:String,
number:Number,
revelationType:String,
});
var surah = mongoose.model('Surah',SurahSchema)
`
`
const API = 'http://api.alquran.cloud/v1/quran/quran-uthmani'
async function getdata(){
const res = await fetch(API)
const data = await res.json()
for (let i = 0; i < data.data.surahs.length; i++) {
const Surah = new surah({
ayahs:[{
number:data.data.surahs[i]['ayahs'].number,
numberInSurah:data.data.surahs[i]['ayahs'].numberInSurah,
text:data.data.surahs[i]['ayahs'].text
}],
englishName:data.data.surahs[i]['englishName'],
englishNameTranslation:data.data.surahs[i]['englishNameTranslation'],
name:data.data.surahs[i]['name'],
number:data.data.surahs[i]['number'],
revelationType:data.data.surahs[i]['revelationType']
})
Surah.save(function (err) {
if (err) return handleError(err);
// saved!
});
}
}
getdata()
`
i tried to search about the problem in google and i did not find anything similar.
Instead of writing such a complicated for loop, you can just use the .insertMany() method on the model
const API = 'http://api.alquran.cloud/v1/quran/quran-uthmani'
async function getdata(){
const res = await fetch(API);
const data = await res.json();
try{
const inserted = await surah.insertMany(data.data.surahs);
}catch(e){
console.log("Some error");
console.log(e);
}
}
getdata()
the following is a complete working snippet
import mongoose from 'mongoose';
import fetch from 'node-fetch';
mongoose.connect('mongodb://localhost:27017/Quran');
const SurahSchema = mongoose.Schema({
ayahs: [
{
number: Number,
numberInSurah: Number,
text: String,
},
],
englishName: String,
englishNameTranslation: String,
name: String,
number: Number,
revelationType: String,
});
const surah = mongoose.model('Surah', SurahSchema);
const API = 'http://api.alquran.cloud/v1/quran/quran-uthmani';
async function getdata() {
const res = await fetch(API);
const data = await res.json();
try {
const inserted = await surah.insertMany(data.data.surahs);
console.log(inserted);
process.exit(0);
} catch (e) {
console.log('Some error');
console.log(e);
process.exit(0);
}
}
getdata();
this inserted 114 documents
PS: you just need to install node-fetch and mongoose

MongoError: Cannot call abortTransaction twice; MongoError: Cannot call abortTransaction after calling commitTransaction

When I run the send route I have error:
MongoError: Cannot call abortTransaction twice
and
MongoError: Cannot call abortTransaction after calling commitTransaction.
I have two collections car and color. And in the same time I want to add to arrays:
car.colors.push(model); color.brands.push(year); and save in database. But I want use withTransaction and session from mongoose. I don't want the situation that due to error, car.colors.push(model); will be saved in database, but color.brands.push(year); won't be saved.
module.exports.send = async (req, res) => {
const sess = await mongoose.startSession();
if(role === 'car') {
try {
await sess.withTransaction(async () => {
const car = await Cars.findOne({ _id: sender});
const color = await Colors.findOne({_id: keeper});
let model = {
contentInfo : {
msg : msg
}
};
let year = {
contentInfo : {
msg : msg
}
}
car.colors.push(model);
color.brands.push(year);
await car.save({session: sess});
await color.save({session: sess});
await sess.commitTransaction();
sess.endSession();
return res.json(car);
});
} catch (error) {
await sess.abortTransaction();
sess.endSession();
throw error;
}
}
}

Getting results from the asynchronus query in nodejs

I'm extracting some data from the database, to use it in my nodejs app.
I'm using node-postgres to connect to the db (https://node-postgres.com/).
I went through the guidance multiple times and tried querying it in different ways (callback, promise, using pool and client), but always get errors.
const { Pool } = require('pg');
const pool = new Pool({
user: 'user',
host: 'host',
database: 'db',
password: 'pass',
port: port,
});
pool.query('SELECT * from table').then(res=> {
var projects = res.rows;
console.log(projects);
return projects;
});
//... few other operations on projects data to follow before exports
exports.raw = projects;
I can see the data in the console output, so the connection is working, but when I try to run the code I get
ReferenceError: projects is not defined.
Grateful for any help with this.
You can simply achieve by async/await code format
users.js
const users = {};
const { Pool } = require('pg');
const pool = new Pool({
user: 'user',
host: 'host',
database: 'db',
password: 'pass',
port: process.env.port,
});
users.getUsers = async () => {
try {
const result = await pool.query('SELECT * FROM users');
console.log(result);
return result;
} catch (err) {
console.error(err);
throw err;
}
};
module.exports = users;
friends.js
const friends = {};
const users = require('./users');
friends.getSampleData = async () => {
try {
const result = await users.getUsers();
console.log(result);
return result;
} catch (err) {
console.error(err);
throw err;
}
};
module.exports = friends;
Note async/await only works on a function which returns a Promise.
Just like Jeremy answered, create a getter method in order to fetch data, don't try to store it before you need it.
const fetchData = async() => {
const res = await pool.query('SELECT * from table')
return res.rows;
}
const myDesiredData = fetchData()
Try the async/await syntax :
let projects;
(async () => {
const res = await pool.query('SELECT * from table');
projects = res.rows;
})();
exports.raw = projects;

MongoDB best approach to only POST data if it doesn't exist in the db?

What would be the best approach to only POST data into MongoDB if it doesn't exist in the table? hash would be the unique field for searching, this field is also indexed.
router.post('/', async (req, res) => {
try{
var id = null
const keywords = await db.dbConn('kw_data');
await keywords.insertOne({
result: req.body,
added: new Date(),
modified: new Date(),
hash: req.body.hash
}).then(resp => {
id = resp.insertedId
})
var data = {}
data = await keywords.findOne({_id: id});
res.status(201).send(data);
}
catch(e) {
res.status(400).send(e);
res.status(404).send(e);
res.status(500).send(e);
}
})
You can use
await keywords.update(
<query>,
<update>,
{
upsert: <boolean>,
}
);
and set
upsert:true
So you are inserting data and database will itself know if data is created it will get updated and if it does not exist the database will create it for you
I think I have got it by just doing a check on the hash, but not sure if this is the best approach, correct me if I'm wrong...
router.post('/sug', async (req, res) => {
try{
var id = null
const keywords = await db.dbConn('kw_sug');
check = await keywords.findOne({hash: req.body.hash});
if(check === null){
await keywords.insertOne({
results: req.body,
added: new Date(),
modified: new Date(),
hash: req.body.hash
}).then(resp => {
id = resp.insertedId
})
var data = {}
data = await keywords.findOne({_id: id});
res.status(201).send(data);
}else{
res.status(201).send('duplicated');
}
}
catch(e) {
res.status(400).send(e);
res.status(404).send(e);
res.status(500).send(e);
}
})

Code not executed after PUT route using Express

I listen to the chat event of the tmijs library, upon the !overlay chat I want to execute some code. What I want to achieve upon getting that message is:
Fetch the user
Check if the user has enough currency
Deduct currency from the user
Trigger a socket event to my react app
Everything seems to work up until the last bullet point. In my terminal it's shown that my user gets currency (called 'kluiten' in my code) deducted, but all the code that comes after it doesn't get executed.
require('dotenv').config();
const PORT = process.env.PORT || 9000;
class TwitchAPI {
constructor({io}) {
this.io = io;
this.client = new tmi.client(options);
this.client.connect();
this.handleOverlayRequest = this.handleOverlayRequest.bind(this);
this.handleChatMessage = this.handleChatMessage.bind(this);
this.client.on('chat', this.handleChatMessage);
}
handleChatMessage (channel, userstate, message) {
if(message === '!overlay') this.handleOverlayRequest(channel, userstate);
}
async handleOverlayRequest (channel, userstate) {
const requiredKluiten = 5;
const rawFoundUser = await fetch(`http://localhost:${PORT}/api/users/${userstate.username}`);
const foundUser = await rawFoundUser.json();
if(foundUser.instakluiten >= requiredKluiten) {
this.client.action(channel, `${userstate[`display-name`]}, you've got enough instakluiten for this.`);
const method = `PUT`;
const payload = { 'requiredKluiten': requiredKluiten };
const body = JSON.stringify(payload);
const headers = { 'Content-Type': `application/json; charset=utf-8` };
const result = await fetch(`http://localhost:${PORT}/api/users/${userstate.username}/decrementKluiten`, { method, body, headers });
console.log(result);
}
}
}
module.exports = TwitchAPI;
I then have an Express router:
const express = require('express');
const userController = require('../controllers/userController');
const router = express.Router();
router.route('/users/:username/decrementKluiten').put(userController.decrementKluiten);
router.route('/users/:username').get(userController.getUser);
router.route('/overview').get(userController.getOverview);
module.exports = router;
which makes sure the currency gets deducted. What I'm stuck on now is that, after all this has happened, I can't execute any code anymore after the fetch. I found though that I could execute code by resolving the promise in my route, but that feels really dirty and messes up my split up files:
router.route('/users/:username/decrementKluiten').put((req, res) => {
userController.decrementKluiten(req, res).then(x => {
console.log(x);
});
});
Is there a way to wait for my PUT to happen and still execute code after it did?
EDIT
userController.js
const {findChattersPerRole, getUserByUsername, decrementKluiten} = require('../actions');
const find = require(`lodash/find`);
const fetch = require(`isomorphic-fetch`);
const parseJSON = response => response.json();
module.exports = {
getUser: (req, res) => {
const username = req.params.username;
findChattersPerRole()
.then(chattersPerRole => {
const wantedUser = find(chattersPerRole, { username });
getUserByUsername(wantedUser.username)
.then(foundUser => {
if (foundUser) {
res.send(foundUser);
} else {
res.send(`No user has been found`);
}
});
});
},
getOverview: (req, res) => {
fetch(`https://tmi.twitch.tv/group/user/instak/chatters`)
.then(parseJSON)
.then(r => {
return res.json(r);
}).catch(err => {
console.log(err);
});
},
decrementKluiten: (req, res) => {
decrementKluiten(req.params.username, req.body.requiredKluiten);
}
}
actions.js
(Because this contains a lot of code I try to only include the relevant parts for this post, the database calls are done using Sequelize.js)
const decrementKluiten = (username, requiredKluiten) => {
return global.db.Viewer.findOne({
where: { username }
}).then(user => {
return user.decrement({ instakluiten: requiredKluiten });
});
};
module.exports = {
decrementKluiten
};
The issue is likely that you don't respond to the HTTP request in your /users/:username/decrementKluiten route. To solve this, change the exported decrementKluiten method in userController.js-file to this:
decrementKluiten: (req, res) => {
decrementKluiten(req.params.username, req.body.requiredKluiten)
.then(() => res.sendStatus(200))
.catch(() => res.sendStatus(500));
}
Some unrelated pointers to make your code a bit more readable, since you already use async functions in some parts of your code, but in other parts you interface directly with Promises.
The exported part of userController.js could utilize async functions:
module.exports = {
getUser: async (req, res) => {
try {
const username = req.params.username;
let chattersPerRole = await findChattersPerRole();
let wantedUser = find(chattersPerRole, { username });
let foundUser = await getUserByUsername(watnerUser.username);
if (foundUser) {
res.status(200).send(foundUser);
} else {
res.status(404).send('No user has been found');
}
} catch (e) {
res.sendStatus(500);
}
},
getOverview: async (req, res) => {
try {
let r = (await fetch('https://tmi.twitch.tv/group/user/instak/chatters')).json();
res.json(r);
} catch (e) {
res.sendStatus(500);
}
},
decrementKluiten: async (req, res) => {
try {
await decrementKluiten(req.params.username, req.body.requiredKluiten);
res.sendStatus(200);
} catch (e) {
res.sendStatus(500);
}
}
}
I've also added error handling in case something goes wrong, the server responds with a 500 Internal Server Error status code.
Judging by these lines in your TwitchAPI class:
const rawFoundUser = await fetch(`http://localhost:${PORT}/api/users/${userstate.username}`);
const foundUser = await rawFoundUser.json();
I assume you've tried to do const foundUser = await fetch('...').json(). This results in an error, but you can call the retuned value's methods and properties on the same line if you wrap the await expression in parentheses, like this:
const foundUser = await (await fetch('...')).json()`
If its methods does not return a Promise (i.e being synchronous), or you want to access a property, you can do:
const something = (await doSomethingAsync()).someMethod()
const somethingElse = (await doSomethingAsync()).property
I also noticed you're using template literals (backticks, `) for most strings without doing any template interpolation, which could simply be replaced with ' (single-quotes) or " (double-quotes).

Categories