mongodb query to show subdocument as maindocument and having pagination with it - mongodb

First of all, I have this type of model:
const produkSchema = new mongoose.Schema({
nama_produk: String,
etalase: {type: mongoose.Schema.Types.ObjectID, ref: 'kategori'},
kategori: {type: mongoose.Schema.Types.ObjectID, ref: 'kategori'},
jenis: {type: mongoose.Schema.Types.ObjectID, ref: 'kategori.jenis'},
bahan: String,
warna: String,
deskripsi: String,
foto_produk: [String],
harga: Number,
link_bukalapak: String,
link_shopee: String,
link_tokopedia: String,
link_lazada: String,
link_website: String,
display: {type: Boolean, default: false},
}, {
weights: {
nama_produk: 5,
},
timestamps: true
})
const tokoSchema = new mongoose.Schema({
username: {type: String, trim: true},
password: {type: String, required: true, select: false},
merek: String,
listMerek: [{type: mongoose.Schema.Types.ObjectID, ref: 'produk'}],
deskripsi: String,
follower: [{type: mongoose.Schema.Types.ObjectID, ref: 'user'}],
email: {type: String, trim: true, unique: true},
instagram: String,
whatsapp: String,
website: String,
alamat: String,
foto_profil: String,
bukalapak: String,
shopee: String,
tokopedia: String,
fotoktp: String,
banner: [{
gambar: {type: String, required: true, trim: true},
// order: {type: Number, required: true},
}],
produk: [produkSchema],
etalase: [{type: mongoose.Schema.Types.ObjectID, ref: 'kategori'}],
// etalase: [{
// kategori: {type: mongoose.Schema.Types.ObjectID, ref: 'kategori'},
// order: {Number}
// }],
approve: {type: Number, default: 0}, // 0: pending, 1: reject, 2: approve
populer: {type: Boolean, default: false},
gambar_populer: [String],
pilihan: {type: Boolean, default: false},
}, {timestamps: true});
and I have an endpoint to filter this produkSchema with following code:
exports.filterProduk = (req, res) => {
const {merek, warna, kategori, jenis, hargaAwal, hargaAkhir, skip, limit} = req.body
let query = {}
const $and = []
if (merek) {
$and.push({$or: merek.map(id => ({_id: mongoose.Types.ObjectId(id)}))})
}
if (warna) {
$and.push({$or: warna.map(warna => ({"produk.warna": warna}))})
}
if (kategori) {
query["produk.etalase"] = mongoose.Types.ObjectId(kategori)
}
if (jenis) {
$and.push({$or: jenis.map(id => ({"produk.jenis": mongoose.Types.ObjectId(id)}))})
}
if (hargaAwal !== '') {
query["produk.harga"] = {
$gte: parseInt(hargaAwal),
}
}
if (hargaAkhir !== '') {
query["produk.harga"] = {
$lte: parseInt(hargaAkhir)
}
}
if ($and.length > 0) {
query = {$and, ...query}
}
toko.aggregate([
{$unwind: '$produk'},
{$match: query},
{
$lookup: {
from: "kategoris",
as: "produk.etalase",
let: {pjid: "$produk.jenis"},
pipeline: [
{$unwind: "$jenis"},
{$match: {$expr: {$eq: ["$$pjid", "$jenis._id"]}}},
{
$project: {
"jenis._id": 1,
"jenis.label": 1
}
}
]
}
},
{$unwind: {path: "$produk.etalase"}},
{$group: {_id: '$_id', produk: {$push: '$produk'}, foto_profil: {$first: '$foto_profil'}}},
{$limit: skip + limit},
{$skip: skip}
])
.then(async data => {
res.status(200).json({data, prefix: {produk: "uploads/produk", toko: "uploads/toko"}})
})
}
the actual result is:
[
{_id: blabla,
foto_profil: "blabla",
produk:[{nama_produk: "blabla", bahan: "blabla", ...rest ProdukSchema as query}]
},
{_id: blabla,
foto_profil: "blabla",
produk:[{nama_produk: "blabla", bahan: "blabla", ...rest ProdukSchema as query}]
},
{_id: blabla,
foto_profil: "blabla",
produk:[{nama_produk: "blabla", bahan: "blabla", ...rest ProdukSchema as query}]
}
]
expected:
[
{nama_produk: "blabla", bahan: "blabla", ...rest ProdukSchema as query, foto_profil(from toko schema): "blabla"},
{nama_produk: "blabla", bahan: "blabla", ...rest ProdukSchema as query, foto_profil(from toko schema): "blabla"}
]
and having pagination with this produkSchema (limit and offset)
actually, before this, I already ask for this query at here, but this query will produce a lot of data and need to be paginated,
how do I do this? Should I split my produkSchema to main subdocument? or any query exist for this condition?

