Mongo aggregate $match and $group in $lookup result - node.js

I have node text
I have 3 models lead, user and company
I want to get all customers for selected company id using lead model
what I try was,
using $lookup and $group I got unique customer with details but after adding $match I got empty array
I hope someone can get my on the right track..
****This is my Aggregation code ****
const customers = await Lead.aggregate([
{
$match: { moving_company: companyId },
},
{
$group: {
_id: {
"customer": "$customer",
},
}
},
{
$lookup: {
from: "users",
localField: "_id.customer",
foreignField: "_id",
as: "myCustomResut",
},
},
]);
This is my Company model
const schemaOptions: mongoose.SchemaOptions = {
_id: true,
id: false,
timestamps: true,
skipVersioning: true,
strict: false,
toJSON: {
getters: true,
virtuals: true,
},
};
export const CompanySchema = new mongoose.Schema(
{
name: {
type: Schema.Types.String,
},
},
schemaOptions
);
const Company = mongoose.model<ICompany>("Company", CompanySchema);
export default Company;
This my lead model
const schemaOptions: mongoose.SchemaOptions = {
_id: true,
id: false,
timestamps: true,
skipVersioning: true,
strict: false,
toJSON: {
getters: true,
virtuals: true,
},
};
export const LeadSchema = new mongoose.Schema(
{
status: {
type: Schema.Types.String,
},
customer: {
type: Schema.Types.ObjectId,
ref: Customer.modelName
},
assigned_sales_agent: {
type: Schema.Types.String,
ref: Agent.modelName
},
moving_company: {
type: Schema.Types.ObjectId,
ref:Company.modelName
},
selected_categories: [
{
type: SelectedCategorySchema,
},
],
},
schemaOptions
);
const Lead = mongoose.model<ILead>("Lead", LeadSchema);
export default Lead;
This is my customer model
export const customerSchema = new mongoose.Schema(
{
address: {
type: Schema.Types.ObjectId,
ref: Addresses,
},
},
UserSchemaOptions
);
export const Customer = User.discriminator<ICustomer>(
"Customer",
customerSchema,
Role.CUSTOMER
);
export default Customer;

Related

MongooseJS: .populate() does not return fields from the parent model

There are currently 2 models Product and Books; where Book inherits from Product as shown below:
const ProductSchema = new mongoose.Schema(
{
name: {...},
description: {...},
images: [{... }],
inventory: { ... },
department: { ... },
....
},
{
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true },
discriminatorKey: "kind",
}
)
Model = mongoose.model("Product", productSchema)
const BookSchema = new mongoose.Schema({
subtitle: { ... },
abstract: { ... },
publisher: { ... },
authors: { ... },
...
},
{
timestamps: true, discriminatorKey: "kind",
toJSON: { virtuals: true },
toObject: { virtuals: true }
}
)
Book = Product.discriminator("Book", BookSchema)
Additionally, there is a Cart schema, which has a subdocument `products` that includes a referenced field `bookID` as shown below:
const cartItem = new mongoose.Schema({
productID: {
type: mongoose.Types.ObjectId,
ref: "Product",
required: [true, "Please provide productID. "]
},
quantity: { ... },
sessionID: { ... },
})
const cartSchema = new mongoose.Schema({
products: [cartItem],
active: {
type: Boolean,
default: true,
hide: true,
},
sessionID: {
type: mongoose.Types.ObjectId,
ref: "Session"
}
}, {
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true },
})
Cart = mongoose.model("Cart", cartSchema)
I am using mongoosejs v-6.8.3
The issue is that .populate() on Cart instances returns only the fields from Book model (without including the fields from Product model).
newCart = new Cart({...})
newCart.save()
let populatedCart = await newCart.populate({ path: "products.productID", model: Product})
Your problem is probably a result of how Mongoose's discriminator feature functions. Mongoose will build a new collection for the child model and store its documents in that collection when you use the discriminator method to generate a new model that inherits from an existing model. The only collection that is queried when populate is run on the Cart instances, which only populates the fields from the Book model.
One potential fix for this problem is to explicitly include the fields from the parent model in the child model's schema. In this manner, the fields from the Product model will be included when fill is executed on the Cart instances. Another alternative is to execute the fill method with the choose option selected to include the desired fields from the parent model.
Here's an example:
const ProductSchema = new mongoose.Schema({
name: {...},
description: {...},
images: [{... }],
inventory: { ... },
department: { ... },
....
}, {
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true },
discriminatorKey: "kind"
});
const Model = mongoose.model("Product", productSchema);
const BookSchema = new mongoose.Schema({
subtitle: { ... },
abstract: { ... },
publisher: { ... },
authors: { ... },
...
}, {
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true },
discriminatorKey: "kind",
});
const Book = Model.discriminator("Book", BookSchema);
const cartItem = new mongoose.Schema({
productID: {
type: mongoose.Types.ObjectId,
ref: "Product",
required: [true, "Please provide productID. "]
},
quantity: { ... },
sessionID: { ... },
});
const cartSchema = new mongoose.Schema({
products: [cartItem],
active: {
type: Boolean,
default: true,
},
sessionID: {
type: mongoose.Types.ObjectId,
ref: "Session"
}
}, {
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true },
});
const Cart = mongoose.model("Cart", cartSchema);
let newCart = new Cart({...});
await newCart.save();
let populatedCart = await Cart.findById(newCart._id).populate({ path: "products.productID", model: "Product", select: '-__v' });
console.log(populatedCart);

