How to access a virtual attribute from within another virtual using Mongoose - node.js

I have an Invoice model that uses virtual attributes to compute values for tax, subtotal, total etc. The problem I have is that some of the virtual attributes need to be able to reference other virtual attributes.
For example, here is the Mongoose schema for the Invoice:
var InvoiceSchema = Schema({
number: String,
customer: {ref:String, email:String},
invoiceDate: {type: Date, default: Date.now},
dueDate: {type: Date, default: Date.now},
memo: String,
message: String,
taxRate: {type:Number, default:0},
discount: {
value: {type:Number, default:0},
percent: {type:Number, default:0}
},
items: [ItemSchema],
payment: {type: Schema.Types.ObjectId, ref: 'Payment'}
});
InvoiceSchema.virtual('tax').get(function(){
var tax = 0;
for (var ndx=0; ndx<this.items.length; ndx++) {
var item = this.items[ndx];
tax += (item.taxed)? item.amount * this.taxRate : 0;
}
return tax;
});
InvoiceSchema.virtual('subtotal').get(function(){
var amount = 0;
for (var ndx=0; ndx<this.items.length; ndx++) {
amount += this.items[ndx].amount;
}
return amount;
});
InvoiceSchema.virtual('total').get(function(){
return this.amount + this.tax;
});
InvoiceSchema.set('toJSON', { getters: true, virtuals: true });
var ItemSchema = Schema({
product: String,
description: String,
quantity: {type: Number, default: 1},
rate: Number,
taxed: {type: Boolean, default: false},
category: String
});
ItemSchema.virtual('amount').get(function(){
return this.rate * this.quantity;
});
ItemSchema.set('toJSON', { getters: true, virtuals: true });
module.exports = mongoose.model('Invoice', InvoiceSchema);
Now to understand the issue take a look at the virtual definition for 'tax' ...
InvoiceSchema.virtual('tax').get(function(){
var tax = 0;
for (var ndx=0; ndx<this.items.length; ndx++) {
var item = this.items[ndx];
tax += (item.taxed)? item.amount * this.taxRate : 0;
}
return tax;
});
... in this example item.amount, when called inside a virtual, doesn't use the virtual getter for item.amount.
Is there some way to tell Mongoose that I need to use the getter instead of trying to read a property that doesn't exist?

Did you try item.get('amount')?
That seems to be the explicit way of using virtuals.
Got it from this Issue:
https://github.com/Automattic/mongoose/issues/2326
Didn't find anything else related unfortunately.

Related

mongo db poor performance while doing text search with number

I have a collection called "torrents" with about 3.2M Documents. which basically contains public torrent metadata.
Schema:
let mongoose = require('mongoose');
let TorrentSchema = mongoose.Schema({
infohash: {type: String, index: true, unique: true},
title: {type: String, index: true},
category: {type: String, default: "Unknown", index: true},
size: {type: Number, default: 0},
trackers: [{
downloads: {type: Number},
peers: {type: Number},
seeds: {type: Number},
tracker: {type: String}
}],
files: [{path: String, length: Number}],
swarm: {
seeders: {type: Number, default: 0, index: -1},
leechers: {type: Number, default: 0}
},
imported: {type: Date, default: Date.now, index: true},
lastmod: {type: Date, default: Date.now, index: true}
});
TorrentSchema.virtual('getFiles').set(function () {
return this.map(res => {
if (typeof res === "string") return [{path: res, length: 0}];
return res;
})
});
TorrentSchema.virtual('downloads').get(function () {
let downloads = 0;
for (let download of this.trackers) {
downloads += download.downloads
}
return downloads;
});
TorrentSchema.index({title: 'text'}, { language_override: 'none' });
module.exports = mongoose.model('Torrent', TorrentSchema);
Now that the problem is when I am doing a text search with a keyword which also contents number(s) the search query taking a long time to execute.
let q = req.query.q;
q = q.split(/[\s]/).filter(n => n).map( str => `"${str}"`).join(" ");
let PERPAGE = 20;
let query = {$text: {$search: q}};
// Tottent Is the same schema as above.
let search = await Torrent.find(query).sort({"swarm.seeders" : -1}).select("-files").limit(PERPAGE).skip(skip);
now the issue is. when searching with letters like "ubuntu" it going really fast. but the problem arises when searching with a string which also contains numbers. Like "ubuntu 18", strings with no numbers like "ubuntu iso" taking nowhere near the time. the same thing happening when searching some other keywords like "somevideo 1080p", "somemovie 2" etc.
Do you have any fix for this issue?
It seems, after querying with same keyword couple of times, the query speed seems increased significantly.

find(...).populate is not a function in mongoose