I am not sure with exact requirement, but I can guess 2 options,
Assuming Variables:
let skip = 0;
let limit = 10;
First Option:
remove $group stage from the end and start after $unwind stage
$replaceRoot to merge objects produk and foto_profil using $mergeObjects and replace the root
{
$replaceRoot: {
newRoot: {
$mergeObjects: [{ foto_profil: "$foto_profil" }, "$produk"]
}
}
},
{ $skip: skip * limit },
{ $limit: limit }
Playground
Second Option:
remove $group stage from the end and start after $unwind stage
$group by toko id and produk id, this will group produk and get unique first produk
$replaceRoot to merge objects produk and foto_profil using $mergeObjects and replace the root
{
$group: {
_id: {
_id: "$_id",
produk_id: "$produk._id"
},
root: { $first: "$$ROOT" }
}
},
{
$replaceRoot: {
newRoot: {
$mergeObjects: [{ foto_profil: "$root.foto_profil" }, "$root.produk"]
}
}
},
{ $skip: skip * limit },
{ $limit: limit }
Playground

maybe you can do this after lookup:
$unwind produk (where the produk still on array type, then unwind it).
$group: { _id: nama_produk, etc... }
$project ...... or whatever you wanna do with.
so you can get the list of group by nama_produk as an _id.

Related

Aggregate data and populate in one request

I am a bit puzzled by populate in MongoDB.
I've got a Schema:
import { Schema, Document, model } from "mongoose";
export interface ProductGroupType {
id: Schema.Types.ObjectId,
title: String,
name: String,
description: String,
}
const ProductGroupSchema: Schema<Document<ProductGroupType>> = new Schema({
title: { type: String, trim: true },
name: { type: String, trim: true },
description: { type: String, trim: true },
}, { collection: "productGroups", timestamps: true });
export const ProductGroupModel = model('ProductGroup', ProductGroupSchema);
and products
import { Schema, Document, model } from "mongoose";
import { plugin as autocomplete } from 'mongoose-auto-increment';
const ProductSchema: Schema<Document<IProduct>> = new Schema({
article: Number,
name: String,
category: { type: Schema.Types.ObjectId, ref: 'ProductCategory' },
group: { type: Schema.Types.ObjectId, ref: 'ProductGroup' },
price: { type: Number, default: 0 },
discount: { type: Number, default: 0 },
stock: {
available: { type: Number, default: 0 },
reserved: { type: Number, default: 0 },
},
images: [Object],
description: String,
productDetails: Object,
}, { collection: "products", timestamps: true });
ProductSchema.plugin(autocomplete, {
model: 'Product',
field: 'article',
startAt: 10000,
});
export const ProductModel = model('Product', ProductSchema);
I need to make a request and group on the MongoDB side data by the field 'group'.
I can make this like this:
await ProductModel.aggregate([
{ $match: { category: Types.ObjectId(queryCategory.id) } },
{
$group: {
_id: '$group',
products: {
$push: {
id: '$_id',
name: '$name',
article: '$article',
price: '$price',
discount: '$discount',
description: '$description',
group: '$groupName',
}
},
count: { $sum: 1 },
}
},
]);
but the output here is:
[
{ _id: 61969583ad32e113f87d0e99, products: [ [Object] ], count: 1 },
{
_id: 61993fff452631090bfff750,
products: [ [Object], [Object] ],
count: 2
}
]
almost what I need but I've been playing around with population and I cannot make it work with Aggregation framework.
I already tried to use the 'lookup' operator but it returns an empty array and doesn't want to work.
That's how I wanted to make it work:
const products: Array<IProduct> = await ProductModel.aggregate([
{ $match: { category: Types.ObjectId(queryCategory.id) } },
{
$group: {
_id: '$group',
products: {
$push: {
id: '$_id',
name: '$name',
article: '$article',
price: '$price',
discount: '$discount',
description: '$description',
group: '$groupName',
}
},
count: { $sum: 1 },
}
},
{
$lookup: {
"from": "productGroups",
"localField": "group",
"foreignField": "_id",
"as": "groupName"
},
},
]);
Is it possible to get the same result as I've got now but populate in the same query group field?
So far the only way I've managed to populate it like this as the second request:
await ProductGroupModel.populate( products.map( (product: any) => {
return {
_id: new ProductGroupModel(product),
products: product.products,
count: product.count,
}
} ), { "path": "_id" } )
In a MongoDB aggregation pipeline, the $group stage passes along only those field explicitly declared in the stage.
In the same pipeline you show, the documents passed along by the $group stage would contain the fields:
_id
products
count
When the exector arrives a the $lookup stage, none of the documents contain a field named group.
However, the value previously contained in the group field still exists, in the _id field.
In the $lookup stage, use
"localField": "_id",
to find documents based on that value.

