I am trying to put my queries into transaction and I am failing in runtime. Error I am getting is :
Object #<bound> has no method 'transaction'
I tried to follow this "documentation".
In short my model looks like that :
updateOrCreate: function (profile_id, positive,negative) {
var deferred = Q.defer();
Reputation.transaction().findOne().where({profile: profile_id}).then(function (rep) {
if (rep) {
// Reputation logic
rep.save(function (err) {deferred.resolve();});
} else {
// Reputation does not exist. Create.
Reputation.create({profile: profile_id, positive: positive,negative:negative}).exec(function (e, rep) {
deferred.resolve();});
}
}).fail(function (err) {deferred.reject()});
return deferred.promise;
}
any ideas what did I do wrong?
thanks.
w.
This is now supported in sails v1 (not official release yet at June 26, 2017).
You can follow this link for documentation in next.sailsjs.com:
Datastore.transaction()
In doc above is the following example:
sails.getDatastore()
.transaction(function (db, proceed) {
BankAccount.findOne({ owner: req.session.userId }).usingConnection(db)
.exec(function (err, myAccount) {
if (err) { return proceed(err); }
if (!myAccount) { return proceed(new Error('Consistency violation: Database is corrupted-- logged in user record has gone missing')); }
BankAccount.findOne({ owner: req.param('recipientId') }).usingConnection(db)
.exec(function (err, recipientAccount) {
if (err) { return proceed(err); }
if (!recipientAccount) {
err = new Error('There is no recipient with that id');
err.code = 'E_NO_SUCH_RECIPIENT';
return proceed(err);
}
// Do the math to subtract from the logged-in user's account balance,
// and add to the recipient's bank account balance.
var myNewBalance = myAccount.balance - req.param('amount');
// If this would put the logged-in user's account balance below zero,
// then abort. (The transaction will be rolled back automatically.)
if (myNewBalance < 0) {
err = new Error('Insufficient funds');
err.code = 'E_INSUFFICIENT_FUNDS';
return proceed(err);
}
// Update the current user's bank account
BankAccount.update({ owner: req.session.userId })
.set({
balance: myNewBalance
})
.usingConnection(db)
.exec(function (err) {
if (err) { return proceed(err); }
// Update the recipient's bank account
BankAccount.update({ owner: req.param('recipientId') })
.set({
balance: recipientAccount.balance + req.param('amount')
})
.usingConnection(db)
.exec(function (err) {
if (err) { return proceed(err); }
return proceed();
});
});
});
});
}).exec(function(err){
// At this point, we know that, if our code above passed through
// an error to `proceed`, Sails took care of rolling back the
// transaction. Otherwise, it committed it to the database.
if (err && err.code === 'E_INSUFFICIENT_FUNDS') {
return res.badRequest(err);
}
else if (err && err.code === 'E_NO_SUCH_RECIPIENT') {
return res.notFound();
}
else if (err) {
return res.serverError(err);
}
// All done!
return res.ok();
});
The "documentation" you're following is a proposal for how transaction support could be added to Sails. There is no native transaction support in Sails. See this answer for an example of how to use the .query method for the MySQL or Postgres adapters to perform transactions.
Seems they don't support this. You could use something like:
https://github.com/Shyp/pg-transactions
https://github.com/postmanlabs/sails-mysql-transactions
Related
I am having an issue with concurrent requests that are updating the same document. I'm not using findAndModify() because I need to access the current state of the document to make the update which I don't see supported with findAndModify(). I also would like to avoid using db.fsyncLock() since that locks the entire database and I only need to lock one document in one collection.
First I use findOne() to get a document, then I use the updateOne() in the callback of findOne() to update the same document. When I queue up a bunch of actions and run them all at once I believe they are all accessing the same state when they call findOne() instead of waiting for the updateOne() to complete from the previous action.
How should I handle this?
mongoDBPromise.then((db)=> {
db.collection("notes").findOne(
{path: noteId},
(err, result)=> {
if (err) {
console.log(err);
return;
}
if (!result.UndoableNoteList.future.length) {
console.log("Nothing to redo");
return;
}
let past = result.UndoableNoteList.past.concat(Object.assign({},result.UndoableNoteList.present));
let present = Object.assign({},result.UndoableNoteList.future[0]);
let future = result.UndoableNoteList.future.slice(1, result.UndoableNoteList.future.length);
db.collection("notes").updateOne(
{path: noteId},
{
$set: {
UndoableNoteList: {
past: past,
present: present,
future:future
}
}
},
(err, result)=> {
if (err) {
console.log(err);
return;
}
}
)
}
);
});
As updateOne() is an async call, findOne() won't wait for it to complete and hence there can be situations where the same document is updated simultaneously, which won't be allowed in mongo.
I think updateOne() is not necessary in this case. Note that you have already found the right instance of the document which needs to be updated in findOne() query. Now, you can update that instance and save that document without doing updateOne(). I think the problem can be avoided this way:
mongoDBPromise.then((db)=> {
db.collection("notes").findOne(
{path: noteId},
(err, result)=> {
if (err) {
console.log(err);
return;
}
if (!result.UndoableNoteList.future.length) {
console.log("Nothing to redo");
return;
}
let past = result.UndoableNoteList.past.concat(Object.assign({},result.UndoableNoteList.present));
let present = Object.assign({},result.UndoableNoteList.future[0]);
let future = result.UndoableNoteList.future.slice(1, result.UndoableNoteList.future.length);
result.UndoableNoteList.past = past;
result.UndoableNoteList.present = present;
result.UndoableNoteList.future = future;
//save the document here and return
}
);
});
Hope this answer helps you!
I was not able to find a way to sequentially run the queries using purely mongodb functions. I've written some node.js logic that blocks mongodb queries from running on the same document and adds those queries to a queue. Here's what the code currently looks like.
The Websocket Undo Listener
module.exports = (noteId, wsHelper, noteWebSocket) => {
wsHelper.addMessageListener((msg, ws)=> {
if (msg.type === "UNDO") {
noteWebSocket.broadcast(msg, noteWebSocket.getOtherClientsInPath(noteId, wsHelper));
noteWebSocket.saveUndo(noteId);
}
});
};
The saveUndo function called from the listener
saveUndo(noteId) {
this.addToActionQueue(noteId, {payload: noteId, type: "UNDO"});
this.getNoteByIdAndProcessQueue(noteId);
}
The getNoteByIdAndProcessQueue function called from saveUndo
getNoteByIdAndProcessQueue(noteId) {
if (this.isProcessing[noteId])return;
this.isProcessing[noteId] = true;
mongoDBPromise.then((db)=> {
db.collection("notes").findOne(
{path: noteId},
(err, result)=> {
if (err) {
this.isProcessing[noteId] = false;
this.getNoteByIdAndProcessQueue(noteId);
return;
}
this.processQueueForNoteId(noteId, result.UndoableNoteList);
});
});
}
The processQueueForNoteId function
processQueueForNoteId(noteId, UndoableNoteList) {
this.actionQueue[noteId].forEach((action)=> {
if (action.type === "UNDO") {
UndoableNoteList = this.undoNoteAction(UndoableNoteList);
} else if (action.type === "REDO") {
UndoableNoteList = this.redoNoteAction(UndoableNoteList);
} else if (action.type === "ADD_NOTE") {
UndoableNoteList = this.addNoteAction(UndoableNoteList, action.payload);
} else if (action.type === "REMOVE_NOTE") {
UndoableNoteList = this.removeNoteAction(UndoableNoteList, action.payload);
}
});
let actionsBeingSaved = this.actionQueue[noteId].concat();
this.actionQueue[noteId] = [];
mongoDBPromise.then((db)=> {
db.collection("notes").updateOne(
{path: noteId},
{
$set: {
UndoableNoteList: UndoableNoteList
}
},
(err, result)=> {
this.isProcessing[noteId] = false;
// If the update failed then try again
if (err) {
console.log("update error")
this.actionQueue[noteId] = actionsBeingSaved.concat(this.actionQueue[noteId]);
}
// if action were queued during save then save again
if (this.actionQueue[noteId].length) {
this.getNoteByIdAndProcessQueue(noteId);
}
}
)
});
}
I got this error: Falsy value for recipient key 'registrationTokens' while working with gcm push notifications.
Below you can find my code:
Device.find({ user: { $in: users }}, function (err, devices) {
if (err) {
logger.error('500 ' + err)
return res.status(500).json({
code: config.errorCode.status500.code,
message: config.errorCode.status500.message
})
}
var androidRegTokens = []
var iOSReqTokens = []
for (var i = 0; i < devices.length; i++) {
if (devices[i].platform == 'Android') {
androidRegTokens.push(devices[i].deviceToken)
} else {
iOSReqTokens.push(devices[i].deviceToken)
}
}
if (androidRegTokens.length > 0) {
gcmPush('Notification is sent.', androidRegTokens)
}
if (iOSReqTokens.length > 0) {
apnsPush('Notification is sent.', iOSReqTokens)
}
return res.json({ msg: 'Operation succeed.'})
})
Body of the function gcmPush('Notification is sent.', androidRegTokens) is:
this.sender.send(message, { registrationTokens: deviceTokens }, function (err, response) {
if (err) {
console.error(err)
}else {
console.log(response)
}
})
Does anyone know what wrong is with my code? At first push notifications worked perfect, than this error occured each time I call the service.
EDIT:
I have solved this thanx to Luiz Fernando. The problem is the function:
gcmPush('Notification is sent.', androidRegTokens)
I have forgot to add title which is part of the constructor:
function GcmPush (title, message, deviceTokens) {
this.sender = new gcm.Sender(config.gcmSender)
this.sendPushNotification(title, message, deviceTokens)
}
The deviceTokens object is a falsy value, it can be: null, undefined, 0, '', NaN,false.
Maybe it happend because you are filling iOSReqTokens and androidRegTokens nested in an asynchronous operation (it seems Device.find is async). So, the callback of Device.find and the for-loop happens in different times. Probably, the for-loop is happening AFTER the response, so this response will be undefined (and really is).
So, you need to use some async control flow library, such Promise or Async and ensure the correct order of operations.
Also, where you use the pushed registrations?
I am trying to find my record, update it, and save it. I can see the data updated and it can still show the updated data inside the callback of save(). However, when I go to the database, it is actually not updated:
Skills.findOne({ skillsCat: req.body.skillsCat }, (err, gets)=> {
if (err) {
res.send(err)
return
}
if (gets && gets.skillName.indexOf(req.body.skillName) !== -1) {
// Here I update my data
gets.percent[gets.skillName.indexOf(req.body.skillName)] = req.body.percent
Console.log(gets); // Here I can see the data is updated
return gets.save((err, updated)=> {
Console.log(updated); // Here I can see the data is updated
if (err) { return }
res.json({
message: 'Skill updated successfully',
data: updated
})
})
} else {
return
}
})
Is there anyone encounter similar issues before and help me out with this? Thanks.
Try below code :
gets.percent[gets.skillName.indexOf(req.body.skillName)] = req.body.percent
Console.log(gets); // Here I can see the data is updated
//Notice that 'fieldname' is the name of actual field in database
gets.markModified('fieldname');
return gets.save((err, updated)=> {
Console.log(updated); // Here I can see the data is updated
if (err) { return }
res.json({
message: 'Skill updated successfully',
data: updated
})
})
This is my code:
var thisValue = new models.Value({
id:id,
title:title //this is a unique value
});
console.log(thisValue);
thisValue.save(function(err, product, numberAffected) {
if (err) {
if (err.code === 11000) { //error for dupes
console.error('Duplicate blocked!');
models.Value.find({title:title}, function(err, docs)
{
callback(docs) //this is ugly
});
}
return;
}
console.log('Value saved:', product);
if (callback) {
callback(product);
}
});
If I detect that a duplicate is trying to be inserted, i block it. However, when that happens, i want to return the existing document. As you can see I have implemented a string of callbacks, but this is ugly and its unpredictable (ie. how do i know which callback will be called? How do i pass in the right one?). Does anyone know how to solve this problem? Any help appreciated.
While your code doesn't handle a few error cases, and uses the wrong find function, the general flow is typical giving the work you want to do.
If there are errors other than the duplicate, the callback isn't called, which likely will cause downstream issues in your NodeJs application
use findOne rather than find as there will be only one result given the key is unique. Otherwise, it will return an array.
If your callback expected the traditional error as the first argument, you could directly pass the callback to the findOne function rather than introducing an anonymous function.
You also might want to look at findOneAndUpdate eventually, depending on what your final schema and logic will be.
As mentioned, you might be able to use findOneAndUpdate, but with additional cost.
function save(id, title, callback) {
Value.findOneAndUpdate(
{id: id, title: title}, /* query */
{id: id, title: title}, /* update */
{ upsert: true}, /* create if it doesn't exist */
callback);
}
There's still a callback of course, but it will write the data again if the duplicate is found. Whether that's an issue is really dependent on use cases.
I've done a little clean-up of your code... but it's really quite simple and the callback should be clear. The callback to the function always receives either the newly saved document or the one that was matched as a duplicate. It's the responsibility of the function calling saveNewValue to check for an error and properly handle it. You'll see how I've also made certain that the callback is called regardless of type of error and is always called with the result in a consistent way.
function saveNewValue(id, title, callback) {
if (!callback) { throw new Error("callback required"); }
var thisValue = new models.Value({
id:id,
title:title //this is a unique value
});
thisValue.save(function(err, product) {
if (err) {
if (err.code === 11000) { //error for dupes
return models.Value.findOne({title:title}, callback);
}
}
callback(err, product);
});
}
Alternatively, you could use the promise pattern. This example is using when.js.
var when = require('when');
function saveNewValue(id, title) {
var deferred = when.defer();
var thisValue = new models.Value({
id:id,
title:title //this is a unique value
});
thisValue.save(function(err, product) {
if (err) {
if (err.code === 11000) { //error for dupes
return models.Value.findOne({title:title}, function(err, val) {
if (err) {
return deferred.reject(err);
}
return deferred.resolve(val);
});
}
return deferred.reject(err);
}
return deferred.resolve(product);
});
return deferred.promise;
}
saveNewValue('123', 'my title').then(function(doc) {
// success
}, function(err) {
// failure
});
I really like WiredPrairie's answer, but his promise implementation is way too complicated.
So, I decided to add my own promise implementation.
Mongoose 3.8.x
If you're using latest Mongoose 3.8.x then there is no need to use any other promise module, because since 3.8.0 model .create() method returns a promise:
function saveNewValue(id, title) {
return models.Value.create({
id:id,
title:title //this is a unique value
}).then(null, function(err) {
if (err.code === 11000) {
return models.Value.findOne({title:title}).exec()
} else {
throw err;
}
});
}
saveNewValue('123', 'my title').then(function(doc) {
// success
console.log('success', doc);
}, function(err) {
// failure
console.log('failure', err);
});
models.Value.findOne({title:title}).exec() also returns a promise, so there is no need for callbacks or any additional casting here.
And if you don't normally use promises in your code, here is callback version of it:
function saveNewValue(id, title, callback) {
models.Value.create({
id:id,
title:title //this is a unique value
}).then(null, function(err) {
if (err.code === 11000) {
return models.Value.findOne({title:title}).exec()
} else {
throw err;
}
}).onResolve(callback);
}
Previous versions of Mongoose
If you're using any Mongoose version prior to 3.8.0, then you may need some help from when module:
var when = require('when'),
nodefn = require('when/node/function');
function saveNewValue(id, title) {
var thisValue = new models.Value({
id:id,
title:title //this is a unique value
});
var promise = nodefn.call(thisValue.save.bind(thisValue));
return promise.spread(function(product, numAffected) {
return product;
}).otherwise(function(err) {
if (err.code === 11000) {
return models.Value.findOne({title:title}).exec()
} else {
throw err;
}
});
}
I'm using nodefn.call helper function to turn callback-styled .save() method into a promise. Mongoose team promised to add promises support to it in Mongoose 4.x.
Then I'm using .spread helper method to extract the first argument from .save() callback.
Here is my code :
server.get(url_prefix + '/user/:user_id/photos', function(req, res, next) {
if (!req.headers['x-session-id']) {
res.send({
status: {
error: 1,
message: "Session ID not present in request header"
}
})
} else {
User.findOne({
session_id: req.headers['x-session-id']
}, function(err, user) {
if (user) {
var user_id = req.params.user_id
Album.find({userId : user_id})
.populate('images')
.exec(function (err, albums) {
if (albums) {
albums.forEach(function(album, j) {
var album_images = album.images
album_images.forEach(function(image, i) {
Like.findOne({imageID : image._id, userIDs:user._id}, function(err,like){
if(like){
albums[j].images[i].userLike = true;
}
})
})
})
return res.send({
status: {
error: 0,
message: "Successful"
},
data: {
albums: albums
}
})
} else
return notify_error(res, "No Results", 1, 404)
})
}
else {
res.send({
status: {
error: 1,
message: "Invalid Session ID"
}
})
}
})
}
})
I am trying to add a extra value (albums[j].images[i].userLike = true;) to my images array, which is inside album array.
The problem is return res.send({ send the data before we get response from the foreach
How can I make it work, so that return should happen only after foreach has completed all the iteration
You will have to wait with invoking res.send until you fetched all the likes for all the images in each of the albums. E.g.
var pendingImageLikes = album_images.length;
album_images.forEach(function(image, i) {
Like.findOne({imageID : image._id, userIDs:user._id}, function(err,like){
if (like) {
albums[j].images[i].userLike = true;
}
if (!--pendingImageLikes) {
// we fetched all likes
res.send(
// ...
);
}
});
You might need to special case for album_images.length === 0.
Also, this does not take into account that you have multiple albums with multiple images each. You would have to delay res.send there in a very similar way to make this actually work. You might want to consider using a flow control library like first (or any other of your preference, just search for "flow control library") to make this a bit easier.
Also, you might want to consider not relying on semicolon insertion and manually type your semicolons. It prevents ambiguous expressions and makes the code easier to read.
Since you need your code to wait until all of the find operations have completed, I'd suggest you consider using the async package, and specifically something like each (reference). It makes using async loops cleaner, especially when dealing with MongoDB documents and queries. There are lots of nice features, including the ability to sequentially perform a series of functions or waterfall (when you want to perform a series, but pass the results from step to step).
> npm install async
Add to your module:
var async = require("async");
Your code would look something like this:
albums.forEach(function(album, j) {
async.each(album.images, function(album, done) {
Like.findOne({imageID: image._id, userIDs:user._id}, function(err, like){
if(!err && like){
albums[j].images[i].userLike = true;
}
done(err); // callback that this one has finished
})
})
}, function (err) { // called when all iterations have called done()
if (!err) {
return res.send({
status: {
error: 0,
message: "Successful"
},
data: {
albums: albums
}
});
}
return notify_error(res, "No Results", 1, 404);
});
});