Joining two mongoose collections with many to many relationship

I have two models which are Product & Category with many to many relationship.
Following are my models
This is the model for Product
//Product.js
const mongoose = require("mongoose");
const { Schema } = mongoose;
const { ObjectId } = Schema;
const product = {
name: {
type: String,
required: true,
trim: true,
minlength: [2, "Too short"],
maxlength: [32, "Too long"],
unique: true,
},
slug: {
type: String,
unique: true,
lowercase: true,
index: true,
},
category: [
{
type: ObjectId,
ref: "Category",
},
],
description: {
type: String,
maxlength: 200,
},
price: {
type: Number,
required: true,
trim: true,
maxlength: 32,
},
shipping: {
type: String,
enum: ["Yes", "No"],
},
color: [
{
type: String,
enum: ["Black", "Brown", "Silver", "White", "Blue"],
},
],
sold: {
type: Number,
default: 0,
},
quantity: {
type: Number,
},
images: [
{
public_id: String,
url: String,
},
],
};
const productSchema = new Schema(product, { timestamps: true });
const Product = mongoose.model("Product", productSchema);
module.exports = Product;
This is the model for Category
//Category.js
const mongoose = require("mongoose");
const { Schema } = mongoose;
const { ObjectId } = Schema;
const category = {
name: {
type: String,
required: true,
trim: true,
max: 32,
unique: true,
},
subCategories: [
{
type: Schema.Types.ObjectId,
ref: "Category",
},
],
parent: {
type: Schema.Types.ObjectId,
ref: "Category",
},
products: [
{
type: ObjectId,
ref: "Product",
},
],
slug: {
type: String,
required: "URL can't be empty",
unique: true,
},
};
const categorySchema = new Schema(category, { timestamps: true });
//Validate the slug to ensure proper url is given
// categorySchema.path("slug").validate((val) => {
// urlRegex =
// /(ftp|http|https):\/\/(\w+:{0,1}\w*#)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%#!\-/]))?/;
// return urlRegex.test(val);
// }, "Invalid URL.");
const autoPopulateChildren = function (next) {
this.populate("subCategories");
// this.populate("parent");
next();
};
categorySchema
.pre("findOne", autoPopulateChildren)
.pre("findById", autoPopulateChildren)
.pre("find", autoPopulateChildren);
const Category = mongoose.model("Category", categorySchema);
module.exports = Category;
So, product has a foreign keys of category in array of category. Whereas Category has foreign keys of product in array of products. How do you join them ?
I tried this & it doesn't work. It returns empty object
router.get("/products", [RequireSignIn], async (req, res) => {
try {
const products = await Product.find({}).aggregate({
$lookup: {
from: "Category",
localField: "category",
foreignField: "_id",
as: "ProductCategories",
},
});
return res.status(200).send(products);
} catch (error) {
return res.status(200).send(error);
}
});

filter by reference in mongoose node.jhs

