I'm trying to add in a project pipeline where the values are greater than 100, the values are fields inside and object which are part of an array. I have something like this:
Database:
---Clients Collection---
client: {
_id: 1,
taxID: aldsfkjasdlñfk
// other stuff
}
---Invoices Collection---
invoice: {
_id: 1,
clientID: 1,
total: 50
},
invoice: {
_id: 2,
clientID: 1,
total: 150
},
invoice: {
_id: 3,
clientID: 1,
total: 200
}
AND THIS IS MY QUERY:
{
$lookup: {
from: 'invoices',
localField: '_id',
foreignField: 'client.id',
as: 'invoices'
}
},
{
$project: {
id: 1,
taxID: aldsfkjasdlñfk,
invoicesAmountGreaterThanOneHundred: {
$sum: {
$cond: { if: { $gte: ['$invoices.total', 100] }, then: '$invoices.total', else: 0 }
}
}
}
}
So the output should be:
{
_id: 1.
taxID: aldsfkjasdlñfk,
invoicesAmountGreaterThanOneHundred: 350
}
I'm using Mongo 3.6.3.
Also in the future I will add a "invoicesAmountLesserThanOneHundred", same method, but for lesser than 100 of course.
use $filter before $sum
db.client.aggregate([
{
$lookup: {
from: "invoices",
localField: "_id",
foreignField: "clientID",
as: "invoices"
}
},
{
$set: {
"invoices": {
"$filter": {
"input": "$invoices",
"as": "i",
"cond": { $gte: [ "$$i.total", 100 ] }
}
}
}
},
{
$project: {
id: 1,
taxID: 1,
invoicesAmountGreaterThanOneHundred: {
$sum: "$invoices.total"
}
}
}
])
mongoplayground
use $reduce
db.client.aggregate([
{
$lookup: {
from: "invoices",
localField: "_id",
foreignField: "clientID",
as: "invoices"
}
},
{
$set: {
"invoicesAmountGreaterThanOneHundred": {
$reduce: {
input: "$invoices",
initialValue: "",
in: {
$sum: [
"$$value",
{
$cond: {
if: { $gte: [ "$$this.total", 100 ] },
then: "$$this.total",
else: 0
}
}
]
}
}
}
}
}
])
mongoplayground
I have written a shell query which works perfectly on mongo shell but is not returning any value when run using mongoose in nodejs + typescript;
Mongo shell
db.userworks.aggregate([
{
$match: {
user: ObjectId("607dfc9fd1ae095014ab57a0"),
workspace: ObjectId("607dfca7d1ae095014ab57a1"),
},
},
{
$project: {
_id: 0,
},
},
{
$lookup: {
from: 'workspaces',
localField: 'workspace',
foreignField: '_id',
as: 'workspaces',
},
},
{
$unwind: '$workspaces',
},
{
$lookup: {
from: 'projects',
localField: 'workspaces.projects',
foreignField: '_id',
as: 'projects',
},
},
{
$unwind: '$projects',
},
{
$project: {
projects: 1,
},
},
{ $replaceRoot: { newRoot: '$projects' } },
{
$sort: {
'projects.createdAt': -1,
},
},
]).pretty()
But when I run the same query using mongoose in one of my routes.
Mongoose :
const projects = await UserWorks.aggregate([
{
$match: {
user: '607dfc9fd1ae095014ab57a0',
workspace: '607dfca7d1ae095014ab57a1',
},
},
{
$project: {
_id: 0,
},
},
{
$lookup: {
from: 'workspaces',
localField: 'workspace',
foreignField: '_id',
as: 'workspaces',
},
},
{
$unwind: '$workspaces',
},
{
$lookup: {
from: 'projects',
localField: 'workspaces.projects',
foreignField: '_id',
as: 'projects',
},
},
{
$unwind: '$projects',
},
{
$project: {
projects: 1,
},
},
{ $replaceRoot: { newRoot: '$projects' } },
{
$sort: {
'projects.createdAt': -1,
},
},
])
I would really appreciate if someone could help me with this.
Because it took me a while to make this query on shell.
You can't compare ObjectId and String.
you have to convert it ObjectId
mongoose.Types.ObjectId('607dfc9fd1ae095014ab57a0')
{
$match: {
user: mongoose.Types.ObjectId('607dfc9fd1ae095014ab57a0') ,
workspace: mongoose.Types.ObjectId('607dfca7d1ae095014ab57a1'),
},
},
I am trying to get the last message of every single conversation between User 1 and User N.
I've managed to compile the below together, however it is throwing the above, aforementioned error. How can I solve this, with ObjectIds, as that is what I have in the DB, not strings?
await MostRecentMessages.aggregate(
[
{
$match: {
$or: [
{ from: mongoose.Types.ObjectId(id) },
{ to: mongoose.Types.ObjectId(id) }
],
deletedBy: { $ne: id }
}
},
{ $sort: { date: -1 } },
{ $project: { _id: 1, from: 1, to: 1, conversation: 1, date: 1 } },
{
$group: {
_id: {
lastMessage: {
$cond: [
{
$gt: [
{ $substr: ["$to", 0, 1] },
{ $substr: ["$from", 0, 1] }
]
},
{ $concat: ["$to", " and ", "$from"] },
{ $concat: ["$from", " and ", "$to"] }
]
}
},
conversation: { $first: "$$ROOT" }
}
},
{
$lookup: {
from: "conversations",
localField: "conversation",
foreignField: "_id",
as: "conversation"
}
},
{ $unwind: { path: "$conversation" } },
{
$lookup: {
from: "users",
localField: "to",
foreignField: "_id",
as: "to"
}
},
{ $unwind: { path: "$to" } },
{
$lookup: {
from: "users",
localField: "from",
foreignField: "_id",
as: "from"
}
},
{ $unwind: { path: "$from" } }
],
function(err, docs) {
if (err) {
console.log(err);
} else {
console.log("MostRecentMessages", docs);
return res.json(docs);
}
}
);
My schema, if it matters:
const MostRecentMessageSchema = new Schema({
to: {
type: mongoose.Schema.Types.ObjectId,
ref: "user"
},
from: {
type: mongoose.Schema.Types.ObjectId,
ref: "user"
},
conversation: {
type: mongoose.Schema.Types.ObjectId,
ref: "conversation"
},
deletedBy: {
type: [String]
},
date: {
type: Date,
default: Date.now
}
});
Edit
Here are the 5 relevant documents:
// db.MostRecentMessages
_id:5dca7a61e95bd3341cad64b9
to:5dca58c21825d8269a32cb10
from:5dca58ce1825d8269a32cb11
conversation:5dca7aea51b626350fa865dd
date:2019-11-12T09:24:49.906+00:00
_id:5dca7ab0d3a44a34c98b7263
to:5dca58ce1825d8269a32cb11
from:5dca58c21825d8269a32cb10
conversation:5dca7ab0d3a44a34c98b7262
date:2019-11-12T09:26:08.125+00:00
// db.Conversation
_id:5dca7a61e95bd3341cad64b8
text:"Test1"
user:5dca58ce1825d8269a32cb11 // sender
recipient:5dca58c21825d8269a32cb10
createdAt:2019-11-12T09:24:49.827+00:00
_id:5dca7ab0d3a44a34c98b7262
text:"Test2"
user:5dca58c21825d8269a32cb10 // sender
recipient:5dca58ce1825d8269a32cb11
createdAt:2019-11-12T09:26:08.105+00:00
_id:5dca7aea51b626350fa865dd
text:"Test3"
user:5dca58ce1825d8269a32cb11 // sender
recipient:5dca58c21825d8269a32cb10
createdAt:2019-11-12T09:27:06.562+00:00
I copied and pasted the answer below, but it only returns the message with text Test2
Link in comment is for your own knowledge...
Now let's see what is working (it will probably sounds like 'deja vu' for you, but it works!). All (and your aforementioned error) take place in $group stage
db.lastMessage.aggregate([
{
$match: {
$or: [
{
from: ObjectId("5a934e000102030405000001")
},
{
to: ObjectId("5a934e000102030405000001")
}
],
deletedBy: {
$ne: ObjectId("5a934e000102030405000001")
}
}
},
{
$sort: {
date: -1
}
},
{
$project: {
_id: 1,
from: 1,
to: 1,
conversation: 1,
date: 1
}
},
{
$group: {
_id: {
userConcerned: {
$cond: {
if: {
$eq: [
"$to",
ObjectId("5a934e000102030405000001")
]
},
then: "$to",
else: "$from"
}
},
interlocutor: {
$cond: {
if: {
$eq: [
"$to",
ObjectId("5a934e000102030405000001")
]
},
then: "$from",
else: "$to"
}
}
},
from: {
$first: "$from"
},
to: {
$first: "$to"
},
date: {
$first: "$date"
},
conversation: {
$first: "$conversation"
}
}
},
{
$lookup: {
from: "conversations",
localField: "conversation",
foreignField: "_id",
as: "conversation"
}
},
{
$unwind: {
path: "$conversation"
}
},
{
$lookup: {
from: "users",
localField: "to",
foreignField: "_id",
as: "to"
}
},
{
$unwind: {
path: "$to"
}
},
{
$lookup: {
from: "users",
localField: "from",
foreignField: "_id",
as: "from"
}
},
{
$unwind: {
path: "$from"
}
}
])
You can verify here . I just modified your group stage with the one i already provided in your other question.
I have this endpoint, it's the initial endpoint when a customer is visiting the eshop:
export const getAllProductsByCategory = async (req, res, next) => {
const pageSize = parseInt(req.query.pageSize);
const sort = parseInt(req.query.sort);
const skip = parseInt(req.query.skip);
const { order, filters } = req.query;
const { brands, tags, pricesRange } = JSON.parse(filters);
try {
const aggregate = Product.aggregate();
aggregate.lookup({
from: 'categories',
localField: 'categories',
foreignField: '_id',
as: 'categories'
});
aggregate.match({
productType: 'product',
available: true,
categories: {
$elemMatch: {
url: req.params
}
}
});
aggregate.lookup({
from: 'tags',
let: { tags: '$tags' },
pipeline: [
{
$match: {
$expr: { $in: ['$_id', '$$tags'] }
}
},
{
$project: {
_id: 1,
name: 1,
slug: 1
}
}
],
as: 'tags'
});
aggregate.lookup({
from: 'brands',
let: { brand: '$brand' },
pipeline: [
{
$match: {
$expr: { $eq: ['$_id', '$$brand'] }
}
},
{
$project: {
_id: 1,
name: 1,
slug: 1
}
}
],
as: 'brand'
});
if (brands.length > 0) {
const filterBrands = brands.map((_id) => utils.toObjectId(_id));
aggregate.match({
$and: [{ brand: { $elemMatch: { _id: { $in: filterBrands } } } }]
});
}
if (tags.length > 0) {
const filterTags = tags.map((_id) => utils.toObjectId(_id));
aggregate.match({ tags: { $elemMatch: { _id: { $in: filterTags } } } });
}
if (pricesRange.length > 0 && pricesRange !== 'all') {
const filterPriceRange = pricesRange.map((_id) => utils.toObjectId(_id));
aggregate.match({
_id: { $in: filterPriceRange }
});
}
aggregate.facet({
tags: [
{ $unwind: { path: '$tags' } },
{ $group: { _id: '$tags', tag: { $first: '$tags' }, total: { $sum: 1 } } },
{
$group: {
_id: '$tag._id',
name: { $addToSet: '$tag.name' },
total: { $addToSet: '$total' }
}
},
{
$project: {
name: { $arrayElemAt: ['$name', 0] },
total: { $arrayElemAt: ['$total', 0] },
_id: 1
}
},
{ $sort: { total: -1 } }
],
brands: [
{ $unwind: { path: '$brand' } },
{
$group: {
_id: '$brand._id',
name: { $first: '$brand.name' },
slug: { $first: '$brand.slug' },
total: {
$sum: 1
}
}
},
{ $sort: { name: 1 } }
],
pricesRange: [
{
$bucket: {
groupBy: {
$cond: {
if: { $ne: ['$onSale.value', true] },
then: '$price',
else: '$sale.salePrice'
}
},
boundaries: [0, 20.01, 50.01],
default: 'other',
output: {
count: { $sum: 1 },
products: { $push: '$_id' }
}
}
}
],
products: [
{ $skip: (skip - 1) * pageSize },
{ $limit: pageSize },
{
$project: {
_id: 1,
images: 1,
onSale: 1,
price: 1,
quantity: 1,
slug: 1,
sale: 1,
sku: 1,
status: 1,
title: 1,
brand: 1,
tags: 1,
description: 1
}
},
{ $sort: { [order]: sort } }
],
total: [
{
$group: {
_id: null,
count: { $sum: 1 }
}
},
{
$project: {
count: 1,
_id: 0
}
}
]
});
aggregate.addFields({
total: {
$arrayElemAt: ['$total', 0]
}
});
const [response] = await aggregate.exec();
if (!response.total) {
response.total = 0;
}
res.status(httpStatus.OK);
return res.json(response);
} catch (error) {
console.log(error);
return next(error);
}
};
If no filters are applied all products matches the category requested with no problem.
My issue is when a customer selects a brand or tag, then the facet returns the products, but returns only one brand/tag (as it should be since the products filtered have only this brand).
What I must do in order to retain all brands/tags and let the user select more than one brand/tag? If customer selects a brand, then the tags should match the returned products tags and vice versa.
Is there a better way to implement tags stage in $facet since tags is an array and the desired output is: [{_id: 123, name: {label: 'test', value: 123]}]
The request is like:(1,2,3,4 represents _id)
http://locahost:3000/get-products/?filters={brands: [1, 2], tags: [3,4], pricesRange:[]}
Update
This is the products schema with tags and brands:
brand: {
ref: 'Brand',
type: Schema.Types.ObjectId
},
tags: [
{
ref: 'Tags',
type: Schema.Types.ObjectId
}
]
tags schema:
{
metaDescription: {
type: String
},
metaTitle: {
type: String
},
name: {
label: {
type: String,
index: true
},
value: {
type: Schema.Types.ObjectId
},
},
slug: {
type: String,
index: true
},
status: {
label: {
type: String
},
value: {
default: true,
type: Boolean
}
}
}
brands schema:
description: {
default: '',
type: String
},
name: {
required: true,
type: String,
unique: true
},
slug: {
type: String,
index: true
},
status: {
label: {
default: 'Active',
type: String
},
value: {
default: true,
type: Boolean
}
}
Scenario:
User visits store, selects a category and all matching products should return with matched brands, tags, priceRange & pagination.
Case 1:
User clicks a brand from checkbox, then the request returns matching products,tags & priceRanges and all brands of the selected category, not of matched products
Case 2:
User selects a brand like Case 1, but then decides to check a tag too, then the request should return all brands and tags again, but products matched against them.
Case 3:
User do not select brand but selects a tag only, the request should return all matching products that have that tag/tags and return the brands that matched the products returned.
Case 4:
Same as case 3, but user selects a brand after selecting a tag/tags, the request should return matching products, brands & tags.
In all cases pagination should return proper total, also priceRanges should match the returned results.
I hope it's clear now, I think I've not missed any other case. I could probably grey out/disable the tags/brands that do not match the response in the front end but I don't know if this is user friendly.
This is what I ended up with:
export const getAllProductsByCategory = async (req, res, next) => {
const pageSize = parseInt(req.query.pageSize);
const sort = parseInt(req.query.sort);
const skip = parseInt(req.query.skip);
const { order, filters } = req.query;
const { brands, tags, pricesRange } = JSON.parse(filters);
try {
const aggregate = Product.aggregate();
aggregate.lookup({
from: 'categories',
localField: 'categories',
foreignField: '_id',
as: 'categories'
});
aggregate.match({
productType: 'product',
available: true,
categories: {
$elemMatch: {
url: `/${JSON.stringify(req.params['0']).replace(/"/g, '')}`
}
}
});
aggregate.lookup({
from: 'tags',
let: { tags: '$tags' },
pipeline: [
{
$match: {
$expr: { $in: ['$_id', '$$tags'] }
}
},
{
$project: {
_id: 1,
name: 1,
slug: 1
}
}
],
as: 'tags'
});
aggregate.lookup({
from: 'brands',
let: { brand: '$brand' },
pipeline: [
{
$match: {
$expr: { $eq: ['$_id', '$$brand'] }
}
},
{
$project: {
_id: 1,
name: 1,
slug: 1
}
}
],
as: 'brand'
});
const filterBrands = brands.map((_id) => utils.toObjectId(_id));
const filterTags = tags.map((_id) => utils.toObjectId(_id));
const priceRanges = pricesRange ? pricesRange.match(/\d+/g).map(Number) : '';
aggregate.facet({
tags: [
{ $unwind: { path: '$brand' } },
{ $unwind: { path: '$tags' } },
{
$match: {
$expr: {
$and: [
filterBrands.length ? { $in: ['$brand._id', filterBrands] } : true
]
}
}
},
{ $group: { _id: '$tags', tag: { $first: '$tags' }, total: { $sum: 1 } } },
{
$group: {
_id: '$tag._id',
name: { $addToSet: '$tag.name' },
total: { $addToSet: '$total' }
}
},
{
$project: {
name: { $arrayElemAt: ['$name', 0] },
total: { $arrayElemAt: ['$total', 0] },
_id: 1
}
},
{ $sort: { name: 1 } }
],
brands: [
{ $unwind: { path: '$brand' } },
{ $unwind: { path: '$tags' } },
{
$match: {
$expr: {
$and: [
filterTags.length ? { $in: ['$tags._id', filterTags] } : true
]
}
}
},
{
$group: {
_id: '$brand._id',
name: { $first: '$brand.name' },
slug: { $first: '$brand.slug' },
total: {
$sum: 1
}
}
},
{ $sort: { name: 1 } }
],
products: [
{ $unwind: { path: '$brand', preserveNullAndEmptyArrays: true } },
{ $unwind: { path: '$tags', preserveNullAndEmptyArrays: true } },
{
$match: {
$expr: {
$and: [
filterBrands.length ? { $in: ['$brand._id', filterBrands] } : true,
filterTags.length ? { $in: ['$tags._id', filterTags] } : true,
pricesRange.length
? {
$and: [
{
$gte: [
{
$cond: {
if: { $ne: ['$onSale.value', true] },
then: '$price',
else: '$sale.salePrice'
}
},
priceRanges[0]
]
},
{
$lte: [
{
$cond: {
if: { $ne: ['$onSale.value', true] },
then: '$price',
else: '$sale.salePrice'
}
},
priceRanges[1]
]
}
]
}
: true
]
}
}
},
{ $skip: (skip - 1) * pageSize },
{ $limit: pageSize },
{
$project: {
_id: 1,
brand: 1,
description: 1,
images: 1,
onSale: 1,
price: 1,
quantity: 1,
sale: 1,
shipping: 1,
sku: 1,
skuThreshold: 1,
slug: 1,
status: 1,
stock: 1,
tags: 1,
title: 1
}
},
{ $sort: { [order]: sort } }
],
pricesRange: [
{ $unwind: { path: '$brand', preserveNullAndEmptyArrays: true } },
{ $unwind: { path: '$tags', preserveNullAndEmptyArrays: true } },
{
$match: {
$expr: {
$and: [
filterBrands.length ? { $in: ['$brand._id', filterBrands] } : true,
filterTags.length ? { $in: ['$tags._id', filterTags] } : true
]
}
}
},
{
$project: {
price: 1,
onSale: 1,
sale: 1,
range: {
$cond: [
{
$and: [
{
$gte: [
{
$cond: {
if: { $ne: ['$onSale.value', true] },
then: '$price',
else: '$sale.salePrice'
}
},
0
]
},
{
$lte: [
{
$cond: {
if: { $ne: ['$onSale.value', true] },
then: '$price',
else: '$sale.salePrice'
}
},
20
]
}
]
},
'0-20',
{
$cond: [
{
$and: [
{
$gte: [
{
$cond: {
if: { $ne: ['$onSale.value', true] },
then: '$price',
else: '$sale.salePrice'
}
},
20
]
},
{
$lte: [
{
$cond: {
if: { $ne: ['$onSale.value', true] },
then: '$price',
else: '$sale.salePrice'
}
},
50
]
}
]
},
'20-50',
'50+'
]
}
]
}
}
},
{
$group: {
_id: '$range',
count: { $sum: 1 }
}
},
{
$project: {
_id: 0,
range: '$_id',
count: 1
}
},
{ $unwind: { path: '$range', preserveNullAndEmptyArrays: true } },
{
$sort: {
range: 1
}
}
],
total: [
{ $unwind: { path: '$brand', preserveNullAndEmptyArrays: true } },
{ $unwind: { path: '$tags', preserveNullAndEmptyArrays: true } },
{
$match: {
$expr: {
$and: [
filterBrands.length ? { $in: ['$brand._id', filterBrands] } : true,
filterTags.length ? { $in: ['$tags._id', filterTags] } : true,
pricesRange.length
? {
$and: [
{
$gte: [
{
$cond: {
if: { $ne: ['$onSale.value', true] },
then: '$price',
else: '$sale.salePrice'
}
},
priceRanges[0]
]
},
{
$lte: [
{
$cond: {
if: { $ne: ['$onSale.value', true] },
then: '$price',
else: '$sale.salePrice'
}
},
priceRanges[1]
]
}
]
}
: true
]
}
}
},
{
$group: {
_id: null,
count: { $sum: 1 }
}
},
{
$project: {
count: 1,
_id: 0
}
}
]
});
aggregate.addFields({
total: {
$arrayElemAt: ['$total', 0]
}
});
const [response] = await aggregate.exec();
if (!response.total) {
response.total = 0;
}
res.status(httpStatus.OK);
return res.json(response);
} catch (error) {
console.log(error);
return next(error);
}
};