Is it possible to use a variable object inside collection.find() in mongoose? - mongodb

Context:
I am trying to code the back end of an "Advanced Search" option
It is a project I'm coding to practice JS, MongoDB(Mongoose), Next, Node, Etc.
The Schema I'm using is the following:
House Schema:{
name:String,
address:{
street:String
city:String,
state:String
},
operations:{
rent:Boolean,
shortRent:Boolean,
purchase:Boolean,
agentRentPrice:Number,
agentShortRentPrice:Number,
agentSellingPrice:Number},
features:{
bathrooms:Number,
bedrooms:Number,
beds:Number,
amenities:[String],
},
}
Now, the FrontEnd sends the following info (in req.body):
query = {
name: null,
address: { city: null, state: null },
operations: {
rentAvailable: false,
purchaseAvailable: false,
shortRentAvailable: false,
agentRentPriceMin: null,
agentSellingPriceMin: null,
agentShortRentPriceMin: null,
agentRentPriceMax: null,
agentSellingPriceMax: null,
agentShortRentPriceMax: null,
},
features: {
bathroomsMin: null,
dormsMin: null,
bedsMin: null,
amenities: null,
},
}
Of course, those "null" values will be replaced with numbers or strings with the parameters introduced by the user.
With this object, I then declare another:
let queryObj = {
name: query.name,
address: { city: query.address.city, state: query.address.state },
operations: {
rentAvailable: query.operations.rentAvailable,
purchaseAvailable: query.operations.purchaseAvailable,
shortRentAvailable: query.operations.shortRentAvailable,
agentRentPrice: {
$gte: query.operations.agentRentPriceMin,
$lte: query.operations.agentRentPriceMax,
},
agentSellingPrice: {
$gte: query.operations.agentSellingPriceMin,
$lte: query.operations.agentSellingPriceMax,
},
agentShortRentPrice: {
$gte: query.operations.agentShortRentPriceMin,
$lte: query.operations.agentShortRentPriceMax,
},
},
features: {
bathrooms: { $gte: query.features.bathroomsMin },
dorms: { $gte: query.features.dormsMin },
beds: { $gte: query.features.bedsMin },
amenities: { $in: query.features.amenities },
},
}
Finally I reduce this object, removing any "null", "false", "" and "{}" values.
For example, the user searches for: house available for renting, in New York, with 2 bedrooms, with a pool, and max Rent price of $10.000
So, req.body.query will be
{
name: null,
address: { city: "New York", state: null },
operations: {
rentAvailable: true,
purchaseAvailable: false,
shortRentAvailable: false,
agentRentPriceMin: null,
agentSellingPriceMin: null,
agentShortRentPriceMin: null,
agentRentPriceMax: 10000,
agentSellingPriceMax: null,
agentShortRentPriceMax: null,
},
features: {
bathroomsMin: null,
dormsMin: 2,
bedsMin: null,
amenities: ["pool"],
},
}
Next, I declare
let queryObj = {
name: null,
address: { city: "New York", state: null },
operations: {
rentAvailable: true,
purchaseAvailable: false,
shortRentAvailable: false,
agentRentPrice: {
$gte: null,
$lte: 10000,
},
agentSellingPrice: {
$gte: null,
$lte: null,
},
agentShortRentPrice: {
$gte: null,
$lte: null,
},
},
features: {
bathrooms: { $gte: null },
dorms: { $gte: 2 },
beds: { $gte: null },
amenities: { $in: ["pool"] },
},
};
I have a function here that reduces this object (removing "false" "null" "" and "{}" values):
queryObj = {
address: { city: "New York" },
operations: {
rentAvailable: true,
agentRentPrice: {
$lte: 10000,
},
},
features: {
dorms: { $gte: 2 },
amenities: { $in: ["pool"] },
},
};
As you can see, "query" (and therefore "queryObj") will vary a lot; the user may or may not use any of the available search parameters, so (as i see it) it is not possible to "hard-code" the queryObj structure
I've tried using
Home.aggregate([{$match:queryObj}])
without success (returns no results).
Is it possible what I'm trying to do?

