Related
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({}));
});
});
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
I have this Event array but can't figure out how to query into the 'guests' nested array and do two things.
count the number of guests
count the number of attended guests (marked 'Y')
{ _id: new ObjectId('1'),
name: event1,
guests: [
{
phone: +12222222222,
_id: new ObjectId,
attended: 'Y'
},
{
phone: +12344466666,
_id: new ObjectId,
attended: 'Y'
},
{ phone: +11234567890,
_id: new ObjectId,
attended:
},
{
phone: +14443332222,
_id: new ObjectId,
attended: 'Y'
},
{ phone: +19090909090,
_id: new ObjectId
attended:
}
],
},
{ _id: new ObjectId('2'),
name: event2,
guests: [
{
phone: +11111111111,
_id: new ObjectId,
attended:
},
{
phone: +12222222222,
_id: new ObjectId,
attended: 'Y'
},
{
phone: +133333333333,
_id: new ObjectId,
attended: 'Y'
}
],
},
My code below is on its 20th iteration without getting any closer.
const event = await Event.findById(req.params.id);
For MongoDB query, you can use $size for calculating the size of the array field and $filter to filter specific document(s) that matched the criteria in the projection.
db.collection.find({
_id: "1"
},
{
_id: 1,
name: 1,
guests: 1,
totalGuests: {
$size: "$guests"
},
attendedGuests: {
$size: {
"$filter": {
input: "$guests",
cond: {
$eq: [
"$$this.attended",
"Y"
]
}
}
}
}
})
Sample Mongo Playground
Yong Shun helped above but didn't get 100% of the way -- here is the full answer in case anyone encounters something like this in the future:
module.exports.showEvent = async (req, res) => {
const event = await Event.findById(req.params.id).populate('artist');
const { guest_id } = req.cookies;
let totalGuests = 0;
let attendedGuests = 0;
const eventData = await Event.aggregate([
{
"$match": {
"_id": objectId(req.params.id)
}
},
{
$project: {
_id: 1,
name: 1,
guests: 1,
totalGuests: { $cond: { if: { $isArray: "$guests" }, then: { $size: "$guests" }, else: "NA" } },
attendedGuests: {
$size: {
$filter: {
input: "$guests",
as: "guest",
cond: {
$and: [{
$eq: ["$$guest.attended", "Y"]
}]
}
}
}
}
}
}
])
if (eventData && Array.isArray(eventData) && eventData.length > 0) {
totalGuests = eventData[0].totalGuests;
attendedGuests = eventData[0].attendedGuests;
}
if (!event) {
req.flash('error', 'Cannot find that Event');
return res.redirect('/events');
}
res.render('events/show', { event, eventData });
console.log(totalGuests, attendedGuests);
};
I'm trying to get the favorite.user Id and the user. id to match so I could get the product of the individual user added to their favorite here is what I have tried.
const product = [
{
name: "Product A",
price: "$100",
favorites: [
{
_id: "60fe705efc8be22860620d3b",
userId: "3",
username: "Alif",
createdAt: "2021-07-26T08:20:46.522Z",
},
],
},
{
name: "Product B",
price: "$300",
favorites: [
{
_id: "60fe705efc8be22860620d3b",
userId: "1",
username: "John",
createdAt: "2021-07-26T08:20:46.522Z",
},
],
},
{
name: "Product C",
price: "$1300",
favorites: [
{
_id: "60fe705efc8be22860620d3b",
userId: "1",
username: "John",
createdAt: "2021-07-26T08:20:46.522Z",
},
],
},
];
const user = {
id: "1",
};
const favoriteUser = product?.map(({ favorites }) => {
return favorites.map(({ userId }) => {
return userId;
});
});
const wishlistProduct = product?.filter(() => {
return user.id === favoriteUser;
});
console.log(wishlistProduct);
I intended it to return both the object Product B and Product C, for example, because they share the same ids. The user.id and the favorite.userId are identical. If you don't understand something, please let me know and I'll try to explain it to you.
Just use filter with proper predicate
product.filter(({ favorites }) => favorites.some(({ userId }) => userId === user.id))
const product = JSON.parse(`[{\"name\":\"Product A\",\"price\":\"$100\",\"favorites\":[{\"_id\":\"60fe705efc8be22860620d3b\",\"userId\":\"3\",\"username\":\"Alif\",\"createdAt\":\"2021-07-26T08:20:46.522Z\"}]},{\"name\":\"Product B\",\"price\":\"$300\",\"favorites\":[{\"_id\":\"60fe705efc8be22860620d3b\",\"userId\":\"1\",\"username\":\"John\",\"createdAt\":\"2021-07-26T08:20:46.522Z\"}]},{\"name\":\"Product C\",\"price\":\"$1300\",\"favorites\":[{\"_id\":\"60fe705efc8be22860620d3b\",\"userId\":\"1\",\"username\":\"John\",\"createdAt\":\"2021-07-26T08:20:46.522Z\"}]}]`);
const user = {
id: "1",
}
const result = product.filter(({ favorites }) => favorites.some(({ userId }) => userId === user.id))
console.log(result)
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
}
]
}