I am trying to populate two tables in mongoose and node and I receive the error that populate is not a function.
I have search and in the documentation it seems that it does the same as I.
Here the model:
var mongoose = require('mongoose');
var dodleSchema = new mongoose.Schema({
name: String,
productorId: {type: mongoose.Schema.ObjectId, ref: "Productor"},
description: String,
ambassadorId: {type: mongoose.Schema.ObjectId, ref: "Ambassador"},
accepted: { type: Boolean, default: false },
read: { type: Boolean, default: false },
deliveryAddress: {
lat: String,
lng: String
},
createdAt: Date,
products: [
{
primaryImage: String,
images: [],
name: String,
max: Number,
min: Number,
step: Number,
stock: Number,
normalPrice: Number,
doodlePrice: Number,
freeShipping: Boolean,
freeShippingFrom: Number
}
],
endsAt: Date,
packagingType: String,
orders: [
{
name: String,
email: String,
purchases: [
{
productId: String,
quantity: Number
}
]
}
]
});
module.exports = mongoose.model('Doodle', dodleSchema);
And then the find that I use:
router.route('/requests/:id')
.get(function (req, res) {
doodleCollection
.find({
ambassadorId: new mongodb.ObjectID(req.params.id),
read: false
})
.populate('productorId')
.toArray(function (error, results) {
if (error) {
res.json({'error': "Ha habido un error en la extracción de requests"});
return false;
}
var alertNew = false;
for (var i = 0; i < results.length; i++) {
if (results[i].read == false) {
readed = true;
break;
}
}
res.json({
requests: results,
alertNew: alertNew
});
});
});
This is the error that I get:
Error
I found the solution and was pretty easy. Seemed that I was really close yesterday.
I was doing the populate method in a collection doodleCollection and I needed to do it in the model.
Changing the object that makes the find totally worked.
Instead of doodleCollection.find(...) now I call doodleModel.find(...) and populate is working perfect!
I was using following which gives me null as data:-
let PostModel = mongoose.model('Post');
Then I changed it as following and populate method worked:-
let PostModel = require("../model/post.js"); //path to your post model
Make sure you exported Model from your post.js model file like following:-
module.exports = mongoose.model('Post', PostSchema);

Mongoose, is it possible to assign parent id to sub document on save in one call?

I am wondering if it's possible(or even needed) to provide a subdocument with a reference to it's parents id all in one call. Here is the code I'm working with:
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var LatLng = new Schema({
id: Schema.ObjectId,
created_at: Date,
accuracy: {
type: Number,
required: true
},
latitude: {
type: Number,
required: true
},
longitude: {
type: Number,
required: true
},
_walk: {
type: Schema.ObjectId,
ref: 'Walk',
required: true
}
});
LatLng.pre('save', function(next){
if(!this.created_at)
this.created_at = new Date();
});
var Walk = new Schema({
id: Schema.ObjectId,
created_at: Date,
updated_at: Date,
description: String,
elapsedTime: Number,
distance: Number,
waypoints: [LatLng],
_user: {
type: Schema.ObjectId,
ref: 'User',
required: true
}
});
Walk.pre('save', function(next){
var now = new Date();
this.updated_at = now;
if(!this.created_at)
this.created_at = now;
next();
});
Walk.pre('update', function(next){
this.updated_at = new Date();
});
Walk.pre('findOneAndUpdate', function(next){
this.updated_at = new Date();
});
Walk.pre('findByIdAndUpdate', function(next){
this.updated_at = new Date();
});
module.exports = mongoose.model('Walk', Walk);
Wondering if there's some way, maybe in LatLng.pre('save'), to assign LatLng._walk to Walk.id?
So when I do something like:
var walk = new Walk({
description: req.body.description,
elapsedTime: req.body.elapsedTime,
distance: req.body.distance,
waypoints: req.body.waypoints,
_user: req.user._id
});
It would allow me to just call walk.save() and not have to iterate through the waypoints and manually assign _walk to walk._id
I hope this makes sense, thanks for the help!
If you really do need it why not just manually create it before and save the document with it. You can generate the ObjectId yourself instead of letting mongo handle it. There is nothing against that.

Mongoose .find() is empty when querying on subdocument