joining collections sub-subdocument with other collections subdocument using aggregate mongodb mongoose

So, I have this kind of model
const produkSchema = new mongoose.Schema({
nama_produk: String,
etalase: {type: mongoose.Schema.Types.ObjectID, ref: 'kategori'},
kategori: {type: mongoose.Schema.Types.ObjectID, ref: 'kategori'},
jenis: {type: mongoose.Schema.Types.ObjectID, ref: 'kategori.jenis'},
bahan: String,
warna: String,
deskripsi: String,
foto_produk: [String],
harga: Number,
link_bukalapak: String,
link_shopee: String,
link_tokopedia: String,
}, {
weights: {
nama_produk: 5,
},
timestamps: true
})
const tokoSchema = new mongoose.Schema({
username: {type: String, trim: true},
password: {type: String, required: true, select: false},
merek: String,
listMerek: [{type: mongoose.Schema.Types.ObjectID, ref: 'produk'}],
deskripsi: String,
follower: [{type: mongoose.Schema.Types.ObjectID, ref: 'user'}],
email: {type: String, trim: true, unique: true},
instagram: String,
whatsapp: String,
website: String,
alamat: String,
foto_profil: String,
bukalapak: String,
shopee: String,
tokopedia: String,
fotoktp: String,
banner: [{
gambar: {type: String, required: true, trim: true},
order: {type: Number, required: true},
}],
produk: [produkSchema],
etalase: [{type: mongoose.Schema.Types.ObjectID, ref: 'kategori'}],
approve: {type: Number, default: 0}, // 0: pending, 1: reject, 2: approve
populer: {type: Boolean, default: false},
}, {timestamps: true});
exports.toko = mongoose.model("toko", tokoSchema);
const jenisSchema = new mongoose.Schema({
label: String,
gambar: String,
}, {timestamps: true})
const kategoriSchema = new mongoose.Schema({
label: String,
gambar: String,
jenis: [jenisSchema]
}, {timestamps: true});
so what I want to join is, toko.produk.jenis with kategori.jenis, but as you know, mongoose can't populate between subdocument, I have tried toko.find().populate("produk.jenis", "label") but it showing error Schema hasn't been registered for model "kategori.jenis". Use mongoose.model(name, schema) any query suggestions? I've tried
{
$lookup: {
"from": "kategoris",
"localField": "produk.jenis",
"foreignField": "jenis",
"as": "jenisnya"
}
}
but it doesn't seem work, and returning an empty array instead. What should I do? Should I rearrange my schema?
You can try this,
$match your conditions
$unwind deconstruct produk array
$lookup with pipeline
$unwind deconstruct jenis array
$match match jenis._id
$project to show only _id and label
$unwind deconstruct in path produk.jenisnya
$group by _id and push in to produk
db.toko.aggregate([
{ $match: { _id: ObjectId("5f1d77aca53cb13980324c73") } },
{ $unwind: "$produk" },
{
$lookup: {
from: "kategoris",
as: "produk.jenisnya",
let: { pjid: "$produk.jenis" },
pipeline: [
{ $unwind: "$jenis" },
{ $match: { $expr: { $eq: ["$$pjid", "$jenis._id"] } } },
{ $project: { "jenis._id": 1, "jenis.label": 1 } }
]
}
},
{ $unwind: { path: "$produk.jenisnya" } },
{
$group: {
_id: "$_id",
produk: { $push: "$produk" },
// you can add otehr fields as well like alamat
alamat: { $first: "$alamat" }
}
}
])
Playground

