Use findOneAndUpdate $pull and populate does not update or Populate - javascript

This is my data:
{
"_id" : ObjectId("594e762b03cc52508686ceef"),
"_members" : [
"59527bd5e521801bf07eab98",
"594ecca47a699d1775c2a2db"
],
}
I want to delete 59527bd5e521801bf07eab98 from _members.
PostSchema:
const PostsSchema = new Schema({
// ...
_members: [{ type: Schema.Types.ObjectId, ref: 'user' }],
// ...
})
UserSchema:
const UserSchema = new Schema({
// ...
posts : [{ type: Schema.Types.ObjectId, ref: 'post' }],
// ...
})
Find And Update:
let id = mongoose.mongo.ObjectID(req.params.id)
let member = mongoose.mongo.ObjectID(req.params.member)
let populateQuery = [
{path:'_members', select:'_id'}
]
Post.
findOneAndUpdate(
{'_id' : id} ,
{
$pull: { '_members': { '_id': member }}
},
{'new': true}
).
populate(populateQuery).
exec(
function (err, data) {
res.send(data)
}
);

Interestingly "population" only seems to work by specifying the model option to .populate() in this case.
Also you only need supply the member directly to $pull as the _id is not actually a property, until "populated". The "_members" array is simply an array of ObjectId values within the Post itself. So supply the input directly:
Post.findOneAndUpdate(
{'_id' : id} ,
{
$pull: { '_members': member }
},
{'new': true}
)
.populate({path:'_members', select:'_id', model: 'user' })
.exec(function(err,post) {
if (err) throw err; // or something
res.send(post)
})
Or with Promises
Post.findOneAndUpdate(
{'_id' : id} ,
{
$pull: { '_members': member }
},
{'new': true}
)
.populate({path:'_members', select:'_id', model: 'user' })
.then(post => res.send(post))
.catch(err => console.error(err)); // or something
Alternately call Model.populate() instead:
Post.findOneAndUpdate(
{'_id' : id} ,
{
$pull: { '_members': member }
},
{'new': true}
).exec(function(err,post) {
if (err) throw err; // or something
Post.populate(post, {path:'_members', select:'_id', model: 'user' },function(err,post) {
if (err) throw err; // or something
res.send(post); // now populated
}
})
Or alternately using Promises:
Post.findOneAndUpdate(
{'_id' : id} ,
{
$pull: { '_members': member }
},
{'new': true}
)
.then(post => Post.populate(post, {path:'_members', select:'_id', model: 'user' }))
.then(post => res.send(post)) // also populated
.catch(err => console.error(err)) // or whichever
Somewhat unsure about why you want to call .populate() when you are only asking to return the _id field which is already embedded in the document, but that's another case. With the model option the population actually takes place here.
As a self contained demonstration:
const async = require('async'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
mongoose.Promise = global.Promise;
mongoose.set('debug',true);
mongoose.connect('mongodb://localhost/test');
const relSchema = new Schema({ c: String });
const testSchema = new Schema({
a: String,
b: [{ type: Schema.Types.ObjectId, rel: 'Rel' }]
})
const Test = mongoose.model('Test', testSchema);
const Rel = mongoose.model('Rel', relSchema);
function log(data) {
console.log(JSON.stringify(data, undefined, 2))
}
async.series(
[
(callback) =>
async.each(mongoose.models,(model,callback) =>
model.remove({},callback),callback),
(callback) =>
async.waterfall(
[
(callback) => Rel.create([{ c: 2 },{ c: 3 },{ c: 4 }],callback),
(rels,callback) => Test.create({ a: 1, b: rels },(err,test) => {
if (err) callback(err);
log(test);
callback(err,test.b.slice(1,3))
}),
(rels,calback) =>
Test.findOneAndUpdate(
{ 'a': 1 },
{ '$pull': { 'b': rels[0] } },
{ 'new': true }
)
.populate({ path: 'b', model: 'Rel' })
.exec((err,test) => {
if (err) callback(err);
log(test);
callback(err);
})
],
callback
)
],
(err) => {
if (err) throw err;
mongoose.disconnect();
}
)
And the output:
Mongoose: tests.remove({}, {})
Mongoose: rels.remove({}, {})
Mongoose: rels.insert({ c: '2', _id: ObjectId("595714579afd8860e56d2ec7"), __v: 0 })
Mongoose: rels.insert({ c: '3', _id: ObjectId("595714579afd8860e56d2ec8"), __v: 0 })
Mongoose: rels.insert({ c: '4', _id: ObjectId("595714579afd8860e56d2ec9"), __v: 0 })
Mongoose: tests.insert({ a: '1', _id: ObjectId("595714579afd8860e56d2eca"), b: [ ObjectId("595714579afd8860e56d2ec7"), ObjectId("595714579afd8860e56d2ec8"), ObjectId("595714579afd8860e56d2ec9") ], __v: 0 })
{
"__v": 0,
"a": "1",
"_id": "595714579afd8860e56d2eca",
"b": [
"595714579afd8860e56d2ec7",
"595714579afd8860e56d2ec8",
"595714579afd8860e56d2ec9"
]
}
Mongoose: tests.findAndModify({ a: '1' }, [], { '$pull': { b: ObjectId("595714579afd8860e56d2ec8") } }, { new: true, upsert: false, remove: false, fields: {} })
Mongoose: rels.find({ _id: { '$in': [ ObjectId("595714579afd8860e56d2ec7"), ObjectId("595714579afd8860e56d2ec9") ] } }, { fields: {} })
{
"_id": "595714579afd8860e56d2eca",
"a": "1",
"__v": 0,
"b": [
{
"_id": "595714579afd8860e56d2ec7",
"c": "2",
"__v": 0
},
{
"_id": "595714579afd8860e56d2ec9",
"c": "4",
"__v": 0
}
]
}

Related

Write Conflict mongoose transactions

In my application i am trying to build a route for creating orders the flow should go like this:
1- reserve the products for the user
2- empty the user's cart
3- take payment
however if one operation fails the database should return to its original state before updating any documents
I'm using mongodb
this is what i tried:
export default async function order(userId) {
const User = mongoose.model('user');
const Product = mongoose.model('product');
const Order = mongoose.model('order');
const session = await mongoose.startSession();
session.startTransaction();
try {
const user = await User.findById(userId).session(session);
// reserve the product for the user
const results = await Product.bulkWrite(
user.cart.map((item) => ({
updateOne: {
filter: {
_id: item.product,
'combinations.size': item.size,
'combinations.color': item.color,
'combinations.qty': { $gte: item.qty },
},
update: {
$inc: { 'combinations.$.qty': -item.qty },
},
},
})),
{ session }
);
if (results.nModified !== user.cart.length) {
throw new Error('Not all products available, transaction aborted');
}
// empty user's cart
await User.findByIdAndUpdate(userId, { cart: [] }).session(session);
// take payment from the user
const payment = (successed) => {
if (successed) return { id: 'charge id' };
throw new Error('payment failed');
};
const charge = payment(true);
const productsInCart = await Product.find({
_id: { $in: user.cart.map((item) => item.product.toString()) },
});
// create the order
await Order.create([
{
user: userId,
products: user.cart.map((item) => ({
...item,
price: productsInCart.find(
(product) => product._id.toString() === item.product.toString()
).price.curr1,
})),
currency: 'curr1',
status: 'placed',
chargeId: charge.id,
},
]);
await session.commitTransaction();
} catch (err) {
console.log(err);
await session.abortTransaction();
} finally {
await session.endSession();
}
}
however when testing it i get this error
these are my models:
User:
import { Schema, model } from 'mongoose';
const UserSchema = new Schema({
email: String,
cart: [
{
product: { type: Schema.Types.ObjectId, ref: 'product' },
color: String,
size: String,
qty: Number,
},
],
orders: [{ type: Schema.Types.ObjectId, ref: 'order' }],
});
model('user', UserSchema);
Product
import { Schema, model } from 'mongoose';
const ProductSchema = new Schema({
title: String,
price: {
curr1: Number, // price in a region
curr2: Number, // price in another region
},
combinations: [
{
size: String,
color: String,
qty: Number,
},
],
});
model('product', ProductSchema);
Order:
import { Schema, model } from 'mongoose';
const OrderSchema = new Schema({
user: { type: Schema.Types.ObjectId, ref: 'user' },
products: [
{
product: { type: Schema.Types.ObjectId, ref: 'product' },
size: String,
color: String,
qty: Number,
price: Number,
},
],
currency: String,
status: String,
chargeId: String,
});
model('order', OrderSchema);
and my test:
import async from 'async';
import mongoose from 'mongoose';
import order from '../src/order.js';
const User = mongoose.model('user');
const Order = mongoose.model('order');
const Product = mongoose.model('product');
describe('Race condition for orders', async () => {
let products;
let users;
beforeEach(async () => {
products = await Product.insertMany([
{
title: 'test product 1',
price: { curr1: 10, curr2: 10 },
combinations: [
{
size: 'large',
color: 'black',
qty: 1,
},
{
size: 'small',
color: 'red',
qty: 2,
},
],
},
{
title: 'test product 2',
price: { curr1: 10, curr2: 10 },
combinations: [
{
size: 'medium',
color: 'yellow',
qty: 1,
},
],
},
]);
users = await User.insertMany([
{
email: 'test#test.test',
cart: products.map((product) => ({
product: product._id.toString(),
size: product.combinations[0].size,
color: product.combinations[0].color,
qty: 1,
})),
},
{
email: 'test2#test.test',
cart: products.map((product) => ({
product: product._id.toString(),
size: product.combinations[0].size,
color: product.combinations[0].color,
qty: 1,
})),
},
]);
});
it('calls order twice at the same time', async () => {
await new Promise((resolve, reject) =>
async.parallel(
[
async () => {
await order(users[0]._id.toString());
},
async () => {
await order(users[1]._id.toString());
},
],
(err, res) => {
if (err) return reject(err);
resolve(res);
}
)
);
console.log(await Order.find({}));
});
});

Add field on mongoose find

I have a mongoose find like this
Persona.find(query)
.sort({ order:1 }).exec(async function (err, data) {
if (err) {
return res.status(400).send(err);
}
let dataform = [];
await asyncForEach(data, async persona => {
persona.newField = "Test";
dataform.push(persona);
}
});
res.json(dataform);
});
With the debugger the value of dataform on res.json(dataform); is this
0:{
_id: 123,
name: 'persona1',
newField: 'Test'
},
1:{
_id: 1234,
name: 'persona2',
newField: 'Test'
}
But when my controller gets the api response, the "newField" doesn't exist
0:{
_id: 123,
name: 'persona1'
},
1:{
_id: 1234,
name: 'persona2'
}

MongoDB - Remove object from SubArray

I'm trying to remove an object from a subarray with no luck getting updateOne() is not a function and remove() is not function.
I want to remove the 'subcat 1' object with id of '61cae5daf5bfbebd7cf748ef':
[
{
_id: '61cae5daf5bfbebd7cf748ee'
title: 'category 1',
SubCats: [
{
_id: '61cae5daf5bfbebd7cf748ef'
name: 'subcat 1',
image: '/assets/images/vr-box-6203301_1920.jpg',
},
{
_id: '61cae5daf5bfbebd7cf748fb'
name: 'subcat 2',
image: '/assets/images/galaxy-s20_highlights_kv_00.jpg',
},
]
},
]
Please help
Controller:
const deleteSubCategory = asyncHandler(async (req, res) => {
const subCategory = await Category.aggregate([
{ $unwind: "$SubCats" },
{ $replaceRoot: { newRoot: '$SubCats'} },
{ $match: { _id: ObjectId(req.params.id) }}
])
if (subCategory) {
await subCategory.updateOne({ $pull: {_id: ObjectId(req.params.id)}})
res.json({ message: 'sub-category removed' })
} else {
res.status(404)
throw new Error('sub-Category not found')
}
})
$update with $pull
db.collection.update({
"SubCats._id": "61cae5daf5bfbebd7cf748ef"
},
{
"$pull": {
SubCats: {
_id: "61cae5daf5bfbebd7cf748ef"
}
}
},
{
"multi": true
})
mongoplayground

Update in function of existing or using mongo operator in updateOne mongoose method

Situation :
I have a like button and I wish that when a user clicks on like the like in the database:
Increment if the user didn't yet like it (like with +1 and add user id from the likedBy array)
Decrease if the user already liked it (like - 1 and remove the used id from the likedBy array)
Code:
the controller :
exports.likeIdea = (req,res,next) => {
const userId = getUserId(req)
Ideas.updateOne({ _id: req.params.id}, {
$set: {
like: {
$cond: [ {$in: [userId, "$likedBy"]}, { $inc: { like: +1 } } , { $inc: { like: -1 } } ]
},
likedBy: {
$cond: [ {$in: [userId, "$likedBy"]}, { $pull: { likedBy: userId } } , { $push: { likedBy: userId } } ]
},
_id: req.params.id
}
})
.then(() => res.status(200).json({ message: 'Success'}))
.catch(error => {
res.status(400).json({ error })
});
};
the schema
const ideaSchema = mongoose.Schema({
name: { type: String, required: true},
sumup: { type: String, required: true },
description: { type: String, required: true},
published: {type: Boolean, required: true},
like: {type: Number, required: true},
likedBy: {type: [String]},
author: {type: String, required: true},
dislike: {type: Number, required: true},
dislikedBy: {type: [String]},
imgUrl: {type: String, required: true}
});
the error :
CastError: Cast to Number failed for value "{ '$cond': [ { '$in': [Array] }, { '$inc': [Object] }, { '$inc': [Object] } ] }" at path
"like" [...] {messageFormat: undefined, stringValue: '"{
'$cond': [ { '$in': [Array] }, { '$inc': [Object] }, { '$inc':
[Object] } ] }"', kind: 'Number', value: {…}, path: 'like', …}
The regular update query can not allow to use internal fields and aggregation operators like $cond, so you can't do this operation with regular update query,
You can try with update with aggregation pipeline starting from MongoDB 4.2,
instead of $inc you can use $add operator in aggregation update
instead of $pull you can use $filter to remove specific user
instead of $push you can use $concatArrays operator
exports.likeIdea = (req,res,next) => {
const userId = getUserId(req)
Ideas.updateOne({ _id: req.params.id},
[{
$set: {
like: {
$cond: [
{ $in: [userId, "$likedBy"] },
{ $add: ["$like", 1] },
{ $add: ["$like", -1] }
]
},
likedBy: {
$cond: [
{ $in: [userId, "$likedBy"] },
{
$filter: {
input: "$likedBy",
cond: { $ne: ["$$this", userId] }
}
},
{ $concatArrays: ["$likedBy", [userId]] }
]
}
}
}]
).then(() => res.status(200).json({ message: 'Success'}))
.catch(error => {
res.status(400).json({ error })
});
};
Playground

Mongoose - .populate issue when populating array type nested object

I am trying to populate a object myObj. After saving values to 'a' object, I am able to see the data for 'myObj' in the db. But when I query for the obj 'a', its not pulling the 'myObj' data. My Schema is as below.
var a= new Schema({
b: 'Date',
c: {
d: {
type: Schema.ObjectId,
ref: 'Student'
},
myObj: {
type: [{
type: Schema.ObjectId,
ref: 'Exam'
}],
select: true
},
}
});
Exam Schema
var ExamSchema= new Schema({
name: String,
subject: {
type: [{
name: String,
marks: Number
}],
select: false
}
});
My querying method:
exports.a = function(req, res, next, id) {
A //model of 'a'
.findOne({
_id: id
})
.populate('c.d c.myObj')
.exec(function(err, aObj) {
if (err) return next(err);
if (!aObj) return next(new Error('Failed to load ' + id));
req.a = aObj;
next();
});
};
Expected Output:
var a = new Schema({
b: "2014-07-10T02:30:00.005Z",
,
c: {
d: {
name: 'Hari',
age: 10
},
myObj: {
[{
name: 'Quaterly',
subject: {
[{
name: 'English',
marks: 100
}, {
name: 'Maths',
marks: 90
}, {
name: 'Science',
marks: 100
}],
select: false
}
}, {
name: 'Half-Yearly',
subject: {
[{
name: 'English',
marks: 100
}, {
name: 'Maths',
marks: 90
}, {
name: 'Science',
marks: 100
}],
select: false
}
}],
select: true
},
}
});
**Actual Output:**
var a = new Schema({
b: "2014-07-10T02:30:00.005Z",
,
c: {
d: {
name: 'Hari',
age: 10
},
myObj: []
}
});
Please let me know where I am wrong. Thanks.
This should be working, here is a cut down sample to test and compare and see what you might actually be doing differently:
var async = require('async'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
mongoose.connect('mongodb://localhost/throw');
var oneSchema = new Schema({
name: String
});
var twoSchema = new Schema({
name: String
});
var testSchema = new Schema({
name: String,
c: {
d: { type: Schema.Types.ObjectId, ref: "One" },
e: [{ type: Schema.Types.ObjectId, ref: "Two" }]
}
});
var Test = mongoose.model( "Test", testSchema, "test" );
var One = mongoose.model( "One", oneSchema, 'one' );
var Two = mongoose.model( "Two", twoSchema, 'two' );
var test = new Test({ name: "test" });
var one = new One({ name: "one" });
var two = new Two({ name: "two" });
var three = new Two({ name: "three" });
test.c.d = one;
test.c.e.push(two,three);
async.series([
// Remove prior
function(callback) {
async.each([Test,One,Two],function(model,complete) {
model.remove(function(err) {
if (err) throw err;
complete();
});
},function(err) {
if (err) throw err;
callback();
});
},
// Save new
function(callback) {
async.each([test,one,two,three],function(model,complete) {
model.save(function(err) {
if (err) throw err;
complete();
});
},function(err) {
if (err) throw err;
callback();
});
},
// Unpopulated
function(callback) {
Test.findOne()
.exec(function(err,obj) {
if (err) throw err;
console.log( "Before: %s", JSON.stringify( obj, undefined, 4 ) );
callback();
});
},
// Populated
function(callback) {
Test.findOne()
.populate("c.d c.e")
.exec(function(err,obj) {
if (err) throw err;
console.log( "After: %s", JSON.stringify( obj, undefined, 4 ) );
callback();
});
}
],function(err) {
if (err) throw err;
process.exit();
});
Which should produce results along the lines of:
Before: {
"_id": "53be11986c64035664e4d73a",
"name": "test",
"__v": 0,
"c": {
"d": "53be11986c64035664e4d73b",
"e": [
"53be11986c64035664e4d73c",
"53be11986c64035664e4d73d"
]
}
}
After: {
"_id": "53be11986c64035664e4d73a",
"name": "test",
"__v": 0,
"c": {
"d": {
"_id": "53be11986c64035664e4d73b",
"name": "one",
"__v": 0
},
"e": [
{
"_id": "53be11986c64035664e4d73c",
"name": "two",
"__v": 0
},
{
"_id": "53be11986c64035664e4d73d",
"name": "three",
"__v": 0
}
]
}
}

Categories

Resources