How to populate reference field in virtual method in Mongoose? - node.js

I have following schema.
var ItemSchema = new Schema({
name : String
,location: {
address: { type: String, default:''},
geolocation: {longitude: Number, latitude:Number},
place : {type: Schema.Types.ObjectId, ref:'Place'}
},
ranking_in_place : Number })
Place is a reference to Place schema which has name, city, country etc. fields.
I want to create a virtual for ranking_summary:
ItemSchema.virtual('ranking_summary').get(function() {
if(this.ranking_in_place <= 5){
if(this.ranking_in_place == 1){
return "Most popular item" + " in " + this.location.place.name
}
}
})
I cannot get this.location.place.name value because this.location.place is a reference, and not populated. How can I access this value?

Have you made sure to call .populate() on your queries? Otherwise, Mongoose won't know to pull in the reference object. Example:
ItemModel.findOne().populate('place').exec(function (err, item) {
console.log(item.ranking_in_place)
})

Model.find().populate(path, fields, conditions, options);
so for options you could use
{ sort: 'order' } // ascending
{ sort: [['order', 1 ]] } // ascending
{ sort: [['order', 'asc' ]] } // ascending
{ sort: [['order', 'desc' ]] } // ascending
{ sort: [['order', -1 ]] } // descending
{ sort: [['order', 'desc' ]] } // descending
{ sort: [['order', 'descending' ]] } // descending

I think there is no direct method for doing this. One way you can achieve this is by following the hooks. [Read More]
Here is a sample code. I had a need to calculate the totalTime of tutorial which is made up of videos. So, I had to populate videos and then use their duration to calculate totalTime for a tutorial.
tutorialSchema.pre('findOne', function (next) {
this.populate('videos'); // now available as this.ratings in this schema
next();
});
tutorialSchema.virtual('totalTime').get(function () {
let times = [];
times = this.videos.map((v) => {
return v.duration;
});
if(times.length === 0) return 0;
let totalTime = times.reduce((sum, time) => {
return sum + time;
});
return totalTime;
});

Related

Mongo/Node: Filtering By Single Properties?