How to join between two collections when foreign key in an array of ObjectIs's

Let's say I have the following collection structure for mongodb:
users
id
userId
emailAddress
products
id
sku
price
productRewards
products: [{productId}],
pointsNeeded
rewardAmount
productUsers
productId
userId
The mongoose schema is listed below:
UserSchema: Schema = new Schema({
userName: {type: Schema.Types.String, minlength: 6, maxlength: 10, required: true},
emailAddress: {type: Schema.Types.String, minlength: 8, maxlength: 55, required: true}
}
ProductSchema: Schema = new Schema({
sku: {type: Schema.Types.String, minlength: 1, maxlength: 30, required: true},
price: {type: Schema.Types.Number, required: true}
}
ProductRewardSchema: Schema = new Schema({
products: [{productId: {type: Schema.Types.ObjectId, required: true, ref: 'product'}}],
pointsNeeded: {type: Schema.Types.Number, required: true},
rewardAmount: {type: Schema.Types.Number, required: true}
}
ProductUserSchema: Schema = new Schema({
productId: {type: Schema.Types.ObjectId, ref: 'product'},
userId: {type: Schema.Types.ObjectId, ref: 'user'}
}
DB Version: 3.6
and I need to calculate the points users have accumulated to determine the rewards they are eligibility for. How should I perform this in mongodb? I know how this could be done in sql just not nosql.
I am trying something like this but am not sure this is right:
db.productUsers.aggregate(
{ "$unwind": "productRewards.products" },
{
"$lookup": {
from: "productRewards",
localField: "productRewards.products.productId",
foreignField: "productId",
as: "rewards"
}
},
{
"$group": {
userId: "$userId", productId: "$productId", total: { $sum: "$price" }
}
},
{
"$project": {
userId: 1,
productId: 1,
total: 1
}
}
)
Additionally I am expecting a json output like this:
{ "userId": "ObjectId string", "name": "product name string", "total": 50 }
I'll start from productUsers collection and join to productRewards collection on productId and work from there.
Something like
db.productUsers.aggregate(
{
"$lookup": {
from: "productRewards",
localField: "productId",
foreignField: "products.productId",
as: "rewards"
}
},
{
"$project": {
userId: 1,
productId: 1,
points: {$arrayElemAt:["$rewards.pointsNeeded", 0]}
}
}
)

How to include parent fields into $project when aggregating embbeded

I have the following structure:
var UserSchema = new Schema({
name: String,
email: { type: String, lowercase: true },
offers: [],
});
var OfferSchema = new Schema({
dateFrom: Date,
dateTill: Date,
destination: String,
budget: String,
currency: {},
dateCreated: {type: Date, default: Date.now}
});
I make an aggregations:
User.aggregate(
{ $project: {"offers": 1, _id: 0}},
{ $unwind: "$offers" },
{ $sort: {"offers.dateCreated": -1} },
function (err, result) {
if (!err) {
}
);
And the result is ok, but I want every element to include its parent fields (ex: _id and other fields).
How can I do it?
Just remove the $project stage:
User.aggregate(
{ $unwind: "$offers" },
{ $sort: {"offers.dateCreated": -1} },
function (err, result) {
if (!err) {
}
);

How to make a comma separated list of array values?

I have the following documents
User Schema:
var UserSchema = new Schema({
name: String,
email: { type: String, lowercase: true },
offers: [],
anyCountry: {type: Boolean, default: false},
city: String,
});
Tags Schema
var TagSchema = new Schema({
text: String,
dateCreated: { type: Date, default: Date.now}
});
I am aggregating it this way:
User.aggregate(
{$match: {
$or: [
{'isBlocked': false},
{'isBlocked': {$exists: false}}
]}},
{ $project: {"offers": 1, _id: 0, city: 1, name: 1}},
{ $unwind: "$offers" },
{
$match: {
$and: [
{'offers': { $not: { $size: 0} }},
{'offers.type': type}
]
}
},
{ $sort: {"offers.dateCreated": -1} },
function (err, result) {
if (!err) {
return res.json({status: 'success', data: result});
} else {
return res.send(err);
}
}
)
The output is ok, but it contains tags as array. What I need is:
to have array values assigned to a computed field "offers.tagsList" as a coma separated sting {offers.tagsList = 'tag1, tag2, tag3, ...'}.
check if filed offers.anyCountry doesn't exists and add it to the output with value false.
Thanks!