How to know mongodb transaction all CRUD success? - mongodb

execTransaction(session => {
return async () => {
await db.collection('table1').updateOne({ username: username }, { $push: { apps: client_id } }, { session });
await db.collection('table2').insertOne({ data: data }, { session });
};
}, data => {
console.log(data); //data.result.ok = 1
});
I use withTransaction in nodejs to run transaction.
When I run the transaction correctly, data.result.ok = 1
But when I use no exist username run updateOne, the result is still 1.
How to verify the result?

insertOne should return an Object like this if successful
{
acknowledged: true,
insertId: <object OjectId> // or the _id you specified
}
updateOne should return an Object like this if successful
{
acknowledged: true,
modifiedCount: 1, // or 0 if upserted
upsertedId: null, // or an <object OjectId>
upsertedCount: 0, // or 1 if upserted
matchedCount: 1 // or 0 if upserted
}
Now, updateOne only updates the first document that matched the query. If no document matched the query, it would simply update no document and throw no errors. You would get an object like this if that happens,
{
acknowledged: true,
modifiedCount: 0,
upsertedId: null,
upsertedCount: 0,
matchedCount: 0
}
So, checking data.result.ok would tell you nothing in that scenario. You have to manually check each method call if you want to something like that. In your case, maybe do something like this?
execTransaction(session => {
return async () => {
await Promise.all([
db.collection('table1').updateOne({ username: username }, { $push: { apps: client_id } }, { session }),
db.collection('table2').insertOne({ data: data }, { session })
])
};
}, data => {
if (data[0].matchedCount === 0) {
// do error handling ?
}
}
)
Or, if you want to know if all the CRUD operations succeeded as the title suggests, maybe this?
(await Promise.all([a list of all insertOne and updateOne operations... ])
.every(obj => obj && obj.acknowledged && (obj.insertId || obj.modifiedCount || obj.upsertedCount))
// this checks if all the properties are non zero
// thus if It's true, everything succeed
But again I would suggest you to do error handling separately for each operation when needed.

Related

Mongoose: findByIdAndUpdate() How To Conditionally Update Based On Existing Data?

When using findByIdAndUpdate() or findOneAndUpdate() (that is, updating the database atomically to avoid race conditions), is there a way to reference the existing data to conditionally update a field?
PersonModel.findByIdAndUpdate(
req.user.id,
{
$set: {
// set this to true
// if person.color === blue for example
hasFavoriteColor: true,
}
},
{ new: true },
(err, updatedDoc => {
if (err) return;
return updatedDoc;
});
);
I'm not sure you can do that, I see nowhere in the document. However, for your query particularly, you can add the condition about the color in the filter part, something like this :
PersonModel.findOneAndUpdate(
{_id : req.user.id, color : "blue"},
{
$set: {
// set this to true
// if person.color === blue for example
hasFavoriteColor: true,
}
},
{ new: true },
(err, updatedDoc => {
if (err) return;
return updatedDoc;
});
);

how to refactor mongoose document update code to return updated item instead of status

I was wondering how I might be refactor the following to return the updated document.
const updated = await item.update(
{ name: data.name, $inc: { seq: 1 } },
{ new: true }
);
It's updating but returning this instead(ie value of updated):
{ n: 1, nModified: 1, ok: 1 }
I also just read that using save is recommended, but I can't find a way to add the incrementing logic with save outside of a pre save hook. Thanks.
The update() method returns a WriteResult document that contains the status of the operation.
Just try findOneAndUpdate(), new: true will return updated document,
const updated = await item.findOneAndUpdate(
{ name: data.name },
{ $inc: { seq: 1 } },
{ new: true }
);

Mongoose: consistently save document order

Suppose we have a schema like this:
const PageSchema = new mongoose.Schema({
content: String
order: Number
})
We want order to be always a unique number between 0 and n-1, where n is the total number of documents.
How can we ensure this when documents are inserted or deleted?
For inserts I currently use this hook:
PageSchema.pre('save', async function () {
if (!this.order) {
const lastPage = await this.constructor.findOne().sort({ order: -1 })
this.order = lastPage ? lastPage.order + 1 : 0
}
})
This seems to work when new documents are inserted.
When documents are removed, I would have to decrease the order of documents of higher order. However, I am not sure which hooks are called when documents are removed.
Efficiency is not an issue for me: there are not many inserts and deletes.
It would be totally ok if I could somehow just provide one function, say fix_order, that iterates over the whole collection. How can I install this function such that it gets called whenever documents are inserted or deleted?
You can use findOneAndDelete pre and post hooks to accomplish this.
As you see in the pre findOneAndDelete hook, we save a reference to the deleted document, and pass it to the postfindOneAndDelete, so that we can access the model using constructor, and use the updateMany method to be able to adjust orders.
PageSchema.pre("findOneAndDelete", async function(next) {
this.page = await this.findOne();
next();
});
PageSchema.post("findOneAndDelete", async function(doc, next) {
console.log(doc);
const result = await this.page.constructor.updateMany(
{ order: { $gt: doc.order } },
{
$inc: {
order: -1
}
}
);
console.log(result);
next();
});
Let's say you have these 3 documents:
[
{
"_id": ObjectId("5e830a6d0dec1443e82ad281"),
"content": "content1",
"order": 0,
"__v": 0
},
{
"_id": ObjectId("5e830a6d0dec1443e82ad282"),
"content": "content2",
"order": 1,
"__v": 0
},
{
"_id": ObjectId("5e830a6d0dec1443e82ad283"),
"content": "content3",
"order": 2,
"__v": 0
}
]
When you delete the content2 with "_id": ObjectId("5e830a6d0dec1443e82ad282") with findOneAndDelete method like this:
router.delete("/pages/:id", async (req, res) => {
const result = await Page.findOneAndDelete({ _id: req.params.id });
res.send(result);
});
The middlewares will run, and adjust the orders, the remaining 2 documents will look like this:
[
{
"_id": ObjectId("5e830a6d0dec1443e82ad281"),
"content": "content1",
"order": 0,
"__v": 0
},
{
"_id": ObjectId("5e830a6d0dec1443e82ad283"),
"content": "content3",
"order": 1, => DECREASED FROM 2 to 1
"__v": 0
}
]
Also you had better to include next in your pre save middleware so that other middlewares also work if you add later.
PageSchema.pre("save", async function(next) {
if (!this.order) {
const lastPage = await this.constructor.findOne().sort({ order: -1 });
this.order = lastPage ? lastPage.order + 1 : 0;
}
next();
});
Based on the answer of SuleymanSah, I wrote a mongoose plugin that does the job. This way, it can be applied to multiple schemas without unnessecary code duplication.
It has two optional arguments:
path: pathname where the ordinal number is to be stored (defaults to order)
scope: pathname or array of pathnames relative to which numbers should be given (defaults to [])
Example. Chapters should not be numbered globally, but relative to the book to which they belong:
ChapterSchema.plugin(orderPlugin, { path: 'chapterNumber', scope: 'book' })
File orderPlugin.js:
function getConditions(doc, scope) {
return Object.fromEntries([].concat(scope).map((path) => [path, doc[path]]))
}
export default (schema, options) => {
const path = (options && options.path) || 'order'
const scope = (options && options.scope) || {}
schema.add({
[path]: Number,
})
schema.pre('save', async function () {
if (!this[path]) {
const last = await this.constructor
.findOne(getConditions(this, scope))
.sort({ [path]: -1 })
this[path] = last ? last[path] + 1 : 0
}
})
schema.post('findOneAndDelete', async function (doc) {
await this.model.updateMany(
{ [path]: { $gt: doc[path] }, ...getConditions(doc, scope) },
{ $inc: { [path]: -1 } }
)
})
}

Using mongoose lean after saving

So I am trying to add a key to a returned post. But I can't seem to get lean() to work. How can I manipulate the returned post after save?
I was thinking maybe I need to add lean to my findById like this Post.findById(req.params.id).lean().then(). But that didn't work, plus that only makes the first initial post mutable. It will say
post.save is not a function
if I do it like Post.findById(req.params.id).lean().then() as well
I want to only return the object about to be sent back to the client, I do not want they key saved in the actual document.
Post.findById(req.params.id)
.then(post => {
if (
post.likes.filter(like => like.user.toString() === req.user.id)
.length === 0
) {
return res
.status(400)
.json({ notliked: "You have not yet liked this post" });
}
// Get remove index
const removeIndex = post.likes
.map(item => item.user.toString())
.indexOf(req.user.id);
// Splice out of array
post.likes.splice(removeIndex, 1);
// Save
post.save().then(post => {
post["liked"] = false; <-------
res.json(post);
});
})
edit
Post.findById(req.params.id)
.lean()
.then(post => {
if (
post.likes.filter(like => like.user.toString() === req.user.id)
.length === 0
) {
return res
.status(400)
.json({ notliked: "You have not yet liked this post" });
}
// Get remove index
const removeIndex = post.likes
.map(item => item.user.toString())
.indexOf(req.user.id);
// Splice out of array
post.likes.splice(removeIndex, 1);
post["liked"] = false;
res.json(post);
// Save
post.save();
})
gives error
post.save is not a function
You can simply do this by searching for the req.user.id inside the indexOf likes array
Post.findOne({ _id: req.params.id }).lean().then((post) => {
if (post.likes.indexOf(req.user.id) !== -1) {
post.isLiked = true
}
post.isLiked = false
res.json(post)
})
Far better with the aggregation
Post.aggregate([
{ "$match": { "_id": mongoose.Types.ObjectId(req.user.id) }},
{ "$addFields": {
"isLiked": { "$in": [mongoose.Types.ObjectId(req.user.id), "$likes"] }
}}
])
EDIT :- If you want to update document then use update query
Post.findOneAndUpdate(
{ _id: req.params.id },
{ $pull: { likes: { user: req.user.id } }},
{ new: true }
).then((post) => {
res.json(post)
})
Post Schema for likes
...
likes: [
{
user: {
type: Schema.Types.ObjectId,
ref: "users"
}
}
]
...

How to indicate an update with findAndModify method in MongoDB?

I'm working on an app using MongoDB and Express.js.
I am creating a post handler that updates a toy (found by its id) with a new proposed name for the toy (which is pushed onto a nameIds array that contains the ids of the other proposed names):
router.post('/names', (req, res) => {
const toyId = req.body.toyId;
const name = req.body.newName;
mdb.collection('names').insertOne({ name }).then(result =>
mdb.collection('toys').findAndModify({
query: { id: toyId },
update: { $push: { nameIds: result.insertedId } },
new: true
}).then(doc =>
res.send({
updatedToy: doc.value,
newName: { id: result.insertedId, name }
})
)
)
});
However, when I test this, I receive this error:
name: 'MongoError',
message: 'Either an update or remove=true must be specified',
ok: 0,
errmsg: 'Either an update or remove=true must be specified',
code: 9,
codeName: 'FailedToParse'
I'm not new to MongoDB, but this simple call is baffling me.
Thanks for any help you can provide!
That is the format for mongo shell. Using mongo driver you would call with these arguments:
.findAndModify( //query, sort, doc, options, callback
{ id: toyId }, //query
[], //sort
{ $push: { nameIds: result.insertedId } }, // doc update
{ new: true }, // options
function(err,result){ //callback
if (err) {
throw err
} else {
res.send({
updatedToy: result.value,
newName: { id: result.insertedId, name }
})
}
}
)