Try to use a function to build the filter:
const body = {
name: null,
address: { city: 'New York', state: null },
operations: {
rentAvailable: true,
purchaseAvailable: false,
shortRentAvailable: false,
agentRentPriceMin: null,
agentSellingPriceMin: null,
agentShortRentPriceMin: null,
agentRentPriceMax: 10000,
agentSellingPriceMax: null,
agentShortRentPriceMax: null,
},
features: {
bathroomsMin: null,
dormsMin: 2,
bedsMin: null,
amenities: ['pool'],
},
};
const buildFilter = (body) => {
const filter = {};
if (body.name != null) {
filter['name'] = body.name;
}
if (body.address.city != null) {
filter['address.city'] = body.address.city;
}
if (body.address.state != null) {
filter['address.state'] = body.address.state;
}
if (body.operations.rentAvailable != null) {
filter['operations.rentAvailable'] = body.operations.rentAvailable;
}
if (body.operations.purchaseAvailable != null) {
filter['operations.purchaseAvailable'] = body.operations.purchaseAvailable;
}
if (body.operations.shortRentAvailable != null) {
filter['operations.shortRentAvailable'] =
body.operations.shortRentAvailable;
}
let agentRentPrice = {};
if (body.operations.agentRentPriceMin != null) {
agentRentPrice = { $gte: body.operations.agentRentPriceMin };
}
if (body.operations.agentRentPriceMax != null) {
agentRentPrice = {
...agentRentPrice,
$lte: body.operations.agentRentPriceMax,
};
}
if (Object.keys(agentRentPrice).length > 0) {
filter['operations.agentRentPrice'] = agentRentPrice;
}
let agentSellingPrice = {};
if (body.operations.agentSellingPriceMin != null) {
agentSellingPrice = { $gte: body.operations.agentSellingPriceMin };
}
if (body.operations.agentSellingPriceMax != null) {
agentSellingPrice = {
...agentSellingPrice,
$lte: body.operations.agentSellingPriceMax,
};
}
if (Object.keys(agentSellingPrice).length > 0) {
filter['operations.agentSellingPrice'] = agentSellingPrice;
}
let agentShortRentPrice = {};
if (body.operations.agentShortRentPriceMin != null) {
agentShortRentPrice = { $gte: body.operations.agentShortRentPriceMin };
}
if (body.operations.agentShortRentPriceMax != null) {
agentShortRentPrice = {
...agentShortRentPrice,
$lte: body.operations.agentShortRentPriceMax,
};
}
if (Object.keys(agentShortRentPrice).length > 0) {
filter['operations.agentShortRentPrice'] = agentShortRentPrice;
}
if (body.features.bathrooms != null) {
filter['features.bathrooms'] = { $gte: body.features.bathrooms };
}
if (body.features.dorms != null) {
filter['features.dorms'] = { $gte: body.features.dorms };
}
if (body.features.beds != null) {
filter['features.beds'] = { $gte: body.features.beds };
}
if (body.features.amenities != null) {
filter['features.amenities'] = { $in: body.features.amenities };
}
return filter;
};
console.log(buildFilter(body));
A little verbose, but it is due to the fact that you don't have a 1-on-1 correspondence between the attributes of the body and your filter's properties.

You can solve this by creating the filter dynamically on the backend. Basically, you will start with an empty filter, and then you will check each user input, and add it to the filter if exists. That way, you don't have to worry if the user will send multiple filters, or if it will not send filters at all. Everything will be added dynamically.
const queryObj = {
address: { city: "New York" },
operations: {
rentAvailable: true,
agentRentPrice: {
$lte: 10000,
},
},
features: {
dorms: { $gte: 2 },
amenities: { $in: ["pool"] },
},
};
// Initialize empty filter.
const filter = {};
if (queryObj?.address?.city) filter['address.city'] = queryObj.address.city;
if (typeof queryObj?.operations?.rentAvailable === boolean) filter['operations.rentAvailable'] = queryObj.operations.rentAvailable;
if (queryObj?.operations?.agentRentPrice?.$lte) filter['operations.agentRentPrice'] = { $lte: queryObj.operations.agentRentPrice.$lte };

Related

MongoDB update a specific nested field

Hi I am trying to update nested filed , but couldn't able to do so.
Here is the sample data,
[{
"_id": {
"$oid": "632ec4128f567511dcd80ed9"
},
"company_id": 1,
"contact_id": 1001,
"roles_to_be_accepted": {
"ROLE#04": {
"assigned_data": {
"assigned_3HFui": {
"is_idle": false,
"send_for_acceptance_date": 1664009233,
"action_date": ""
},
"assigned_b1J9t": {
"is_idle": false,
"send_for_acceptance_date": 1664009233,
"action_date": ""
}
}
},
"ROLE#02": {
"assigned_data": {
"assigned_uPJI1": {
"is_idle": false,
"send_for_acceptance_date": 1664009233,
"action_date": ""
}
}
}
}
}]
Now I want to update that is_idle field to true. I have tried in this way
let query = { contact_id: 1, company_id: 1001};
const db = this.client.db("dbname");
const col= db.collection("collection_name");
col.update(query, {
'$set': { "roles_to_be_accepted.assigned_data.is_idle": true }
});

Mongoose: Filtering documents by date range returns 0 documents

