Related
I have a MongoDB Model which consist of array of members as obejcts.
const guestSchema = new mongoose.Schema({
salutation: {
type: String,
},
members: [membersSchema],
user: {
type: mongoose.Schema.ObjectId,
ref: 'User',
},
});
Members Schema:
const membersSchema = new mongoose.Schema({
name: String,
status: {
type: String,
enum: ['regular', 'helper'],
default: 'regular',
},
});
I want to achieve of doing an update in case documet with given ID exist or push to an array in case ID with document in array does not exist. I use aggregation pipeline, however I am not able to achieve pushing new document to array. Why can't I use push after else statement like this.
const subDocumentToUpsert = { 'name': mem.name, 'status': mem.status, '_id': ObjectId(mem.id)}
const subDocumentNoID = { 'name': mem.name, 'status': mem.status}
await Guest.findOneAndUpdate(
{ "_id": req.params.id },
[
{
$set: {
members: {
$cond: {
if: { $in: [subDocumentToUpsert._id, '$members._id'] },
then: {
$map: {
input: '$members',
as: 'sub_document',
in: {
$cond: {
if: { $eq: ['$$sub_document._id', subDocumentToUpsert._id] },
then: subDocumentToUpsert,
else: '$$sub_document',
},
},
},
},
else: {
$push: {
subDocumentNoID
},
},
},
},
},
},
},
]);
What is the best way of doing so? Thank you
You can do as follow:
db.collection.update({
_id: {
$in: [
1,
2
]
}
},
[
{
$set: {
members: {
$cond: {
if: {
$in: [
5,
"$members._id"
]
},
then: {
$map: {
input: "$members",
as: "sub",
in: {
$cond: {
if: {
$eq: [
"$$sub._id",
5
]
},
then: {
_id: 5,
status: "regular_updated",
name: "Negan_updated"
},
else: "$$sub"
},
},
},
},
else: {
$concatArrays: [
"$members",
[
{
_id: 5,
status: "regular_upserted",
name: "Negan_upserted"
}
]
]
}
}
}
}
}
}
],
{
multi: true
})
Explained:
Check if _id:5 exist in the subobject and update via $map/$cond only the object that has the _id:5.
In case there is no _id:5 add the new object to the array with $concatArrays.
Playground
With MongoDB aggregate, how do you lookup child documents from an array on the parent document and match a field on the child to a value?
I want to lookup the ShopItems for a Shop. And I only want the ShopItems that are not kidFriendly.
Details:
Let's say I have Shops collection
Shop Schema
{
shopItems: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'ShopItem',
},
],
}
And a shop can have multiple Shop Items:
ShopItem Schema
[
itemName: { type: String },
kidFriendly: { type: Boolean, default: false },
]
I want to lookup the ShopItems for a Shop. And I only want the ShopItems that are not kidFriendly.
I tried
const result3 = await Shop.aggregate([
{
$match: {
_id: mongoose.Types.ObjectId('623ae52ba5b1af0004e1c4ec'),
},
},
{
$lookup: {
from: ShopItem.collection.collectionName,
pipeline: [
{
$match: {
kidFriendly: { $ne: true },
_id: { $in: '$shopItems' },
},
},
],
as: 'adultOnlyItems',
},
},
]);
but I get an error:
$in needs an array.
Example data
// shops
[
{
_id: ObjectId('623ae52ba5b1af0004e1c4ec'),
shopItems: [ObjectId('631e6b133b688a0004a17265'), ObjectId('62f4cc974a255f00044c01b5'), ObjectId('625ffc48eec7b20004c9294c')]
},
{
_id: ObjectId('623ae52ba5b1af0004e1c4eb'),
shopItems: [ObjectId('631e6b133b688a0004a17263')]
}
]
//shop items
[
{
_id: ObjectId('631e6b133b688a0004a17265'),
itemName: "Barbie",
kidFriendly: true
},
{
_id: ObjectId('62f4cc974a255f00044c01b5'),
itemName: "Alcohol",
kidFriendly: false
},
{
_id: ObjectId('625ffc48eec7b20004c9294c'),
itemName: "Glass Vase",
kidFriendly: false
},
{
_id: ObjectId('631e6b133b688a0004a17263'),
itemName: "Beach Ball",
kidFriendly: true
}
]
When you use $lookup with pipeline, you need to have the let to store the field value from shops into variable. And use the $expr operator as well.
db.shops.aggregate([
{
$match: {
_id: mongoose.Types.ObjectId('623ae52ba5b1af0004e1c4ec')
}
},
{
$lookup: {
from: ShopItem.collection.collectionName,
let: {
shopItems: "$shopItems"
},
pipeline: [
{
$match: {
$expr: {
$and: [
{
$ne: [
"$kidFriendly",
true
]
},
{
$in: [
"$_id",
"$$shopItems"
]
}
]
}
}
}
],
as: "adultOnlyItems"
}
}
])
Demo # Mongo Playground
Reference
Join Conditions and Subqueries on a Joined Collection
I have an array of mogoDB embedded documents
const ArticleSchema = new mongoose.Schema({
title: { type: String, required: true },
author: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true },
group: { type: mongoose.Schema.Types.ObjectId, ref: "ArticleGroup" },
});
// ArticleGroup
const ArticleGroupSchema = new mongoose.Schema({
name: { type: String, required: true },
desc: { type: String, default: "" },
state: { type: String, required: true, default: "public" },
members: [
{
desc: String,
role: String,
user: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true },
},
],
});
// User
const userSchema = new mongoose.Schema(
{
email: { type: String, unique: true },
password: String,
},
);
test data :
{
_id: "6131aa5b367318e2df14b988",
title: "功能更新清单",
author: ObjectId("607edeb4b1e1bea9acb5af38"),
group: ObjectId("612d00a43c52975d4ade10d4")
}
I want to add a new field 'can_edit' to the document by comparing 'author._id == group.members. User' in 'aggregate' pipeline.
Article.aggregate([
{
$lookup: {
from: "users",
localField: "author",
foreignField: "_id",
as: "author",
},
},
{
$lookup: {
from: "articlegroups",
localField: "group",
foreignField: "_id",
as: "group"
}
},
{
$addFields: {
can_edit: {
$eq: ["$group.members.user", "$author._id"],
},
}
}
])
However, the 'can_edit' I got was always' false ', and I'm sure that the 'author._id' value in my test data is the same as' group.members.
I want to get the data:
{
_id: "6131aa5b367318e2df14b988",
can_edit: true,
title: "功能更新清单",
author: {
_id: "607edeb4b1e1bea9acb5af38",
email: "frmachao#126.com",
},
group: {
_id: "612d00a43c52975d4ade10d4",
desc: "开发",
state: "public",
name: "开发小组",
members: [
{
_id: "612d00a43c52975d4ade10d5",
role: "admin",
user: "607edeb4b1e1bea9acb5af38",
}
]
}
}
Is there any way I can meet my needs? Does anyone know how to do that? I would grateful to you 🙏.
Just now, I try :
Article.aggregate([
{
$lookup: {
from: "users",
localField: "author",
foreignField: "_id",
as: "author",
},
},
{
$lookup: {
from: "articlegroups",
localField: "group",
foreignField: "_id",
as: "group"
}
},
{
$addFields: {
can_edit: {
$in: [
"$author._id",
"$group.members.user"
],
},
},
}
])
However, the 'can_edit' I got was always' false '
With $in (aggregation),
Returns a boolean indicating whether a specified value is in an array.
$in: [
"$author._id",
"$group.members.user"
]
Updated:
Since Post Owner added the issue:
$in requires an array as a second argument
which means $group.members potentially is null and not an array.
Add condition checking for checking $group.members must be an array.
{
case: {
$ne: [
{
"$type": "$group.members"
},
"array"
]
},
then: false
},
$ne - Not equal check
$type - Return the BSON type of field
Complete query:
db.collection.aggregate([
{
$addFields: {
can_edit: {
$switch: {
branches: [
{
case: {
$ne: [
{
"$type": "$group.members"
},
"array"
]
},
then: false
},
{
case: {
$ne: [
{
"$type": "$group.members"
},
"array"
]
},
then: false
},
{
case: {
$in: [
"$author._id",
"$group.members.user"
]
},
then: true
}
],
default: false
}
}
}
}
])
Sample 1 in Mongo playground
Alternatively, the query can be shortened with $switch not needed as $in returns the same boolean result.
$and - Logical AND of multiple expressions
Complete query:
db.collection.aggregate([
{
$addFields: {
can_edit: {
$and: [
{
$eq: [
{
"$type": "$group.members"
},
"array"
]
},
{
$in: [
"$author._id",
"$group.members.user"
]
}
]
}
}
}
])
Sample 2 in Mongo Playground
I have "Offers" and "Requests" collections, I need to get all offers that user made, group them by requests and find the lowest "Offer.price" on each request, each offer has requestId field.
I am using aggregate to solve this,
db.Offer.aggregate([{
$match: {
ownerId: mongoose.Types.ObjectId(req.params.ownerId)
}
},
{
$group: {
_id: "$requestId",
price: {
$min: "$price"
}
}
}
])
and This is what i get :
[ { _id: 5dc47241af1406031489c65c, price: 14 },
{ _id: 5dc47241af1406031489c653, price: 3 },
{ _id: 5dc47241af1406031489c656, price: 5 },
{ _id: 5dc8add63f73953ff408f962, price: 6 },
{ _id: 5dc8add63f73953ff408f969, price: 22 },
{ _id: 5dc47241af1406031489c658, price: 1 } ]
Now I want to populate these with rest of the data from "Offer"
const OfferSchema = new Schema({
requestId: {
type: Schema.Types.ObjectId,
ref: 'Request'
},
ownerId: {
type: Schema.Types.ObjectId,
required: true,
ref: 'User'
},
price: {
type: Number,
required: true
},
createdAt: {
type: Date,
default: Date.now
},
isBest: {
type: Boolean,
default: false
},
isWinner: {
type: Boolean,
default: false,
}
});
What would be best way to do something like this?
Thank you for your help!
Consider the following dataset:
db.dum.insert({ownerId:1, requestId:'a', price:3, createdAt:3, isWinner:true})
db.dum.insert({ownerId:1, requestId:'a', price:1, createdAt:1, isWinner:false})
db.dum.insert({ownerId:1, requestId:'a', price:2, createdAt:2, isWinner:true})
db.dum.insert({ownerId:1, requestId:'b', price:4, createdAt:2, isWinner:true})
db.dum.insert({ownerId:1, requestId:'b', price:5, createdAt:1, isWinner:false})
db.dum.insert({ownerId:2, requestId:'b', price:5, createdAt:1, isWinner:false})
You could use $reduce
Here, for a grouping id, we keep all matching documents as an array (candidates).
On the project stage, for each group we iterate through the array, and reduce it to the minimal element found (by price that is)
db.dum.aggregate([{
$match: {
ownerId: 1
}
},
{
$group: {
_id: "$requestId",
candidates: { $push:'$$ROOT'}
}
},
{
$project:{
item: {
$reduce: {
input: '$candidates',
initialValue: '$candidates.0',
in: {
$cond: {
if: {
$lt: ['$$value.price', '$$this.price']
},
then:'$$value',
else:'$$this'
}
}
}
}
}
},
{
$replaceRoot:{newRoot:'$item'}
}
]).toArray()
output:
[
{
"_id" : ObjectId("5ddcc8e0eb1f0217802fb507"),
"ownerId" : 1,
"requestId" : "b",
"price" : 4,
"createdAt" : 2,
"isWinner" : true
},
{
"_id" : ObjectId("5ddcc8e0eb1f0217802fb505"),
"ownerId" : 1,
"requestId" : "a",
"price" : 1,
"createdAt" : 1,
"isWinner" : false
}
]
When I run the aggregate in the mongo throw terminal it returns the correct data, but when I run the same aggregate in the code it returns an error.
I had tried to use just match and group to test, but I still get the same error even if I used mongoose functions but without any luck
I plan to try to improve my aggregate game but for now, I need this to run and I'm stuck here for the last couple of days.
"APIError: Arguments must be aggregate pipeline operators\n at new ExtendableError (/vagrant/server/helpers/APIError.js:15:11)\n at new APIError (/vagrant/server/helpers/APIError.js:31:5)\n at app.use (/vagrant/config/express.js:64:22)\n at Layer.handle_error (/vagrant/node_modules/express/lib/router/layer.js:71:5)\n at trim_prefix (/vagrant/node_modules/express/lib/router/index.js:315:13)\n at /vagrant/node_modules/express/lib/router/index.js:284:7\n at Function.process_params (/vagrant/node_modules/express/lib/router/index.js:335:12)\n at next (/vagrant/node_modules/express/lib/router/index.js:275:10)\n at /vagrant/node_modules/express/lib/router/index.js:635:15\n at Immediate.next (/vagrant/node_modules/express/lib/router/index.js:260:14)\n at Immediate._onImmediate (/vagrant/node_modules/express/lib/router/index.js:635:15)\n at runCallback (timers.js:706:11)\n at tryOnImmediate (timers.js:676:5)\n at processImmediate (timers.js:658:5)"
customer model
const mongoose = require('mongoose');
const customerContactsSchema = require('./customerContacts.model');
const appSettings = require('../../config/appSettings');
/**
* Customer Schema
*/
const CustomerSchema = new mongoose.Schema({
avatar: {
type: String,
required: false
},
firstName: {
type: String,
required: false
},
lastName: {
type: String,
required: false
},
password: {
type: String,
required: false,
select: false
},
address: {
country: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Country',
required: false
},
city: {
type: mongoose.Schema.Types.ObjectId,
ref: 'City',
required: false
},
},
language: {
type: String,
required: true,
enum: appSettings.languages,
default: 'english'
},
mobileNumber: {
number: {
type: String,
required: true,
trim: true
},
country: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Country',
required: true
}
},
email: {
type: String,
lowercase: true,
trim: true,
required: false
},
birthday: {
type: Date,
required: false
},
gender: {
type: String,
enum: ['male', 'female'],
required: false
},
friends: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Customer',
required: false
}],
contacts: [customerContactsSchema],
favourites: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Company',
required: false
}],
transactions: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'CustomerTransaction',
required: false
}],
googleId: {
type: String
},
facebookId: {
type: String
},
twitterId: {
type: String
},
fcmToken: {
type: String
},
otpToken: {
type: Number,
required: false,
default: 1234
},
otpTokenExpiration: {
type: Date,
required: false
},
status: {
type: String,
enum: ['active', 'pending', 'suspended', 'blocked', 'deleted'],
required: true,
default: 'pending'
}
}, { timestamps: true });
/**
* #typedef Customer
*/
module.exports = mongoose.model('Customer', CustomerSchema);
CustomerTransaction model
const mongoose = require('mongoose');
const locationSchema = require('./location.model');
/**
* CustomerTransaction Schema
*/
const CustomerTransactionSchema = new mongoose.Schema({
company: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Company',
required: true
},
customer: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Customer',
required: true
},
type: {
type: String,
// debit - , credit +
enum: ['debit', 'credit'],
required: true
},
expirationDate: {
type: Date,
required: false
},
point: {
amount: {
type: Number,
required: false
},
value: {
type: Number,
required: false
}
},
closeValue: {
type: Number,
required: false
},
closePoints: {
type: Number,
required: false
},
offer: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Offer',
required: false
},
voucher: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Voucher',
required: false
},
stamp: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Stamp',
required: false
},
punches: {
type: Number,
required: false
},
date: {
type: Date,
required: false,
default: Date.now()
},
gift: {
type: Boolean,
default: false
},
giftSender: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Customer',
required: false
},
giftReceiver: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Customer',
required: false
},
status: {
type: String,
enum: ['active', 'used', 'sent', 'expired', 'deleted'],
required: true,
default: 'active'
},
location: locationSchema
}, { timestamps: true });
/**
* #typedef CustomerTransaction
*/
module.exports = mongoose.model('CustomerTransaction', CustomerTransactionSchema);
aggregate
db.getCollection('customers').aggregate([
{
$match: {
gender: new RegExp(`.*${'' || ''}.*`, 'i'),
birthday: {
$gte: ISODate('1991-04-29T00:00:00.000Z'),
$lte: ISODate('2019-04-29T00:00:00.000Z'),
},
createdAt: {
$gte: ISODate('1991-04-29T00:00:00.000Z'),
$lte: ISODate('2019-04-29T00:00:00.000Z'),
},
}
},
{
$lookup: {
from: 'customertransactions',
localField: '_id',
foreignField: 'customer',
as: 'CustomerTransaction'
}
},
{
$match: {
'CustomerTransaction.company': ObjectId('5cd2af3a5bfc5b1a40b7de49'),
}
},
{
$project: {
firstName: 1,
lastName: 1,
CustomerTransaction: 1,
CustomerStampTransaction: {
$setDifference: [
{
$map: {
input: '$CustomerTransaction',
as: 'el',
in: {
$cond: [
{
$and: [
{ $gte: ['$$el.stamp', 1] },
{ $lt: ['$$el.offer', 0] },
{ $lt: ['$$el.voucher', 0] },
{ $eq: ['$$el.company', ObjectId('5cd2af3a5bfc5b1a40b7de49')] }
]
},
{
_id: '$$el._id'
},
false
]
}
}
},
[false]
]
},
CustomerPointTransaction: {
$setDifference: [
{
$map: {
input: '$CustomerTransaction',
as: 'el',
in: {
$cond: [
{
$and: [
{ $lt: ['$$el.stamp', 0] },
{ $lt: ['$$el.offer', 0] },
{ $lt: ['$$el.voucher', 0] },
{ $eq: ['$$el.company', ObjectId('5cd2af3a5bfc5b1a40b7de49')] }
]
},
{
_id: '$$el._id',
createdAt: '$$el.createdAt',
closePoints: '$$el.closePoints',
},
false
]
}
}
},
[false]
]
}
}
},
{
$project: {
firstName: 1,
lastName: 1,
CustomerPointTransaction: 1,
stampCount: {
$cond: {
if: { $isArray: '$CustomerStampTransaction' },
then: { $size: '$CustomerStampTransaction' },
else: 0
}
},
//"CustomerTransaction": "$CustomerTransaction"
}
},
{
$unwind: {
path: '$CustomerPointTransaction',
preserveNullAndEmptyArrays: true
}
},
{ $sort: { 'CustomerPointTransaction.createdAt': 1 } },
{
$group: {
_id: '$_id',
firstName: { $first: '$firstName' },
lastName: { $first: '$lastName' },
stampCount: { $first: '$stampCount' },
CustomerPointTransaction: { $last: '$CustomerPointTransaction' }
}
},
{
$group: {
_id: '$_id',
firstName: { $first: '$firstName' },
lastName: { $first: '$lastName' },
stampCount: { $first: '$stampCount' },
pointCount: { $first: '$CustomerPointTransaction.closePoints' }
}
},
]);
aggregate result
{ "_id" : ObjectId("5c8918ab9979f345214158ba"), "firstName" : "mohammed", "lastName" : "alqahtani", "stampCount" : 1, "pointCount" : null }
{ "_id" : ObjectId("5cc161039f11d13109185b15"), "firstName" : "Mohammedaadsxxxa", "lastName" : "AlQahtaniss", "stampCount" : 0, "pointCount" : 9 }
code
function search(req, res) {
Customer.aggregate([
{
$match: {
gender: new RegExp(`.*${'' || ''}.*`, 'i'),
}
},
{
$lookup: {
from: 'customertransactions',
localField: '_id',
foreignField: 'customer',
as: 'CustomerTransaction'
}
},
{
$match: {
'CustomerTransaction.company': req.company._id,
}
},
{
$project: {
firstName: 1,
lastName: 1,
CustomerTransaction: 1,
CustomerStampTransaction: {
$setDifference: [
{
$map: {
input: '$CustomerTransaction',
as: 'el',
in: {
$cond: [
{ $and: [
{ $gte: ['$$el.stamp', 1] },
{ $lt: ['$$el.offer', 0] },
{ $lt: ['$$el.voucher', 0] },
{ $eq: ['$$el.company', req.company._id] }
] },
{
_id: '$$el._id'
},
false
]
}
}
},
[false]
]
},
CustomerPointTransaction: {
$setDifference: [
{
$map: {
input: '$CustomerTransaction',
as: 'el',
in: {
$cond: [
{ $and: [
{ $lt: ['$$el.stamp', 0] },
{ $lt: ['$$el.offer', 0] },
{ $lt: ['$$el.voucher', 0] },
{ $eq: ['$$el.company', req.company._id] }
] },
{
_id: '$$el._id',
createdAt: '$$el.createdAt',
closePoints: '$$el.closePoints',
},
false
]
}
}
},
[false]
]
}
}
},
{
$project: {
firstName: 1,
lastName: 1,
CustomerPointTransaction: 1,
stampCount: { $cond: { if: { $isArray: '$CustomerStampTransaction' }, then: { $size: '$CustomerStampTransaction' }, else: 0 } },
//"CustomerTransaction": "$CustomerTransaction"
}
},
{
$unwind: {
path: '$CustomerPointTransaction',
preserveNullAndEmptyArrays: true
}
},
{ $sort: { 'CustomerPointTransaction.createdAt': 1 } },
{
$group: {
_id: '$_id',
firstName: { $first: '$firstName' },
lastName: { $first: '$lastName' },
stampCount: { $first: '$stampCount' },
CustomerPointTransaction: { $last: '$CustomerPointTransaction' }
}
},
{
$group: {
_id: '$_id',
firstName: { $first: '$firstName' },
lastName: { $first: '$lastName' },
stampCount: { $first: '$stampCount' },
pointCount: { $first: '$CustomerPointTransaction.closePoints' }
}
},
], { cursor: {} })
.then((customers) => {
console.log('kkkkkkkkk');
if (customers) {
res.json(Response.success(customers));
} else {
res.json(Response.success('klklk'));
}
})
.catch(err => res.status(httpStatus.INTERNAL_SERVER_ERROR)
.json(Response.failure(err)));
}
keep in mind even if I run the code like the following it's still throwing the same error
function search(req, res) {
Customer.aggregate([
{
$match: {
gender: new RegExp(`.*${'' || ''}.*`, 'i'),
}
}
], { cursor: {} })
.then((customers) => {
console.log('kkkkkkkkk');
if (customers) {
res.json(Response.success(customers));
} else {
res.json(Response.success('klklk'));
}
})
.catch(err => res.status(httpStatus.INTERNAL_SERVER_ERROR)
.json(Response.failure(err)));
}
Customer.aggregate([
{
$match: {
gender: new RegExp(`.*${'' || ''}.*`, 'i'),
}
}
], { cursor: {} }) // ERROR IS HERE
With mongoose and .aggregate, there is no option parameter like how it is on .find. Instead try this
Customer.aggregate([
{
$match: {
gender: new RegExp(`.*${'' || ''}.*`, 'i'),
}
}
]).cursor()
UPDATED
Customer.aggregate([
{
$match: {
gender: new RegExp(`.*${'' || ''}.*`, 'i'),
}
}
]).exec((error, docs) => {
console.log(docs)
res.json(docs)
})
will this worked with me
async function search(req, res) {
try {
const customers = await Customer.aggregate([
{
$match: {
gender: 'male'
}
}
]
)
.cursor({})
.exec();
customers.get(function(err, ress){
if (ress) {
res.json(Response.success(ress));
} else {
res.json(Response.success('klklk'));
}
});
} catch(e) {
res.status(httpStatus.INTERNAL_SERVER_ERROR) .json(Response.failure(e));
}
}
but I could not figure out why it's working and what was wrong with my previous code