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.