I have this model:
const HistorySchema = new Schema({
user: {
type: Schema.Types.ObjectId,
ref: "users",
},
category: {
type: String,
enum: category_enum,
required: true,
},
date: {
type: Date,
default: Date.now,
},
});
So a document could be:
{
"_id": "60ddad447b0e9d3c4d4fd1f1",
"user": "60dc8118118ea36a4f3cab7d",
"category": "LOGIN",
"date": "2021-03-02T00:00:00.000Z",
"__v": 0
},
I am trying to get events that happen in a given year with this:
const getEventsOfYear = async (year) => {
let response = {
events_of_year_per_user: null,
};
const start_date_of_the_year = moment(year);
const end_date_of_the_year = moment(year).endOf("year");
const filter_stage = {
$match: {
date: {
$gte: start_date_of_the_year,
$lte: end_date_of_the_year,
},
},
};
const pipeline = [filter_stage];
const history_events_with_aggregate = await History.aggregate(pipeline);
response.events_of_year_per_user = history_events_with_aggregate;
return response;
};
The problem is that this always returns an empty array:
{
"events_of_year_per_user": []
}
Any idea what I'm doing wrong?
EDIT 1:
I even tried with another model and direct date input instead of using moment and it's still the same result:
const filter_stage = {
$match: {
date: {
$gte: "2022-01-01",
$lte: "2022-12-30",
},
},
};
const pipeline = [filter_stage];
const history_events_with_aggregate = await userModel.aggregate(pipeline);
But, using find works:
const history_events_with_aggregate = await userModel.find({
date: { $gte: "2022-01-01", $lte: "2022-12-30" },
});
This is how I solved the issue:
const filter_stage = {
$match: {
date: {
$gte: new Date("2022-01-01"),
$lte: new Date("2022-12-30"),
},
},
};
And if you want to use moment:
const start_date_of_the_year = moment(year);
const end_date_of_the_year = moment(year).endOf("year");
const filter_stage = {
$match: {
date: {
$gte: new Date(start_date_of_the_year),
$lte: new Date(end_date_of_the_year),
},
},
};
You can also do this:
const today = moment().startOf("day");
const filter_stage = {
$match: {
user: ObjectId(user_id),
date: {
$gte: today.toDate(),
$lte: moment(today).endOf("day").toDate(),
},
},
};

Sequelize nested include without null properties

