Mongoose Populate with a condition - javascript

in a Node.js App with Mongoose(Mongodb), With this code i fetch users and their books:
Users.find({user_id: req.user.id}).populate('books').exec(..);
Right now i want to fetch users that has an special book. i do like this:
Users.find({user_id: req.user.id}).populate('books',null,{bookname:"harry potter"}).exec(..);
But it doesn't work. With this, my code fetches users with their books of null value and if that condition matches, return them instead of null. In fact most of my users has null value for books. but what i want is that if that condioton in populate section is not matches, do not return that user in result array at all!
What i have to do? i have to do another query or something on results for what i need?

All I can think here is that you are calling it wrong. You do not really show much context here other than that your .populate() parameters do not look correct.
Here is a correct listing as a reproducible example:
var async = require('async'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
var thingSchema = new Schema({
_id: Number,
name: String
},{ _id: false });
var parentSchema = new Schema({
name: String,
things: [{ type: Number, ref: 'Thing' }]
});
var Thing = mongoose.model( 'Thing', thingSchema ),
Parent = mongoose.model( 'Parent', parentSchema );
mongoose.connect('mongodb://localhost/thingtest');
var things = { "one": 1, "two": 2, "three": 3 };
async.series(
[
function(callback) {
async.each([Thing,Parent],function(model,callback) {
model.remove({},callback);
},callback);
},
function(callback) {
var parentObj = new Parent({ "name": "me" });
async.each(
Object.keys(things).map(function(key) {
return { "name": key, "_id": things[key] }
}),
function(thing,callback) {
var mything = new Thing(thing);
parentObj.things.push(thing._id)
mything.save(callback)
},
function(err) {
if (err) callback(err);
parentObj.save(callback);
}
);
},
function(callback) {
console.log("filtered");
var options = {
path: 'things',
match: { "name": { "$in": ['two','three'] } }
};
Parent.find().populate(options).exec(function(err,docs) {
if (err) callback(err);
console.log(docs);
callback();
});
},
function(callback) {
console.log('unfiltered');
Parent.find().populate('things').exec(function(err,docs) {
if (err) callback(err);
console.log(docs);
callback();
})
}
],
function(err) {
if (err) throw err;
mongoose.disconnect();
}
);
Which will consistently give results like this:
filtered
[ { _id: 55ec4c79f30f550939227dfb,
name: 'me',
__v: 0,
things:
[ { _id: 2, name: 'two', __v: 0 },
{ _id: 3, name: 'three', __v: 0 } ] } ]
unfiltered
[ { _id: 55ec4c79f30f550939227dfb,
name: 'me',
__v: 0,
things:
[ { _id: 1, name: 'one', __v: 0 },
{ _id: 2, name: 'two', __v: 0 },
{ _id: 3, name: 'three', __v: 0 } ] } ]
So take a good look at your data and your calls. The .populate() call needs to match the "path" and then also provide a "match" to query the documents that are to be populated.

Use elemMatch:
var title = 'Harry Potter';
Users.find({books: {$elemMatch: {name: title}})
.exec(processResults);

Related

comparing 2 arrays of objects and performing a mongodb bulkwrite with the differences

I am trying to compare an array of sub documents with one array being the current version of the subdocuments and a new array which will be sent by the front end to update the current array of subdocuments with the changes. this array of subdocuments is used to generate unique forms in the long term but have made example code to see if it would work. I generate an array of updates to use with the mongodb bulkwrite feature and need to know if all seems dandy in my code. If there is a more efficient way of doing this, I'm all ears.
This code example is the main function that will be called when the user submits the http put request.
const exampleFunction = async () => {
//create bulkWrite array for mongodb
//compare if the name has changed
//compare if the object does not exist so that it may be pushed into template array.
// EXAMPLE OF PARENT DOCUMENT
// const parentObj = {
// _id: '123',
// //array of sub documents of original document that will be used for comparison
// formData: [
// { _id: 'abc', name: 'Cat' },
// { _id: 'def', name: 'Dog' },
// { _id: 'ghi', name: 'Bird' }
// ]
// }
// ID of parent document
const parentID = '123'
// Current formData array of sub documents in the parent document
const oldArray = [
{ _id: 'abc', name: 'Cat' },
{ _id: 'def', name: 'Dog' },
{ _id: 'ghi', name: 'Bird' }
]
// New array sent from front end to be compared with against the old array
const newArray = [
{ _id: 'abc', name: 'Cat' },
{_id: 'def', name: 'Dog' },
{ _id: 'ghi', name: 'Lizard' },
{ name: 'Goat' }
]
const update = compareArrays(oldArray, newArray, parentID)
// bulkWrite function made to mongodb
await collection.bulkWrite(update).catch((err) => {
console.log(err)
})
}
While this code is the main comparison function that is called in the code block above.
const compareArrays = async (a, b, parentID) => {
let bulkArray = []
a.map((itemA) => {
b.map(async (itemB) => {
if(!itemB._id) {
// Check if a new object has been added that has yet to receive ID from database, if no id, then push to array of parent document
bulkArray.push(
{
updateOne: {
filter: {_id: parentID},
update: { $push: { formData: itemB } },
},
}
)
} else {
if(itemA._id === itemB._id) {
//match ids and perform another check
if (itemA.name !== itemB.name) {
//check if the names do not match and push the update to bulkArray for mongodb to update sub document of parent document
bulkArray.push(
{
updateOne: {
filter: {_id: parentID, 'formData._id': itemA._id},
update: { $set: { name: itemB.name } }
}
}
)
}
}
}
})
})
return bulkArray
}
The final document should look something like this,
const finalParentObj = {
_id: '123',
formData: [
{ _id: 'abc', name: 'Cat' },
{ _id: 'def', name: 'Dog' },
{ _id: 'ghi', name: 'Lizzard' },
{ id: 'jkl', name: 'Goat'}
]
}
So the bulkwrite() is used mainly to reduce the number of multiple trips required to write multiple operations. It does all the operations in an atomic way. It takes the input as an array of operations you wanted to perform on the DB.collection
db.collection.bulkWrite([
{ updateOne: { filter: { a: 1 }, update: { $set: { a: 2 } } } },
{ updateMany: { filter: { b: 1 }, update: { $set: { b: 2 } } } },
]);

Mongoose: Access parent key from child object in Schema

I am working with mongoose schema and trying to get a parent obj in child from the child itself (I know it is not allowed in Javascript, but there are workarounds). This was my first implementation
const customer = mongoose.Schema({
name: String,
products_sold: [
{
name: String,
price: Number,
qty: Number,
},
{
name: String,
price: Number,
qty: Number,
},
],
messages: [
{
timestamp: {
type : Date,
default: Date.now
},
_my_key_: {
type: String,
default: () => {
// here i need to get products_sold.name in array like [products_sold[0].name, products_sold[1].name]
// this.products_sold does not work
},
},
}
]
})
I looked up some resources like this one. So i also tried
const customer = mongoose.Schema({
name: String,
products_sold: [
{
name: String,
price: Number,
qty: Number,
},
{
name: String,
price: Number,
qty: Number,
},
],
messages: [
{
timestamp: {
type : Date,
default: Date.now
},
_my_key_: {
type: String,
default: () => {
// here this.parent.products_sold does not work also
},
},
}
],
init: function(){
this.messages._my_key_.parent = this;
delete this.init;
return this;
}
}.init()
)
For Reference:
Mongoose Default Functions and This
This question does not answer mine.
EDIT # 1
I tried this with both arrow and regular function.
EDIT # 2
As per comment feedback from #Molda. After the above code, This is how i make the instance and save a record.
const Customer = mongoose.model('Customer', customer);
const customer = {
name: "John Doe",
products_sold: [
{
name: "product_name",
price: 1245,
qty: 2,
}
],
messages: [
{
// message timestamp will generate from default and _my_key_ too will generate from default
}
]
}
const callingFunc = async () => {
const cust = await Customer(customer);
await cust.save();
return cust;
};

Javascript map operator

I have an object like this
[
{
_id: '5ef34e92858bff53bcf69e11',
factors: [ {factor_id:'factor_id1',calmode:'calmode1',desc:'desc1',webserv:'webserv1',approach:'approach1'},
{factor_id:'factor_id2',calmode:'calmode2',desc:'desc2',webserv:'webserv2',approach:'approach2'},
{factor_id:'factor_id3',calmode:'calmode3',desc:'desc3',webserv:'webserv3',approach:'approach3'}
],
clientId: 'Company1',
module: 'Mod1',
__v: 0
},
{
_id: '5ef350c9c1acd61e58ef9d08',
factors: [ {factor_id:'factor_id4',calmode:'calmode4',desc:'desc4',webserv:'webserv4',approach:'approach4'},
{factor_id:'factor_id5',calmode:'calmode5',desc:'desc5',webserv:'webserv5',approach:'approach5'}
],
clientId: 'Company1',
module: 'Mod2',
__v: 0
}
]
I want to create a final list like below
_id, ClientId,module,factor_id,calmode,desc,webserv,approach
I am trying to use map operator within another map operator but its not coming out properly. Any help would be appreciated.
const tmpFacLst = FacExists.map((module) => {
const Factor = {
module_id: module._id,
module: module.module,
};
return Factor;
/*const Fac = module.factors.map((factor)=>{
const FactorDtl = {
factor_id:factor._id,
factor_desc: factor.desc
}
return FactorDtl;
})*/
});
Update: I am able to achieve using loop
const result = [];
FacExists.forEach((item) => {
const Factors = item.factors;
Factors.forEach((Subitem) => {
const Facobj = {
_id: item._id,
ClientId: item.clientId,
module: item._id,
factor_id: Subitem._id,
calmode: Subitem.calmode,
desc: Subitem.desc,
webserv: Subitem.webserv,
};
result.push(Facobj);
});
});
I want to know is there any better way of doing this without looping.
An approach like this should work:
const items = [
{
_id: "5ef34e92858bff53bcf69e11",
factors: [
{
factor_id: 2,
calmode: "cal",
desc: "something",
webserv: "10.0.0.0",
approach: "forwards",
},
],
clientId: "Company1",
module: "Mod1",
__v: 0,
},
{
_id: "5ef350c9c1acd61e58ef9d08",
factors: [
{
factor_id: 3,
calmode: "cal",
desc: "something",
webserv: "10.0.0.1",
approach: "forwards",
},
],
clientId: "Company1",
module: "Mod2",
__v: 0,
},
];
const result = [];
items.forEach((item) => {
const { factors, __v, ...rest } = item;
result.push(...factors.map((factor) => ({ ...factor, ...rest })));
});
console.log(result);
First, you need to clean your question up a bit because you have the Object keyword / class listed as elements of your factors array, which you call an "object". You should include those objects in your snippets.
let notAnObj = [
{
_id: '5ef34e92858bff53bcf69e11',
factors: [ {_id: 1234, desc: 'bob loblaw'}],
clientId: 'Company1',
module: 'Mod1',
__v: 0
},
{
_id: '5ef350c9c1acd61e58ef9d08',
factors: [],
clientId: 'Company1',
module: 'Mod2',
__v: 0
}
]
console.log(notAnObject)
let arr= [
{
_id: '5ef34e92858bff53bcf69e11',
factors: [ {_id: 1234, desc: 'bob loblaw'}],
clientId: 'Company1',
module: 'Mod1',
__v: 0
},
{
_id: '5ef350c9c1acd61e58ef9d08',
factors: [],
clientId: 'Company1',
module: 'Mod2',
__v: 0
}
]
const tmpFacLst = arr.map((obj) => {
return {
_id: obj._id,
ClientId: obj.clientId,
module: obj.module,
factors: obj.factors.map(factor => {
return {
_id: factor._id,
desc: factor.desc,
}
}),
calmode: undefined,
webserv: undefined,
approach: undefined
};
});
console.log(tmpFacLst)
you can do it like this
const finalResult = FacExists.reduce((aggregator,fact) => {
let result = fact.factors.map(fac=>{
return {
_id: fact._id,
clientId: fact.clientId,
module: fact.module,
...fac
}})
aggregator = [...aggregator,...result];
return aggregator
},[]);
you will get the desired result in the "finalResult" variable.

Add/Merge element from from one Object to another Object inside array

I have the following code in my async function
async function test () {
try {
const aucs = await auctions.find({owner: 'owner', place: 'place'}).limit(15);
const item = await Promise.all(aucs.map(async aucs => {
const map = await items.find({id: aucs.item});
return map[0]
}));
--->we are here [1]
} catch (err) {
console.log(err);
}
}
test();
and the point [1] I have two arrays avaliable which contain another objects (both are responces from Mongo) here they are:
aucs = [ { _id: 5c00faa4936359120ceb3632,
auc: 177215422,
item: 130251,
price: 26000000,
lastModified: 1543567955000,
date: 2018-11-30T08:53:56.290Z,
__v: 0 },
{ _id: 5c00faa4936359120ceb363f,
auc: 177215440,
item: 130251,
price: 26000000,
lastModified: 1543567955000,
date: 2018-11-30T08:53:56.290Z,
__v: 0 },... ]
and
item = [ { _id: 5bcd8a6134cdd1223cd3239b,
id: 130251,
name: 'TEST_NAME_1',
__v: 0 },
{ _id: 5bcd8a6134cdd1223cd3239b,
id: 130252,
name: 'TEST_NAME_2',
__v: 0 },...]
And I'd like to add to aucs[i]every element in aucs, item[i].name (name: 'TEST_NAME_1')
Like:
combined = [ { _id: 5c00faa4936359120ceb3632,
auc: 177215422,
item: 130251,
name: 'TEST_NAME_1',
price: 26000000,
lastModified: 1543567955000,
date: 2018-11-30T08:53:56.290Z,
__v: 0 },...]
I'm trying to use for loop with auc[i].name = item[i].name or using aucs.push() but for some unknown reason it wasn't worked for me.
I receive error for .push is not a function and for loop didn't add anything. So maybe someone have any idea?
Note: 1
actually solve one problem with item, mongo returns me array inside array like [ [ { ...} ] ] so I should using return map[0] to fix it.
Note: 2
both of aucs and item are object according to typeof and have .length option (they are both the same length and should be all the time. So they are not promises
Note: 3
let merged = {...aucs, ...item}; returns me
{ '0': { _id: 5bcd8a6134cdd1223cd3239b,
id: 130251,
name: 'JewelCraft',
icon: 'inv_jewelcrafting_70_jeweltoy',
stackable: 1,
isAuctionable: true,
__v: 0 }...
but not what I need to
Would be more superior and faster if you use some aggregation trick here
auctions.aggregate([
{ "$match": { "owner": "owner", "place": "place" }},
{ "$lookup": {
"from": "items",
"let": { "item": "$item" },
"pipeline": [
{ "$match": { "$expr": { "$eq": ["$id", "$$item"] }}}
],
"as": "item"
}},
{ "$limit": 15 }
])
If I understand correctly, the aim is to create a new collection of aucs like the ones found, each updated to include an item name from the item collection where the item has a matching id.
let aucs = [ { _id: "5c00faa4936359120ceb3632",
auc: 177215422,
item: 130251,
price: 26000000,
lastModified: 1543567955000,
date: "2018-11-30T08:53:56.290Z",
__v: 0 },
{ _id: "5c00faa4936359120ceb363f",
auc: 177215440,
item: 130251,
price: 26000000,
lastModified: 1543567955000,
date: "2018-11-30T08:53:56.290Z",
__v: 0 } ];
item = [ { _id: "5bcd8a6134cdd1223cd3239b",
id: 130251,
name: 'TEST_NAME_1',
__v: 0 },
{ _id: "5bcd8a6134cdd1223cd3239b",
id: 130252,
name: 'TEST_NAME_2',
__v: 0 }];
// create a new collection of aucs modified to include the names of matching items
let combined = [];
aucs.forEach(auc => {
let combinedAuc = Object.assign({}, auc);
combined.push(combinedAuc);
let matchingItem = item.find(i => auc.item === i.id);
if (matchingItem) combinedAuc.name = matchingItem.name
});
console.log(combined)

Mongoose populate denormalized data

I have a User model and a Book model. I want some data from my books to be denormalized on each User document, but still have the option to populate if needed. If I set ref: 'Book' on the books.$._id it gets populated inside the _id path which is unintended. I would like the population to overwrite the denormalized data.
How do I accomplish this?
in users.model.js:
const { Schema } = require('mongoose');
const UserSchema = new Schema({
name: String,
books: {
type: [
{
_id: Schema.Types.ObjectId,
title: String,
length: Number,
},
],
default: [],
},
});
Desired outcome
in users.controller.js:
app.get('/', async (req, res, next) => {
const users = await User.find({})
/*
users: [{
_id: ObjectId(),
name: 'Andrew',
books: [{
_id: ObjectId(),
title: 'Game of Thrones',
length: 298,
}, { ... }],
}, { ... }]
*/
});
app.get('/:id', async (req, res, next) => {
const book_id = req.params.id;
const user = await User.findById(book_id).populate({
path: 'books',
model: 'Book',
});
/*
user: {
_id: ObjectId(),
name: 'Andrew',
books: [{
_id: ObjectId(),
name: 'Game of Thrones',
length: 298,
author: 'Simone Dunow',
releasedOn: Date(),
price: 30,
...
}, { ... }],
}
*/
});
Schemas I've tried so far:
books: {
type: [
{
_id: Schema.Types.ObjectId,
title: String,
length: Number,
},
],
default: [],
ref: 'Book',
},
returns array of { _id: null }
books: {
type: [
{
_id: {
type: Schema.Types.ObjectId,
ref: 'Book',
},
title: String,
length: Number,
},
],
default: [],
},
books are populated inside of _id: { _id: { Book } }
books: {
type: [
{
type: {
_id: Schema.Types.ObjectId,
title: String,
length: Number,
},
ref: 'Book',
},
],
default: [],
},
throws exception: invalid type
const UserSchema = new Schema({
name: String,
books: [{
id: { type : Schema.Types.ObjectId, ref : 'Book'} //Whatever string you have used while modeling your schema
title: String,
length: Number,
}],
});
While using the schema you can populate as follows :
populate({ path: 'books.id' })
Output :
{
_id : // someid
name : "somename"
books : [
{
id : {//document referring to Books collection},
title : "sometitle",
length : //somelength
}, ...
]
}
To anybody that might be still looking to achieve a full replacement, full disclosure: It might be a bit hacky for some evangelists or even have a performance toll on high traffic apps, but if you really want to do it, you can tap into the toJSON method of the schema like the following:
UserSchema.method('toJSON', function () {
let obj = this.toObject();
obj.books = obj.books.map(
(book) => (Schema.Types.ObjectId.isValid(book.id)) ? book : book.id
);
return obj;
});
What's going on here is basically we're replacing the whole property with the populated result when the book.id has been populated otherwise we just return the original object by checking the validity of the book's id (when populated will be a full bloomed object rather than an id).

Categories