I'm trying to query a document based off of its subdocument data. When I do this I get no data returned. When I remove the "where" or when I query on a primitive type field within the parent it works fine. What am I missing?
My collections are broken up into separate files, here they are together for simplicity:
var PlayerSchema = new Schema({
firstName: { type: String, default: '', required: true},
lastName: { type: String, default: '', required: true},
nickname: { type: String, default: '' },
});
module.exports = mongoose.model('Player', PlayerSchema);
var GameSchema = new Schema({
winner: { type: Schema.Types.ObjectId, ref: 'Player', required: true},
datePlayed: { type: Date, default: Date.now, required: true },
});
module.exports = mongoose.model('Game', GameSchema);
var GamePlayerSchema = new Schema({
game: { type: Schema.Types.ObjectId, ref: 'Game', required: true},
player: { type: Schema.Types.ObjectId, ref: 'Player', required: true},
points: { type: Number, default: 0 },
place: { type: Number, default: 0 },
});
module.exports = mongoose.model('GamePlayer', GamePlayerSchema);
My query:
GamePlayerModel.find()
//.where('player.firstName').equals('Brian') // returns empty
//.where(place).equals(1) // returns correct dataset
.where('game.datePlayed').gte(startDateRange).lt(endDateRange) // returns empty
.select('game player points place')
.populate('game')
.populate('player')
.exec(function (err, gamePlayers) {
if(err) return next(err);
res.json(gamePlayers);
});
So again, if I query on a subdocument in any way it returns an empty dataset. I've tried game.datePlayed and even games.datePlayed. I'm not sure what to do. I don't need the player.firstName results, however I figured that'd be an easy thing to test to make sure the query is setup correctly.
Lastly, this is how I setup the date ranges. The date objects come out correctly, but are they possibly the wrong type?
var now = new Date();
var month = req.query.month ? parseInt(req.query.month) : now.getUTCMonth();
var year = req.query.year ? parseInt(req.query.year) : now.getUTCFullYear();
var endMonth = month+1;
if(endMonth > 11) endMonth = 0;
var startDateRange = new Date(year, month, 1);
var endDateRange = new Date(year, endMonth, 1);
Joins are not supported in MongoDB, which is what you are trying to do. The closest you can get is to filter as part of your .populate call:
.populate('game', null, {datePlayed: {$gte: startDateRange, $lt: endDateRange}})
.populate('player', null, {firstName: 'Brian'})
However what this will do is get all GamePlayer documents and only get the Game and Player subdocuments that match your criteria. If the subdocuments don't match your criteria, the GamePlayer document will still be returned with .game or .player equal to null.
You may want to reconsider your schema to be less like a SQL schema and to take advantage of the benefits of MongoDB. I'm not sure what your requirements are, but consider something like this:
var PlayerSchema = new Schema({
firstName: { type: String, default: '', required: true},
lastName: { type: String, default: '', required: true},
nickname: { type: String, default: '' },
});
var GameSchema = new Schema({
winner: { type: Schema.Types.ObjectId, ref: 'Player', required: true},
datePlayed: { type: Date, default: Date.now, required: true },
players: [{
player: { type: Schema.Types.ObjectId, ref: 'Player', required: true},
points: { type: Number, default: 0 },
place: { type: Number, default: 0 }
}]
});
Then your query could look something like:
GameModel.find()
.where('players.place').equals(1) // This will limit the result set to include only Games which have at least one Player with a place of 1
.where('datePlayed').gte(startDateRange).lt(endDateRange)
.populate('players.player')
.exec(function (err, games) {
if(err) return next(err);
res.json(games);
});
The above example will include all Players in each returned Game, regardless of whether their place is 1, however it will only include Games which have at least one player with a place of 1.
If you want to limit the items in the players array, you might need to take it a step further and use an aggregate command to unwind the array then filter. For example:
GameModel.aggregate([
{$unwind: 'players'},
{$match: {'players.place' : 1}}
], function(err, results) {
if (err) return next(err);
res.json(results);
});
This will return a separate Game object for each Player with a place of 1. If a Game has more than one Player with a place of 1, it will return duplicate Game objects, each with a different Player.

How can I auto-case key names when saving to Mongoose?

I have an object:
{ SKU: 'TR1234',
Description: 'Item 1',
UoM: 'each',
client_id: '531382e3005fe0c926bd3957',
Meta: { Test: 'test1', Image: 'http://www.aol.com' } }
I'm trying to save it given my schema:
var ItemSchema = new Schema({
sku: {
type: String,
trim: true,
},
description: {
type: String,
trim: true,
},
company_id: {
type: Schema.ObjectId,
ref: 'Client',
},
createdOn: {
type: Date,
default: Date.now
},
updatedOn: {
type: Date,
default: Date.now
}
}, {versionKey: false});
But it doesn't save and I assume it's because of the capitalized key names. However, those are dynamically generated from a CSV which is parsed with https://github.com/Keyang/node-csvtojson
Ideas?
You can also just use a setter in your mongoose schema, like that:
function toLower (v) {
return v.toLowerCase();
}
var UserSchema = new Schema({
email: { type: String, set: toLower }
});
Just apply it to your fields.
There is also one more approach, just:
email : { type: String, lowercase: true }
Update for keys:
If you would like to change keys, you should the approach likes 'ecdeveloper' mentioned below. My answer was for values, so it makes sense to give this reputation to 'ecdeveloper'. Sorry for confusing.
Here is one more approach without creating a new object:
Object.prototype.keysToUpper = function () {
var k;
for (k in this) {
if (this.hasOwnProperty(k))
this[k.toLowerCase()] = this[k];
delete this[k];
}
return this;
};
What about calling toLowerCase() on each key from your object, and build a new object with lower case keys?
// Assumy your object name is obj
var newObj = {};
Object.keys(obj).forEach(function(key) {
newObj[key.toLowerCase()] = obj[key];
});
// Here you can save your newObj

Resources