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

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

Related

populate or aggregator, how?

I want to get only all messages related to the projects posted by user.
I don't think there is a way to do so using populate because we have to get the user id from the other collection and then render messages using his id which is "submittedBy" in the project schema.
I am trying aggregator but it is also not working.
Any help or clue would be appreciated.
message Schema:
const MessageSchema = new Schema({
content:{
type: String,
trim: true
},
projectId:{
type: Schema.Types.ObjectId,
ref: 'Form'
},
imageId:{
type: Schema.Types.ObjectId,
ref: 'Image'
},
sender:{
type: Schema.Types.ObjectId,
ref: 'User'
},
readBy:[{
type: Schema.Types.ObjectId,
ref: 'User'
}],
}, {timestamps:true});
Project schema:
const FormSchema = new Schema({
submittedBy: {
type: Schema.Types.ObjectId,
ref: 'User'
},
logoName: {
type: String,
trim: true
},
industry: [{
type: String,
trim: true
}],
logoType: [{
type: String,
trim: true
}],
colors: [{
type: String,
trim: true
}],
tagline: {
type: String,
trim: true
},
org: {
type: String,
trim: true
},
communicate: {
type: String,
trim: true
},
deadline : {
type: String,
trim: true
},
services: {
type: String,
trim: true
},
package: {
type: String,
trim: true
},
assignedTo: {
type: String,
trim: true
}
}, {timestamps:true})
I tried population but I don't think it will work or efficient.
So I am using aggregation but it's not working:
var objId = mongoose.Types.ObjectId(req.session.user._id);
results = await Model.aggregate([
{
$lookup:
{
from: "forms",
localField: "projectId",
foreignField: "_id",
as: "tags"
}
},
{ $unwind: "$tags" },
{$match:{ 'tags.submittedBy': userId }},
]).exec(function(err, results) {
console.log(results)
});
Result After Combining two collections:
{
_id: 609bd3772583c56dd3e302d3,
readBy: [],
content: 'Remove Tagline and align.',
sender: 604db5708cf232652cd4469e,
imageId: 6099964c2583c56dd3e302c3,
projectId: 60968eed2583c56dd3e302ac,
createdAt: 2021-05-12T13:09:11.060Z,
updatedAt: 2021-05-12T13:09:11.060Z,
__v: 0,
tags: {
_id: 60968eed2583c56dd3e302ac,
industry: [Array],
logoType: [Array],
colors: [Array],
logoName: 'global consultancy services',
tagline: 'Follow your dreams',
org: 'We guide students who seeks to study overseas e.g. the UK. We offer consultancy services to students from selecting a university to getting visa and then getting in the country.',
communicate: '3 concepts\r\n' +
'Keep "Indra global " big AND bold\r\n' +
'and "consultancy services" below it.\r\n' +
'In one concept you can use IGCS initials to create icon form'
deadline: '2021-05-12',
services: '',
package: 'undefined',
submittedBy: 60968eed2583c56dd3e302ab,
createdAt: 2021-05-08T13:15:25.661Z,
updatedAt: 2021-05-10T08:28:18.587Z,
__v: 0,
assignedTo: '6050d8485cc8bb6a20ef36fa'
}
}
]

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

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.

Id is created for every nested objects in documents of a collection in mongodb via mongoose

I have a user schema. While saving document, for every nested object (quizHistory,record & responses) in document, mongoose add _id field automatically. For ref- quizHistory path
const userSchema = new Schema({
firstName: { type: String, required: true ,trim:true},
lastName:{ type: String, required: true ,trim:true},
email: { type: String, unique: true, required: true },
isUser: { type: Boolean, default: true },
password: String,
quizHistory: [{
quizId: { type: Schema.Types.ObjectId, ref: 'Quiz' },
record: [{
recordId:{ type: Number},
startTime: { type: Date },
responses: [{
quesId: { type: Schema.Types.ObjectId, ref: 'Question' },
answers: [Number]
}],
score: Number
}],
avgScore: Number
}]
})
Mongoose create virtual id by default(guide id).
Add this line to your schema.
_id : {id:false}

Is it possible to have two types of objects in an array of mongoose schema

I am building an e-commerce application and this is my orders schema.
const mongoose = require("mongoose");
const orderSchema = new mongoose.Schema({
buyer: {
type: mongoose.Schema.Types.ObjectId,
ref: "buyer",
required: true
},
items: [{
item: {
type: mongoose.Schema.Types.ObjectId,
ref: "item",
},
quantity: {
type: Number,
default: 1
}}
],
seller: {
type: mongoose.Schema.Types.ObjectId,
ref: "seller",
required: true
},
location: {
type: {
type: "String",
enum:['Point']
},
coordinates: {
type: [Number],
index: '2dsphere'
}
},
sendAt:{
type: Date,
default: Date.now
}
});
const orderModel = mongoose.model("orders", orderSchema);
module.exports = orderModel;
I want to have an array having item-reference-id and quantity.
But with the above schema when i enter data, each item is acting as an another sub-document and having _id. Query response image.
I have found solution:
order: [
{
_id: false,
item: {
type: mongoose.Schema.Types.ObjectId,
ref: "items",
required: true,
},
quantity: { type: Number },
},
],
_id: false will stop the subdocument from creating another id for the subdocument.

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]}
}
}
)