I am having the following scenario and I need to understand better how population works: I have 3 schema as below. I need to filter child schema based on grandparent property name. I created a populate on pre find but looks like the populate does not help to filter based on property but only on object ID.
The controller code is like below:
filter= {childname: req.params.childname, 'child.parent.grandparent.grandparentname': req.params.grandparent};
const result = await Child.find(filter)
const childSchema = new mongoose.Schema(
{
childname: {
type: String,
},
parent:{
type:mongoose.Schema.ObjectId,
ref: 'Parent',
required: [true, "Parent missing"],
// select: false
}
},
{ timestamps: true },
{
toJSON: { virtuals: true },
toObject: { virtuals: true }
}
);
childSchema.pre(/^find/, function(next) {
this.populate
(
{path: 'parent',select: '-__v',
populate: [
{ path: 'grandparent', select: 'grandparentname' }
]
});
next();
});
const Child = mongoose.model( 'Child', childSchema);
module.exports = Child;
const parentSchema = new mongoose.Schema(
{
parentname: {
type: String,
},
grandparent:{
type:mongoose.Schema.ObjectId,
ref: 'grandparent',
required: [true, "Grand parent missing"],
// select: false
},
},
{ timestamps: true },
{
toJSON: { virtuals: true },
toObject: { virtuals: true }
}
);
const Parent = mongoose.model( 'Parent', parentSchema);
module.exports = Parent;
const grandparentSchema = new mongoose.Schema(
{
grandparentname: {
type: String,
}
},
{ timestamps: true },
{
toJSON: { virtuals: true },
toObject: { virtuals: true }
}
);
const Grandparent = mongoose.model( 'Grandparent', grandparentSchema);
module.exports = Grandparent;
I have the answer!
Firstly - remove your Childschema.pre('^/find...
and use next code:
const result = await Child.aggregate([{
$match: {
childname: YOUR_CHILDNAME_VARIABLE,
}
}, {
$lookup: {
from: 'parents',
localField: 'parent',
foreignField: '_id',
as: 'parent',
pipeline: [
{
"$lookup": {
"from": "grandparents",
localField: "grandparent",
foreignField: "_id",
"as": "grandparent"
},
}, {
$unwind: '$grandparent'
},
]
}
}, {
$unwind: '$parent'
}, {
"$match": {"parent.grandparent.grandparentname": YOUR_GRANDPARENTNAME_VAIRABLE}
}]);
I tried to do it using populate but haven't had a result :(
so, I'v found the solution with aggregate, it works!

mongoose middleware, show length of array in model

I want to get the size of array in the model:
const UserSchema = mongoose.Schema(
{
username: { type: String, lowercase: true, required: true },
items: { type: [mongoose.Types.ObjectId], default: [] },
},
{
toJSON : {
virtuals : true
},
timestamps: true,
}
);
UserSchema.virtual("itemsCount").get(function () {
return this.items.length;
});
module.exports = {
UserModel: mongoose.model("user", UserSchema ),
};
const ProductSchema = mongoose.Schema(
{
name: { type: String, required: true },
owner: { type: mongoose.Types.ObjectId,required: true, ref:"user"
},
},
{
toJSON : {
virtuals : true
},
timestamps: true,
}
);
module.exports = {
ProductModel: mongoose.model("product", ProductSchema ),
};
But I want to hide items in the output whenever I try to use projection it gives an error:
Cannot read properties of undefined (reading 'length')
const newPosts = await ProductModel.find({}).populate([{ path: "owner", select: { itemsCount: 0 }}]);
if I don't use select it works:
const newPosts = await ProductModel.find({}).populate([{ path: "owner" }}]);
But I don't want to show items filed in output
You can use aggregation pipeline for this:
ProductModel.aggregate([
{
"$lookup": {
"from": "users",
"localField": "user",
"foreignField": "_id",
"as": "users"
}
},
{
"$unwind": "$users"
},
{
"$project": {
_id: "$_id",
name: "$name",
"users": {
itemsCount: {
$size: "$users.items"
}
}
}
}
])
Read more about $lookup, $unwind, $project to understand.
Here is the Mongodb playground to see the results: https://mongoplayground.net/p/R0ZQiV8I-YM

Graphql query returns null id in mongodb aggregation

Graphql query returns id null (in frontend) after mongodb aggregation, but if I print a console.log in the resolvers aggregation, _id is not null. I don't know why in the backend I have a id and in front is null. Anybody can help me? Thanks
Client Schema
const ClientSchema = mongoose.Schema({
surname: {
type: String,
require: true,
trim: true
},
company: {
type: String,
require: true,
trim: true
},
salesman: {
type: mongoose.Schema.Types.ObjectId,
require: true,
ref: 'User'
}
})
module.exports = mongoose.model('Client', ClientSchema)
Order Schema
const OrderSchema = mongoose.Schema({
order: {
type: Array,
require: true,
},
total: {
type: Number,
require: true,
},
client: {
type: mongoose.Schema.Types.ObjectId,
require: true,
ref: 'Client'
},
salesman: {
type: mongoose.Schema.Types.ObjectId,
require: true,
ref: 'User'
},
stage: {
type: String,
default: "PENDENT"
},
})
module.exports = mongoose.model('Order', OrderSchema)
Resolver
getOrdersBySalesman: async (_, { }, ctx) => {
const mongoObjectId = mongoose.Types.ObjectId(ctx.user.id);
try {
const orders = await Order.aggregate([
{ $match: { salesman: mongoObjectId }},
{ $lookup: {
localField: "client",
foreignField: "_id",
from: "clients",
as: "client"
}},
{ $unwind: '$client' },
{ $sort: { 'client.company': 1 }}
]);
console.log(orders);
return orders;
} catch (error) {
console.log(error);
}
}
Graphql query
const GET_ORDERS = gql`
query getOrdersBySalesman {
getOrdersBySalesman {
id
order{
name
quantity
}
total
client {
id
surname
company
}
stage
}
}`
Result console.log resolver _id is fine
{ _id: 5ee0da703b683e071c0adfe2,
order: [ [Object] ],
stage: 'PENDENT',
total: 6166.65,
client:
{ _id: 5ea813b3085b4417b8627557,
surname: 'NOU2',
company: 'NN',
salesman: 5ea568905d47ed2760e8d11c,
__v: 0 },
salesman: 5ea568905d47ed2760e8d11c,
__v: 0 } ]
Result in frontend after graphql query id: null
{ id: null,
order: [{…}]
stage: 'PENDENT',
total: 6166.65,
client:
{ id: null,
surname: 'NOU2',
company: 'NN',
}
}
Use type ID for your id in GraphQL Schema as long as you are returning ObjectID and it should work just fine.
type Order {
id: ID
...
}

Resources