using ExpressJs/Postgres/Sequelize and when doing findOne I am getting an object return that contains nested objects with following data in the response:
"CompanyVolume": null,
"CompanyMarkets": [],
Is there a way to avoid receiving these properties with null/[] from Postgres?
Appreciate any thoughts...:)
const kyc = await db.Kyc.findOne({
where: {id: req.params.id},
include: [
{model: db.Company,
include: [
{ model: db.CompanyStakeholder, through: db.Role },
{ model: db.CompanyDelivery,
// where: {
// deliveryAverageDurationAmount: {[Op.not]:null},
// }
//returns Cannot read properties of undefined (reading 'not')
},
{ model: db.CompanyVolume },
{ model: db.CompanyDelivery },
{ model: db.CompanyMarket },
],
nest: true,
})
the response object with properties including null or empty arrays
[
{
"id": 5,
"kycTitle": "KYC1006428",
"kycPasscode": null,
"kycDescription": "The best kyc ever, lorem epsum sanctus vita loca",
"kycStartDate": "2022-01-28",
"kycStatus": null,
"kycConsent": "approved",
"kycConsentDate": "2022-01-27T14:01:39.924Z",
"createdAt": "2022-01-27T13:19:41.649Z",
"updatedAt": "2022-01-27T14:01:39.919Z",
"id_User": 1,
"Company": {
"id": 6,
"companyName": "Niky",
"companyEntityType": null,
"companyVatNumber": "",
"companyRegistrationNumber": "",
"companyPubliclyListed": null,
"companyAddress1": "Pepovej 234",
"companyAddress2": "",
"companyCity": "Dada",
"companyPostCode": "3400",
"companyCountry": "Owner",
"companyPhone": "+21321321231",
"createdAt": "2022-01-27T13:19:41.669Z",
"updatedAt": "2022-01-27T13:19:41.676Z",
"KycId": 5,
"CompanyStakeholders": [], //I don't want this returned
"CompanyDelivery": null, //I don't want this returned
"CompanyVolume": null, //I don't want this returned
"CompanyMarkets": [], //I don't want this returned
},
}
]
The different Associations:
Company.associate = function (models) {
Company.hasOne(models.Contact)
Company.hasOne(models.CompanyDelivery)
Company.hasMany(models.CompanyMarket)
Company.hasMany(models.CompanyPosBilling)
Company.hasMany(models.CompanySettlement)
Company.hasOne(models.CompanySubscription)
Company.hasOne(models.CompanyVolume)
Company.hasMany(models.CompanyWebsite)
Company.belongsTo(models.Kyc)
Company.belongsToMany(models.CompanyStakeholder, {
through: models.Role,
unique: false
})
}
CompanyDelivery.associate = function (models) {
CompanyDelivery.belongsTo(models.Company)
}
CompanyMarket.associate = function (models) {
CompanyMarket.belongsTo(models.Company)
}
Kyc.associate = function (models) {
Kyc.hasOne(models.Company)
}
CompanyStakeholder.associate = function (models) {
CompanyStakeholder.belongsToMany(models.Company, {
through: models.Role,
unique: false
})
}

Statistics with Mongo DB

I have the following DB structure :
{
"uploadedAt": "2021-09-22T22:09:12.133Z",
"paidAt: "2021-09-30T22:09:12.133Z",
"amount": {
"currency": "EUR",
"expected": 70253,
"paid": 0
},
}
I would like to know how do I calculate the total amount that still need to be paid (expected - paid), and the average date between uploadedAt and paidAt. This for multiple records.
My function for getting the data is (the criteria should be updated to get this data).
const invoiceParams = new FindParams();
invoiceParams.criteria = { company: company._id }
const invoices = await this.findAll(invoiceParams);
FindAll function looks like:
async findAll(
params: FindParams,
ability?: Ability,
includeDeleted: boolean = false,
): Promise<Entity[]> {
let queryCriteria: Criteria = params.criteria;
let query: DocumentQuery<Entity[], Entity> = null;
if (!includeDeleted) {
queryCriteria = {
...queryCriteria,
deleted: { $ne: true },
};
}
try {
if (ability) {
ability.throwUnlessCan('read', this.entityModel.modelName);
queryCriteria = {
...toMongoQuery(ability, this.entityModel.modelName),
...queryCriteria,
};
}
query = this.entityModel.find(queryCriteria);
if (params.populate) {
query = query.populate(params.populate);
}
if (params.sort) {
query = query.sort(params.sort);
}
if (params.select) {
query = query.select(params.select);
}
return query.exec();
} catch (error) {
if (error instanceof ForbiddenError) {
throw new ForbiddenException(error.message);
}
throw error;
}
}
Update:
const paymentTime = await this.invoiceModel.aggregate([
{
$group: {
_id: "$account",
averageSpread: { $avg: { $subtract: ["$paidAt", "$uploadedAt"] } },
count: { $sum: 1 }
}
}
]);
Try this aggregation pipeline:
db.invoiceParams.aggregate([
{
$set: {
expectedPaid: { $subtract: ["$amount.expected", "$amount.paid"] },
averageDate: { $toDate: { $avg: [{ $toLong: "$uploadedAt" }, { $toLong: "$paidAt" }] } }
}
}
])

How to calculate ratios for an additive attribute with mongodb?

Using the sample mongodb aggregation collection (http://media.mongodb.org/zips.json), I would like to output the population share of every city in California.
In SQL, it could look like this:
SELECT city, population/SUM(population) as poppct
FROM (
SELECT city, SUM(population) as population
FROM zipcodes
WHERE state='CA'
GROUP BY city
) agg group by state;
This can be done using mongodb map/reduce:
db.runCommand({
mapreduce : "zipcodes"
, out : { inline : 1}
, query : {state: "CA"}
, map : function() {
emit(this.city, this.pop);
cache.totalpop = cache.totalpop || 0;
cache.totalpop += this.pop;
}
, reduce : function(key, values) {
var pop = 0;
values.forEach(function(value) {
if (value && typeof value == 'number' && value > 0) pop += value;
});
return pop;
}
, finalize: function(key, reduced) {
return reduced/cache.totalpop;
}
, scope: { cache: { } }
});
Can this be also achieved using the new aggregation framework (v2.2)? This would require some form of global scope, as in the map/reduce case.
Thanks.
Is this what you're after?
db.zipcodes.remove();
db.zipcodes.insert([
{ city:"birmingham", population:1500000, state:"AL" },
{ city:"London", population:10000, state:"ON" },
{ city:"New York", population:1000, state:"NY" },
{ city:"Denver", population:100, state:"CO" },
{ city:"Los Angeles", population:1000000, state:"CA" },
{ city:"San Francisco", population:2000000, state:"CA" },
]);
db.zipcodes.runCommand("aggregate", { pipeline: [
{ $match: { state: "CA" } }, // WHERE state='CA'
{ $group: {
_id: "$city", // GROUP BY city
population: { $sum: "$population" }, // SUM(population) as population
}},
]});
produces
{
"result" : [
{
"_id" : "San Francisco",
"population" : 2000000
},
{
"_id" : "Los Angeles",
"population" : 1000000
}
],
"ok" : 1
}
you could try:
db.zipcodes.group( { key: { state:1 } ,
reduce: function(curr, result) {
result.total += curr.pop;
result.city.push( { _id: curr.city, pop: curr.pop } ); },
initial: { total: 0, city:[] },
finalize: function (result) {
for (var idx in result.city ) {
result.city[idx].ratio = result.city[idx].pop/result.total;
}
} } )