I am dealing with a query with a criteria object that is being passed as the first argument to this query:
module.exports = (criteria, sortProperty, offset = 0, limit = 20) => {
// write a query that will follow sort, offset, limit options only
// do not worry about criteria yet
console.log(criteria);
const query = Artist.find({ age: { $gte: 19, $lte: 44 } })
.sort({ [sortProperty]: 1 })
.skip(offset)
.limit(limit);
return Promise.all([query, Artist.count]).then(results => {
return {
all: results[0],
count: results[1],
offset: offset,
limit: limit
};
});
};
By default, the criteria object has a single name property that is an empty string.
The age property points to an object that has both min and max values assigned to it. I also have a yearsActive property inside of the criteria object and that also has a min and max value.
So three different properties: age, name and yearsActive.
This has been an extremely challenging one for me and if you look above that's as far as I got.
When my criteria property is console logged it only has a name { name: "" }. It has no yearsActive or age by default when it first starts. So that is where the point of the sliders come in. When I start moving these sliders around on the frontend, then it gets the age and yearsActive appended to the criteria object.
So I need to figure out how to update the query to consider for example the different ages and I have been considering using an if conditional inside a helper function.
Regarding to the comment that I left you.
You have three states at least one when you retrieve the data to the UI. In this case, I would recommend you use aggregation in order to retrieve the data as a model as your business.
For example, the problem as you have is that sometimes you don't know about the max or min value for age or yearsActive, but also you should have an identifier that could be an ObjectId which will be used to update the model identified by that property.
Artist.aggregate([
{
$match: { age: { $gte: 19, $lte: 44 } }
},
{
$sort: { yourProperty: 1 }
},
{
$skip: 10
},
{
$limit: 10
},
{
$project: {
// You set your properties to retrieve with the 1 as flag
propertieX: 1,
"another.property": 1,
"age.max": {
$cond: {
if: { $eq: [ "", "$age.max" ] },
then: 0, // Or the value that you want to set it
else: "$age.max"
}
}
}
}]);
The other state is when you do the query according to the parameters that you're submitting from the form.
If you assurance to retrieve a model with the logic as you want. For example you should return this model in every request using $project and applying the default values when doesn't exist the manipulation in the front-end side as in the searching should be easy to manage.
{
ObjectId: YOUR_OBJECT_ID,
age: {
min: YOUR_MIN_VALUE,
max: YOUR_MAX_VALUE
},
yearsActive: {
min: YOUR_MIN_VALUE,
max: YOUR_MAX_VALUE
}
}
Finally, when you would send the data to save it you should sent the entire model that you returned but the must important thing is identify only that element by the ObjectId to do the update.
NOTE: This is an approach that I will do according with the information that I understand from your question, If I'm bad with me interpretation let me know, and if you want to share more information or open a repository to understand in code, should more easy to me understand the problem.
So what I decided to do since the code would look messy to throw all inside the Artist.find({}) was to create a separate helper function:
const buildQuery = (criteria) => {
console.log(criteria);
};
This helper function is being called with the criteria object and I have to form up the object in such a way that it will represent the query the way in which I want to search the Artist collection.
What made this difficult to wrap my head around was the not very well formed object for searching over a collection with its random properties such as age which has a min and a max which Mongo does not know how to deal with by default. MongoDB does not know what min and max mean exactly.
So inside the helper function I made a separate object to return from this function thats going to represent the actual query that I want to send off to Mongo.
const buildQuery = (criteria) => {
console.log(criteria);
const query = {};
};
I am not modifying the object in anyway, I am just reading some of the desired search results or what the user wants to see from this UI object and so I made this object called query and I added the idea of age.
const buildQuery = (criteria) => {
console.log(criteria);
const query = {};
query.age = {};
};
I decided to do an if conditional inside of the helper function for the specific age range that I want to find.
const buildQuery = (criteria) => {
console.log(criteria);
const query = {};
if (criteria.age) {
query.age = {};
}
};
So this is where the Mongo query operators come into play. The two operators I want to be concerned with is the greater than or equal to ($gte) and the less than or equal to ($lte) operators.
This is how I actually implemented in practice:
const buildQuery = (criteria) => {
console.log(criteria);
const query = {};
if (criteria.age) {
query.age = {
$gte: criteria.age.min,
$lte: criteria.age.max
};
}
};
The query object here will eventually be returned from the buildQuery function:
const buildQuery = (criteria) => {
console.log(criteria);
const query = {};
if (criteria.age) {
query.age = {
$gte: criteria.age.min,
$lte: criteria.age.max
};
}
return query;
};
That query object will be passed off to the find operation:
module.exports = (criteria, sortProperty, offset = 0, limit = 20) => {
// write a query that will follow sort, offset, limit options only
// do not worry about criteria yet
const query = Artist.find(buildQuery(criteria))
.sort({ [sortProperty]: 1 })
.skip(offset)
.limit(limit);
return Promise.all([query, Artist.count]).then(results => {
return {
all: results[0],
count: results[1],
offset: offset,
limit: limit
};
});
};
const buildQuery = (criteria) => {
console.log(criteria);
const query = {};
if (criteria.age) {
query.age = {
$gte: criteria.age.min,
$lte: criteria.age.max
};
}
return query;
};
So what I am doing here is to get the equivalent of Artist.find({ age: { $gte: minAge, $lte: maxAge }).
So for yearsActive I decided to implement something that is nearly identical:
const buildQuery = criteria => {
console.log(criteria);
const query = {};
if (criteria.age) {
query.age = {
$gte: criteria.age.min,
$lte: criteria.age.max
};
}
if (criteria.yearsActive) {
}
return query;
};
So if the user changes the slider, I am going to expect my criteria object to have a yearsActive property defined on it like so:
const buildQuery = criteria => {
console.log(criteria);
const query = {};
if (criteria.age) {
query.age = {
$gte: criteria.age.min,
$lte: criteria.age.max
};
}
if (criteria.yearsActive) {
query.yearsActive = {
$gte: criteria.yearsActive.min,
$lte: criteria.yearsActive.max
}
}
return query;
};

MongoDB - find one and add a new property

Background: Im developing an app that shows analytics for inventory management.
It gets an office EXCEL file uploaded, and as the file uploads the app convert it to an array of JSONs. Then, it comapers each json object with the objects in the DB, change its quantity according to the XLS file, and add a timestamp to the stamps array which contain the changes in qunatity.
For example:
{"_id":"5c3f531baf4fe3182cf4f1f2",
"sku":123456,
"product_name":"Example",
"product_cost":10,
"product_price":60,
"product_quantity":100,
"Warehouse":4,
"stamps":[]
}
after the XLS upload, lets say we sold 10 units, it should look like that:
{"_id":"5c3f531baf4fe3182cf4f1f2",
"sku":123456,
"product_name":"Example",
"product_cost":10,
"product_price":60,
"product_quantity":90,
"Warehouse":4,
"stamps":[{"1548147562": -10}]
}
Right now i cant find the right commands for mongoDB to do it, Im developing in Node.js and Angular, Would love to read some ideas.
for (let i = 0; i < products.length; i++) {
ProductsDatabase.findOneAndUpdate(
{"_id": products[i]['id']},
//CHANGE QUANTITY AND ADD A STAMP
...
}
You would need two operations here. The first will be to get an array of documents from the db that match the ones in the JSON array. From the list you compare the 'product_quantity' keys and if there is a change, create a new array of objects with the product id and change in quantity.
The second operation will be an update which uses this new array with the change in quantity for each matching product.
Armed with this new array of updated product properties, it would be ideal to use a bulk update for this as looping through the list and sending
each update request to the server can be computationally costly.
Consider using the bulkWrite method which is on the model. This accepts an array of write operations and executes each of them of which a typical update operation
for your use case would have the following structure
{ updateOne :
{
"filter" : <document>,
"update" : <document>,
"upsert" : <boolean>,
"collation": <document>,
"arrayFilters": [ <filterdocument1>, ... ]
}
}
So your operations would follow this pattern:
(async () => {
let bulkOperations = []
const ids = products.map(({ id }) => id)
const matchedProducts = await ProductDatabase.find({
'_id': { '$in': ids }
}).lean().exec()
for(let product in products) {
const [matchedProduct, ...rest] = matchedProducts.filter(p => p._id === product.id)
const { _id, product_quantity } = matchedProduct
const changeInQuantity = product.product_quantity - product_quantity
if (changeInQuantity !== 0) {
const stamps = { [(new Date()).getTime()] : changeInQuantity }
bulkOperations.push({
'updateOne': {
'filter': { _id },
'update': {
'$inc': { 'product_quantity': changeInQuantity },
'$push': { stamps }
}
}
})
}
}
const bulkResult = await ProductDatabase.bulkWrite(bulkOperations)
console.log(bulkResult)
})()
You can use mongoose's findOneAndUpdate to update the existing value of a document.
"use strict";
const ids = products.map(x => x._id);
let operations = products.map(xlProductData => {
return ProductsDatabase.find({
_id: {
$in: ids
}
}).then(products => {
return products.map(productData => {
return ProductsDatabase.findOneAndUpdate({
_id: xlProductData.id // or product._id
}, {
sku: xlProductData.sku,
product_name: xlProductData.product_name,
product_cost: xlProductData.product_cost,
product_price: xlProductData.product_price,
Warehouse: xlProductData.Warehouse,
product_quantity: productData.product_quantity - xlProductData.product_quantity,
$push: {
stamps: {
[new Date().getTime()]: -1 * xlProductData.product_quantity
}
},
updated_at: new Date()
}, {
upsert: false,
returnNewDocument: true
});
});
});
});
Promise.all(operations).then(() => {
console.log('All good');
}).catch(err => {
console.log('err ', err);
});

Query top level array of subdocs with MONGOOSE, to only return N most recent subdocuments in array

I'm looking for the conditions, fields, etc that i would need in a query, or an async flow to query a single document which has an array of subdocuments at the top-level.
Model:
let postSchema = new mongoose.Schema({
date : {type: Boolean, require: true},
message : {type: String, require: true}
});
let Post = new mongoose.Schema({
locid: {type: String, unique: {indexed: true}, require: true},
posts : {type: [postSchema], require: false}
});
Essentially, I would provide a Locid value as shown above into a function that i'm hoping would look like this:
Post.methods.getLastTwentyPostsForOne = function (locid) {
return new Promise(function (values, error) {
let p = new Post();
p.find({"locid" : locid}, {/*...the conditions and elements here...*/}).exec()
.then(function (found) {
//..would return the last 20 posts, or less if there are less...
}, function (err) {
//..handle errors..
})
})
};
The most basic way i can think of doing this, would be to just fetch the whole array, and since mongo stores entries one after the other chronologically, to just parse the array and appending the last 20 or less posts contained in it into a final array (with a second promise-based function).
this makes the previous method, look like this:
function returnLastTwenty(posts) {
return new Promise(function (results) {
if (posts.length === 0) {
return results([]);
}
if (posts.length <= 20) {
return results(posts);
}
let length = posts.length;
var next = [];
for (i = length - 21; i < (length -1); i++) {
next.append(posts[i]);
}
if (next.length === 20) {
return results(next);
} else {
console.log('Some Error: found more than 20 posts, but parsed less');
return results(next)
}
});
}
Post.methods.getLastTwentyPostsForOne = function (locid) {
return new Promise(function (values, error) {
let p = new Post();
p.find({"locid" : locid})
.then(function (found) {
if (found.length === 1) {
returnLastTwenty(found[0].posts)
.then(function (results) {
return values(results)
})
} else {
return error("Didn't find a unique document for locid: " + locid);
}
}, function (err) {
return error(err)
})
})
};
While this does work.. the for loop and second async method seems too much.. Any clues as to how to achieve this with mongoose with a single query?
Update - Answer
Thanks to Ratan's comment, the following seemed to do the trick.
function getPosts(amount, asOfIndex, locid) {
return new Promise(function (posts, none) {
Post.find({'locid' : locid},
{'posts' :
{"$slice" : [asOfIndex, amount]}
}
)
.exec()
.catch(function (err) {
return none(err);
})
.then(function (found) {
if (found.length === 0 || found.length > 1) return none(); //This can be changed for more specific error handling
return posts(found[0].posts);
});
});
}
With index being an arbitrary Number passed through. Which could be an index from an API call, etc. Where asOfIndex should be negative in order to slice from the bottom of the array, and positive from the top (https://docs.mongodb.com/manual/reference/operator/projection/slice/). Slicing a given interval then just becomes a question of playing with the values of asOfIndex and amount.
Also while doing some digging, found a way to query the date element of the subdocs inside the array based on a date interval thanks to the following posts:
https://stackoverflow.com/a/29026860/7183483
and
https://stackoverflow.com/a/22287745/7183483 . For those who might want something like it!
function getPostsBetween(recentDate, farthestDate, forLocid) {
return new Promise(function (posts, none) {
Post.aggregate(
{'$match': {'locid': forLocid}},
{'$unwind' : '$posts'},
{'$match' :
{'$and': [
{'posts.date': {'$gt': new Date(farthestDate.toISOString())}},
{'posts.date': {'$lt': new Date(recentDate.toISOString())}}
]
}
},
{"$group" : {
'_id' : '$_id',
"posts" : {'$push' : '$posts'}
}
})
.exec()
.catch(function (err) {
return none(err);
})
.then(function (found) {
if (found.length === 0 || found.length > 1) return none();
return found(found[0].posts);
})
})
}

Updating nested object in mongoose

I have searched many questions on nested objects, but all I found where related to array[s].
I am looking for a updating simple nested object in mongoose.
From here http://mongoosejs.com/docs/guide.html
there is an example schema :
var blogSchema = new Schema({
title: String,
author: String,
body: String,
comments: [{ body: String, date: Date }],
date: { type: Date, default: Date.now },
hidden: Boolean,
meta: {
votes: Number,
favs: Number
}
});
Once created a document,
How can I change the favs number later on?
There is no document for the same that I could find.
This is what I did:
blog.findById(entityId, function(err, mainDoc){
if(err || !mainDoc) return next(err || 'Document not found');
var subDoc = mainDoc['meta'];
if(subDoc){
subDoc = _.extend(subDoc, { favs : 56 }); //_ lib already available
console.log(mainDoc.get('meta')); //Prints the updated result with favs = 56 OK
mainDoc.save(function(err, doc){
console.log(doc.get('meta')); // prints the updated results with favs = 56 OK
});
} else next('Not found');
});
Everything works file and all console gives the desired result.
But when I switch to mongoose console and query the document, I do not get the updated result.
I know there can be other ways to achieve the same, but I am only looking for what I am doing wrong in this particular code.
Why the console, after saving document, gives unmatched data from database?
Upon enabling the mongoose debug option, I found the in query there is no such data to be updated. Query fires with blank $set. { $set : {} }
If you just want to change the value of favs, you can use a simpler query:
blog.findByIdAndUpdate(entityId, {$set: {'meta.favs': 56}}, function(err, doc) {
console.log(doc);
});
Hope I ain't late and will be able to help someone. This Works with deep nested objects as well. No limitations.
const updateNestedObjectParser = (nestedUpdateObject) => {
const final = {
}
Object.keys(nestedUpdateObject).forEach(k => {
if (typeof nestedUpdateObject[k] === 'object' && !Array.isArray(nestedUpdateObject[k])) {
const res = updateNestedObjectParser(nestedUpdateObject[k])
Object.keys(res).forEach(a => {
final[`${k}.${a}`] = res[a]
})
}
else
final[k] = nestedUpdateObject[k]
})
return final
}
console.log(updateNestedObjectParser({
a: {
b: {
c: 99
},
d: {
i: {
l: 22
}
}
},
o: {
a: 22,
l: {
i: "ad"
}
}
}))
The problem is that you can't do anything with data from mongoose once you've got it other than sending it to the client.
HOWEVER, there is the lean method that makes it so you can then update the info and do whatever you want with it.
That would look like this:
blog.findById(entityId).lean().exec(function (err, mainDoc) {
if (err || !mainDoc) {
return next(err || 'Document not found');
}
var subDoc = mainDoc.meta;
if(subDoc){
subDoc.favs = 56;
blog.update({_id: entityId}, mainDoc, function(err, doc){
console.log(doc.get('meta'));
});
} else {
next('Not found');
}
});

Order and limit results in a query with a callback

Using Mongoose, I'd like to make a query with MongoDB and order and limit the results I get. I am doing this with Node.js so I am using callbacks.
So far, I have managed to order my results like this:
myModel.find({ $query: {}, $orderby: { created_at : -1 }}, function (err, items) {
callback( null, items )
});
How can I limit the results I get selecting and index and the number of items I want to get?
Using mongodb native:
http://mongodb.github.io/node-mongodb-native/api-generated/collection.html#find
myModel.find(filter)
.limit(pageSize)
.skip(skip)
.sort(sort)
.toArray(callback);
You can also specify the items in your query:
myModel.find(filter, {sort: {created_at: -1}, limit: 10}, function(err, items){
});
There is no $orderby in node mongodb native, so I'm not sure what library or other tool you're using.
...
Now that you've clarified Mongoose (which in general I recommend against):
myModel.find(filter).limit(10).exec(function(err, items){
//process
});
To sort documents, we can apply sort on a cursor object. To enforce order of sort, instead of passing an object, we need to pass an array to the sort method.
var MongoClient = require('mongodb').MongoClient,
commandLineArgs = require('command-line-args'),
assert = require('assert');
var options = commandLineOptions();
MongoClient.connect('mongodb://localhost:27017/crunchbase', function(err, db) {
assert.equal(err, null);
console.log("Successfully connected to MongoDB.");
var query = queryDocument(options);
var projection = {
"_id": 0,
"name": 1,
"founded_year": 1,
"number_of_employees": 1
};
var cursor = db.collection('companies').find(query);
cursor.project(projection);
cursor.limit(options.limit);
cursor.skip(options.skip);
cursor.sort([
["founded_year", 1],
["number_of_employees", -1]
]);
var numMatches = 0;
cursor.forEach(
function(doc) {
numMatches = numMatches + 1;
console.log(doc.name + "\n\tfounded " + doc.founded_year +
"\n\t" + doc.number_of_employees + " employees");
},
function(err) {
assert.equal(err, null);
console.log("Our query was:" + JSON.stringify(query));
console.log("Documents displayed: " + numMatches);
return db.close();
}
);
});
function queryDocument(options) {
console.log(options);
var query = {
"founded_year": {
"$gte": options.firstYear,
"$lte": options.lastYear
}
};
if ("employees" in options) {
query.number_of_employees = {
"$gte": options.employees
};
}
return query;
}
function commandLineOptions() {
var cli = commandLineArgs([{
name: "firstYear",
alias: "f",
type: Number
}, {
name: "lastYear",
alias: "l",
type: Number
}, {
name: "employees",
alias: "e",
type: Number
}, {
name: "skip",
type: Number,
defaultValue: 0
}, {
name: "limit",
type: Number,
defaultValue: 20000
}]);
var options = cli.parse()
if (!(("firstYear" in options) && ("lastYear" in options))) {
console.log(cli.getUsage({
title: "Usage",
description: "The first two options below are required. The rest are optional."
}));
process.exit();
}
return options;
}
One thing to notice is the order in which MongoDB applies skip, limit and sort
sort
skip
limit
There's also a possibility that we can sort data on the MongoDB side as well, provided that we've setup the indexing.
Notice that MongoDB driver will send a query when we call a cursor method passing a callback function to process query results.

Resources