How to search through mongoose ObjectId in feathers? - javascript

I have two feathers services, one for profiles and the other one for labels.
A profile can have array of ObjectId labels from other collections.
Now I have an input for search and a user types "linux"
The profile foo should be returned because it contains the id "594ceeff6905e622425f523b" in the labels array.
This kind of search query through ObjectId between objects is possible through feathers?
Profiles
 Mongoose model
{
name: { type: String, trim: true, required: true },
labels: [{ type: ObjectId, ref: 'Labels' }],
}
Feathers api get response to profiles
get http://localhost:3030/profiles
{
"name" : "foo",
"labels" : [
"594ceeff6905e622425f523b",
"594ceeff6905e622425f523c",
"594ceeff6905e622425f523d"
],
}
{
"name" : "bar",
"labels" : [
"594ceeff6905e622425f523e",
"594ceeff6905e622425f523d"
],
}
Labels
 Mongoose model
{
name: { type: String, trim: true, unique: true, required: true },
}
Feathers api get response to labels
get http://localhost:3030/labels
{
"_id": "594ceeff6905e622425f523b",
"name": "linux"
},
{
"_id": "594ceeff6905e622425f523c",
"name": "systemd"
},
{
"_id": "594ceeff6905e622425f523d",
"name": "mongodb"
},
{
"_id": "594ceeff6905e622425f523e",
"name": "javascript"
}
Now I have to populate all the labels on the profiles response, send all the profiles and then filter them on the front with that value of the input for search.
As the database grows this is going to be very inefficient, it has to exist a better way of doing this right?

You can try code like this
Profile.find({}).populate({
path: 'labels',
match: {
name: {
$regex: new RegExp(searchText, 'i');
//searchText: passed from the front end.
}
}
}).then(function(profiles){
var filteredProfiles = profiles.forEach(function(profile){
return profile.labels; //will be null for items don't match the
//searching regex.
//resolve the filtered profiles to the front end.
})
},function(error){
//Error handling
})

Feathers does not restrict you on anything that you can do with Mongoose itself and for what you would like to do you can use the Mongoose query population.
The feathers-mongoose adapter supports this through the $populate query parameter so querying
http://localhost:3030/labels?$populate=labels
Should do what you are looking for.

I the end I just two calls to the api like this:
computed: {
...mapState('profiles', { profiles: 'keyedById' }),
...mapState('labels', { labels: 'keyedById' }),
},
methods: {
...mapActions('profiles', { findProfiles: 'find' }),
async fetch() {
const labels = this.labels
const search = this.search_input.toLowerCase()
// Generates an array of matched labels per profile
const labels_id = Object.keys(labels).filter(label => {
const name = labels[label].name.toLowerCase()
return name.includes(search)
})
// Searches profiles by name or labels
this.findProfiles({
query: {
$or: [
{
name: { $regex: search, $options: 'igm' },
},
{ labels: { $in: labels_id } },
],
$populate: ['user'],
$sort: { updatedAt: -1 },
},
})
},
},

Related

Conditional joins on collections using mongoose

I'm new to mongoDB, I am trying to achieve the following SQL query on it. but could not find anything useful so far. can anyone tell equivalent mongoose query
select * from interviews
inner join candidate on interviews.clientId = candidate._id
inner join billing on appointment._id = billing.appointmentId
where ('
interviews.status= "upcoming",
interviews.startTime= "2017-01-01",
candidate.clientAgeGroup= "adult",
candidate.candidatetatus= "new",
billing.paymentStatus= "paid"
')
what I got so far is following
const [result, err] = await of(Interview.find({ ...filterQuery }).limit(perPage)
.skip(perPage * page)
.sort({
startTime: 'asc'
})
.populate([{ path: 'candidateId', model: 'Candidate', select: 'firstName status avatar' },
{ path: 'billingId', model: 'Billing', select: "status" }]));
UPDATE
I have following name and export scheme
//interview.model.js => mongodb show name as interview
module.exports = mongoose.model('Interview', interviewSchema);
//candidate.model.js => mongodb show name as candidate
module.exports = mongoose.model('Candidate', candidateSchema);
You can use filter out objects included in resulting array using match but in the case if it couldn't find any, it would still return a null value. So in comparison this works similar to sql left join.
const [result, err] = await of(Interview.find({ ...filterQuery }).limit(perPage)
.skip(perPage * page)
.sort({
startTime: 'asc'
})
.populate([{ path: 'candidateId', model: 'Candidate', select: 'firstName status avatar', match: {clientAgeGroup: "adult", candidatetatus: "new"} },
{ path: 'billingId', model: 'Billing', select: "status", match: {paymentStatus: "paid"} }]));
Also see https://mongoosejs.com/docs/populate.html#query-conditions
If you need strictly a inner join then you can use mongodb aggregate pipeline.
Interview.aggregate([
{
"$match": {
status: "upcoming",
startTime: "2017-01-01",
}
},
{
'$lookup': {
'from': 'candidates', // this should be your collection name for candidates.
'localField': 'candidateId', // there should be an attribute named candidateId in interview model that refer to candidate collection
'foreignField': '_id',
'as': 'candidates'
}
}, {
'$match': {
'candidates.clientAgeGroup': "adult",
'candidates.candidatetatus': "new"
}
},
{
'$lookup': {
'from': 'billing', // this should be your collection name for billing.
'localField': 'billingId', // there should be an attribute named billingId in interview model that refer to billing collection
'foreignField': '_id',
'as': 'billing'
}
}, {
'$match': {
'billing.paymentStatus': "paid"
}
},
{ "$sort": { startTime: 1 } },
{ "$limit": perPage },
{ "$skip": perPage * page }
])

Sequelize querying both parent model and included model using [Op.or]

I want to findAll Bookings where the booking has been paid.
A legacy booking has been paid if the paymentAuthorised: boolean attribute on the Booking table is true.
A new booking has been paid if the paymentAuthorised: boolean attribute on the Payment table is true and type: string attribute on the Payment table is 'booking'.
When I perform the following query it returns an error saying payments.paymentAuthorised not found.
const bookings = await db.Booking.findAll({
include: [
{
model: db.Payment,
as: "payments",
},
],
where: {
[Op.or]: [
{
paymentAuthorised: { [Op.eq]: true },
},
{
"$payments.paymentAuthorised$": { [Op.eq]: true },
"$payments.type$": { [Op.eq]: "booking" },
},
],
},
order: [["dateTime", "asc"]],
});
I worked this issue out in the end by logging the generated SQL. You need to add the following parameter: subQuery: false. This is so it generates an SQL query which includes the joins before the where.
const bookings = await db.Booking.findAll({
include: [
{
model: db.Payment,
as: "payments",
},
],
where: {
[Op.or]: [
{
paymentAuthorised: { [Op.eq]: true },
},
{
"$payments.paymentAuthorised$": { [Op.eq]: true },
"$payments.type$": { [Op.eq]: "booking" },
},
],
},
subQuery: false,
order: [["dateTime", "asc"]],
});

Virtual populate mongoose

I've tried to use virtual populate between two models I created:
in this case to get all the reviews with the tour id and show them with the tour.
(when using query findById() to show only this tour)
my virtual is set to true in the Schema (I've tried to set them to true after using the virtual populate but it doesn't work - by this soultion)
after checking the mongoose documentation its seems to be right but it doesn't work.
my tourSchema:
const tourSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'A tour must have a name'], //validator
unique: true,
trim: true,
maxlength: [40, 'A tour name must have less or equal then 40 characters'],
minlength: [10, 'A tour name must have at least 10 character']
//validate: [validator.isAlpha, 'A tour name must have only alphabetic characters']
},
etc...
etc...
etc...
//guides: Array --- array of user id's || embedding
guides: [
//Reference to the user data model without saving the guides in the tour data model
//Child referencing
{
type: mongoose.Schema.ObjectId,
ref: 'User'
}
]
},
{
//passing options, getting the virual properties to the document/object
toJSON: { virtuals: true },
toObject: { virtuals: true }
}
);
//Define virtual properties
tourSchema.virtual('durationWeeks').get(function () {
console.log('Virtual 1');
//using function declaration => using this keyword
return this.duration / 7;
});
//Virtual populate
tourSchema.virtual('reviews', {
ref: 'review',
localField: '_id', // Find tour where `localField`
foreignField: 'tour', // is equal to `foreignField`
//look for the _id of the tour in the tour field in review
});
my reviewSchema:
** I used in the review schema in for the tour and user populate for the tour id **
const reviewSchema = new mongoose.Schema({
review: {
type: String,
required: [true, 'Review can not be empty!']
},
rating: {
type: Number,
min: 1,
max: 5
},
createdAt: {
type: Date,
default: Date.now(),
},
tour: [
{
type: mongoose.Schema.ObjectId,
ref: 'Tour',
required: [true, 'Review must be belong to a tour.']
}
],
user: [
{
type: mongoose.Schema.ObjectId,
ref: 'User',
required: [true, 'Review must be belong to a user.']
}
]
},
{
//passing options, getting the virual properties to the document/object
toJSON: { virtuals: true },
toObject: { virtuals: true },
}
);
//Query middleware
reviewSchema.pre(/^find/, function (next) {
this.populate({
path: 'tour',
select: 'name'
})
.populate({
path: 'user',
select: 'name'
});
next();
});
My output:
get all reviews (review model data):
{
"status": "success",
"data": {
"review": [
{
"_id": "5f6ba5b45624454efca7e0b1",
"review": "What an amzing tour",
"tour": {
"guides": [],
"_id": "5c88fa8cf4afda39709c2955",
"name": "The Sea Explorer",
"durationWeeks": null,
"id": "5c88fa8cf4afda39709c2955"
},
"user": {
"_id": "5f69f736e6eb324decbc3a52",
"name": "Liav"
},
"createdAt": "2020-09-23T19:44:52.519Z",
"id": "5f6ba5b45624454efca7e0b1"
}
]
}
}
and the get tour by id:
{
"status": "success",
"data": {
"tour": {
"startLocation": {
"type": "Point",
"coordinates": [
-80.185942,
25.774772
],
"description": "Miami, USA",
"address": "301 Biscayne Blvd, Miami, FL 33132, USA"
},
"ratingsAverage": 4.8,
"ratingsQuantaity": 0,
"images": [
"tour-2-1.jpg",
"tour-2-2.jpg",
"tour-2-3.jpg"
],
"startDates": [
"2021-06-19T09:00:00.000Z",
"2021-07-20T09:00:00.000Z",
"2021-08-18T09:00:00.000Z"
],
"secretTour": false,
"guides": [],
"_id": "5c88fa8cf4afda39709c2955",
.
.
.
.
"slug": "the-sea-explorer",
"__v": 0,
"durationWeeks": 1,
"id": "5c88fa8cf4afda39709c2955"
}
}
}
as you can see the review has the tour as an arr and the id is inside the arr of the tour is there an option that the populate is not targeting the right field?
You need an option virtuals: true passed into the schema creation:
const tourSchema = new mongoose.Schema({
...
}, {
virtuals: true
}
In addition, we use the mongoose-lean-virtuals module to help with .lean and virtuals. e.g.
const mongooseLeanVirtuals = require('mongoose-lean-virtuals');
...
tourSchema.plugin(mongooseLeanVirtuals);
tourSchema.set('toJSON', { virtuals: true });
tourSchema.set('toObject', { virtuals: true });
though I'm guessing that's not strictly necessary.
So I figure it out.
First i asked in github - mongoose repo and got answerd:
reviewSchema.pre(/^find/, function (next) {
this.populate({
path: 'tour',
options: { select: 'name' } // <-- wrap `select` in `options` here...
}).populate({
path: 'user',
options: { select: 'name photo' } // <-- and here
});
next();
});
We should improve this: nested options are very confusing and it's
hard to remember whether something should be options.select or select
The second issue was to add populate after using the FindById method in the tour controller, using the populate without using the wrap 'select' didn't work for me.
exports.getTour = catchAsync(async (req, res, next) => { //parameter => :id || optinal parameter => :id?
//populate reference to the guides in the user data model
const tour = await Tour.findById(req.params.id).populate('reviews');
if (!tour) {
return next(new AppError('No tour found with that id', 404));
}
res.status(200).json({
status: 'success',
data: {
tour
}
});
})
and in the tour model, I changed the foreign key from "tour_id" (as I saw in other questions to "tour").
//Virtual populate
tourSchema.virtual('reviews', {
ref: 'Review',
localField: '_id', // Find tour where `localField`
foreignField: 'tour' // is equal to `foreignField`
//look for the _id of the tour in the tour field in review
});
Now i do have reviews in my tour data and it does virtual populate to the tour by id

MongoDB find() with dot notation does not work [duplicate]

I'm pretty new to Mongoose and MongoDB in general so I'm having a difficult time figuring out if something like this is possible:
Item = new Schema({
id: Schema.ObjectId,
dateCreated: { type: Date, default: Date.now },
title: { type: String, default: 'No Title' },
description: { type: String, default: 'No Description' },
tags: [ { type: Schema.ObjectId, ref: 'ItemTag' }]
});
ItemTag = new Schema({
id: Schema.ObjectId,
tagId: { type: Schema.ObjectId, ref: 'Tag' },
tagName: { type: String }
});
var query = Models.Item.find({});
query
.desc('dateCreated')
.populate('tags')
.where('tags.tagName').in(['funny', 'politics'])
.run(function(err, docs){
// docs is always empty
});
Is there a better way do this?
Edit
Apologies for any confusion. What I'm trying to do is get all Items that contain either the funny tag or politics tag.
Edit
Document without where clause:
[{
_id: 4fe90264e5caa33f04000012,
dislikes: 0,
likes: 0,
source: '/uploads/loldog.jpg',
comments: [],
tags: [{
itemId: 4fe90264e5caa33f04000012,
tagName: 'movies',
tagId: 4fe64219007e20e644000007,
_id: 4fe90270e5caa33f04000015,
dateCreated: Tue, 26 Jun 2012 00:29:36 GMT,
rating: 0,
dislikes: 0,
likes: 0
},
{
itemId: 4fe90264e5caa33f04000012,
tagName: 'funny',
tagId: 4fe64219007e20e644000002,
_id: 4fe90270e5caa33f04000017,
dateCreated: Tue, 26 Jun 2012 00:29:36 GMT,
rating: 0,
dislikes: 0,
likes: 0
}],
viewCount: 0,
rating: 0,
type: 'image',
description: null,
title: 'dogggg',
dateCreated: Tue, 26 Jun 2012 00:29:24 GMT
}, ... ]
With the where clause, I get an empty array.
With a modern MongoDB greater than 3.2 you can use $lookup as an alternate to .populate() in most cases. This also has the advantage of actually doing the join "on the server" as opposed to what .populate() does which is actually "multiple queries" to "emulate" a join.
So .populate() is not really a "join" in the sense of how a relational database does it. The $lookup operator on the other hand, actually does the work on the server, and is more or less analogous to a "LEFT JOIN":
Item.aggregate(
[
{ "$lookup": {
"from": ItemTags.collection.name,
"localField": "tags",
"foreignField": "_id",
"as": "tags"
}},
{ "$unwind": "$tags" },
{ "$match": { "tags.tagName": { "$in": [ "funny", "politics" ] } } },
{ "$group": {
"_id": "$_id",
"dateCreated": { "$first": "$dateCreated" },
"title": { "$first": "$title" },
"description": { "$first": "$description" },
"tags": { "$push": "$tags" }
}}
],
function(err, result) {
// "tags" is now filtered by condition and "joined"
}
)
N.B. The .collection.name here actually evaluates to the "string" that is the actual name of the MongoDB collection as assigned to the model. Since mongoose "pluralizes" collection names by default and $lookup needs the actual MongoDB collection name as an argument ( since it's a server operation ), then this is a handy trick to use in mongoose code, as opposed to "hard coding" the collection name directly.
Whilst we could also use $filter on arrays to remove the unwanted items, this is actually the most efficient form due to Aggregation Pipeline Optimization for the special condition of as $lookup followed by both an $unwind and a $match condition.
This actually results in the three pipeline stages being rolled into one:
{ "$lookup" : {
"from" : "itemtags",
"as" : "tags",
"localField" : "tags",
"foreignField" : "_id",
"unwinding" : {
"preserveNullAndEmptyArrays" : false
},
"matching" : {
"tagName" : {
"$in" : [
"funny",
"politics"
]
}
}
}}
This is highly optimal as the actual operation "filters the collection to join first", then it returns the results and "unwinds" the array. Both methods are employed so the results do not break the BSON limit of 16MB, which is a constraint that the client does not have.
The only problem is that it seems "counter-intuitive" in some ways, particularly when you want the results in an array, but that is what the $group is for here, as it reconstructs to the original document form.
It's also unfortunate that we simply cannot at this time actually write $lookup in the same eventual syntax the server uses. IMHO, this is an oversight to be corrected. But for now, simply using the sequence will work and is the most viable option with the best performance and scalability.
Addendum - MongoDB 3.6 and upwards
Though the pattern shown here is fairly optimized due to how the other stages get rolled into the $lookup, it does have one failing in that the "LEFT JOIN" which is normally inherent to both $lookup and the actions of populate() is negated by the "optimal" usage of $unwind here which does not preserve empty arrays. You can add the preserveNullAndEmptyArrays option, but this negates the "optimized" sequence described above and essentially leaves all three stages intact which would normally be combined in the optimization.
MongoDB 3.6 expands with a "more expressive" form of $lookup allowing a "sub-pipeline" expression. Which not only meets the goal of retaining the "LEFT JOIN" but still allows an optimal query to reduce results returned and with a much simplified syntax:
Item.aggregate([
{ "$lookup": {
"from": ItemTags.collection.name,
"let": { "tags": "$tags" },
"pipeline": [
{ "$match": {
"tags": { "$in": [ "politics", "funny" ] },
"$expr": { "$in": [ "$_id", "$$tags" ] }
}}
]
}}
])
The $expr used in order to match the declared "local" value with the "foreign" value is actually what MongoDB does "internally" now with the original $lookup syntax. By expressing in this form we can tailor the initial $match expression within the "sub-pipeline" ourselves.
In fact, as a true "aggregation pipeline" you can do just about anything you can do with an aggregation pipeline within this "sub-pipeline" expression, including "nesting" the levels of $lookup to other related collections.
Further usage is a bit beyond the scope of what the question here asks, but in relation to even "nested population" then the new usage pattern of $lookup allows this to be much the same, and a "lot" more powerful in it's full usage.
Working Example
The following gives an example using a static method on the model. Once that static method is implemented the call simply becomes:
Item.lookup(
{
path: 'tags',
query: { 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } }
},
callback
)
Or enhancing to be a bit more modern even becomes:
let results = await Item.lookup({
path: 'tags',
query: { 'tagName' : { '$in': [ 'funny', 'politics' ] } }
})
Making it very similar to .populate() in structure, but it's actually doing the join on the server instead. For completeness, the usage here casts the returned data back to mongoose document instances at according to both the parent and child cases.
It's fairly trivial and easy to adapt or just use as is for most common cases.
N.B The use of async here is just for brevity of running the enclosed example. The actual implementation is free of this dependency.
const async = require('async'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
mongoose.Promise = global.Promise;
mongoose.set('debug', true);
mongoose.connect('mongodb://localhost/looktest');
const itemTagSchema = new Schema({
tagName: String
});
const itemSchema = new Schema({
dateCreated: { type: Date, default: Date.now },
title: String,
description: String,
tags: [{ type: Schema.Types.ObjectId, ref: 'ItemTag' }]
});
itemSchema.statics.lookup = function(opt,callback) {
let rel =
mongoose.model(this.schema.path(opt.path).caster.options.ref);
let group = { "$group": { } };
this.schema.eachPath(p =>
group.$group[p] = (p === "_id") ? "$_id" :
(p === opt.path) ? { "$push": `$${p}` } : { "$first": `$${p}` });
let pipeline = [
{ "$lookup": {
"from": rel.collection.name,
"as": opt.path,
"localField": opt.path,
"foreignField": "_id"
}},
{ "$unwind": `$${opt.path}` },
{ "$match": opt.query },
group
];
this.aggregate(pipeline,(err,result) => {
if (err) callback(err);
result = result.map(m => {
m[opt.path] = m[opt.path].map(r => rel(r));
return this(m);
});
callback(err,result);
});
}
const Item = mongoose.model('Item', itemSchema);
const ItemTag = mongoose.model('ItemTag', itemTagSchema);
function log(body) {
console.log(JSON.stringify(body, undefined, 2))
}
async.series(
[
// Clean data
(callback) => async.each(mongoose.models,(model,callback) =>
model.remove({},callback),callback),
// Create tags and items
(callback) =>
async.waterfall(
[
(callback) =>
ItemTag.create([{ "tagName": "movies" }, { "tagName": "funny" }],
callback),
(tags, callback) =>
Item.create({ "title": "Something","description": "An item",
"tags": tags },callback)
],
callback
),
// Query with our static
(callback) =>
Item.lookup(
{
path: 'tags',
query: { 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } }
},
callback
)
],
(err,results) => {
if (err) throw err;
let result = results.pop();
log(result);
mongoose.disconnect();
}
)
Or a little more modern for Node 8.x and above with async/await and no additional dependencies:
const { Schema } = mongoose = require('mongoose');
const uri = 'mongodb://localhost/looktest';
mongoose.Promise = global.Promise;
mongoose.set('debug', true);
const itemTagSchema = new Schema({
tagName: String
});
const itemSchema = new Schema({
dateCreated: { type: Date, default: Date.now },
title: String,
description: String,
tags: [{ type: Schema.Types.ObjectId, ref: 'ItemTag' }]
});
itemSchema.statics.lookup = function(opt) {
let rel =
mongoose.model(this.schema.path(opt.path).caster.options.ref);
let group = { "$group": { } };
this.schema.eachPath(p =>
group.$group[p] = (p === "_id") ? "$_id" :
(p === opt.path) ? { "$push": `$${p}` } : { "$first": `$${p}` });
let pipeline = [
{ "$lookup": {
"from": rel.collection.name,
"as": opt.path,
"localField": opt.path,
"foreignField": "_id"
}},
{ "$unwind": `$${opt.path}` },
{ "$match": opt.query },
group
];
return this.aggregate(pipeline).exec().then(r => r.map(m =>
this({ ...m, [opt.path]: m[opt.path].map(r => rel(r)) })
));
}
const Item = mongoose.model('Item', itemSchema);
const ItemTag = mongoose.model('ItemTag', itemTagSchema);
const log = body => console.log(JSON.stringify(body, undefined, 2));
(async function() {
try {
const conn = await mongoose.connect(uri);
// Clean data
await Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()));
// Create tags and items
const tags = await ItemTag.create(
["movies", "funny"].map(tagName =>({ tagName }))
);
const item = await Item.create({
"title": "Something",
"description": "An item",
tags
});
// Query with our static
const result = (await Item.lookup({
path: 'tags',
query: { 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } }
})).pop();
log(result);
mongoose.disconnect();
} catch (e) {
console.error(e);
} finally {
process.exit()
}
})()
And from MongoDB 3.6 and upward, even without the $unwind and $group building:
const { Schema, Types: { ObjectId } } = mongoose = require('mongoose');
const uri = 'mongodb://localhost/looktest';
mongoose.Promise = global.Promise;
mongoose.set('debug', true);
const itemTagSchema = new Schema({
tagName: String
});
const itemSchema = new Schema({
title: String,
description: String,
tags: [{ type: Schema.Types.ObjectId, ref: 'ItemTag' }]
},{ timestamps: true });
itemSchema.statics.lookup = function({ path, query }) {
let rel =
mongoose.model(this.schema.path(path).caster.options.ref);
// MongoDB 3.6 and up $lookup with sub-pipeline
let pipeline = [
{ "$lookup": {
"from": rel.collection.name,
"as": path,
"let": { [path]: `$${path}` },
"pipeline": [
{ "$match": {
...query,
"$expr": { "$in": [ "$_id", `$$${path}` ] }
}}
]
}}
];
return this.aggregate(pipeline).exec().then(r => r.map(m =>
this({ ...m, [path]: m[path].map(r => rel(r)) })
));
};
const Item = mongoose.model('Item', itemSchema);
const ItemTag = mongoose.model('ItemTag', itemTagSchema);
const log = body => console.log(JSON.stringify(body, undefined, 2));
(async function() {
try {
const conn = await mongoose.connect(uri);
// Clean data
await Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()));
// Create tags and items
const tags = await ItemTag.insertMany(
["movies", "funny"].map(tagName => ({ tagName }))
);
const item = await Item.create({
"title": "Something",
"description": "An item",
tags
});
// Query with our static
let result = (await Item.lookup({
path: 'tags',
query: { 'tagName': { '$in': [ 'funny', 'politics' ] } }
})).pop();
log(result);
await mongoose.disconnect();
} catch(e) {
console.error(e)
} finally {
process.exit()
}
})()
what you are asking for isn't directly supported but can be achieved by adding another filter step after the query returns.
first, .populate( 'tags', null, { tagName: { $in: ['funny', 'politics'] } } ) is definitely what you need to do to filter the tags documents. then, after the query returns you'll need to manually filter out documents that don't have any tags docs that matched the populate criteria. something like:
query....
.exec(function(err, docs){
docs = docs.filter(function(doc){
return doc.tags.length;
})
// do stuff with docs
});
Try replacing
.populate('tags').where('tags.tagName').in(['funny', 'politics'])
by
.populate( 'tags', null, { tagName: { $in: ['funny', 'politics'] } } )
Update: Please take a look at the comments - this answer does not correctly match to the question, but maybe it answers other questions of users which came across (I think that because of the upvotes) so I will not delete this "answer":
First: I know this question is really outdated, but I searched for exactly this problem and this SO post was the Google entry #1. So I implemented the docs.filter version (accepted answer) but as I read in the mongoose v4.6.0 docs we can now simply use:
Item.find({}).populate({
path: 'tags',
match: { tagName: { $in: ['funny', 'politics'] }}
}).exec((err, items) => {
console.log(items.tags)
// contains only tags where tagName is 'funny' or 'politics'
})
Hope this helps future search machine users.
After having the same problem myself recently, I've come up with the following solution:
First, find all ItemTags where tagName is either 'funny' or 'politics' and return an array of ItemTag _ids.
Then, find Items which contain all ItemTag _ids in the tags array
ItemTag
.find({ tagName : { $in : ['funny','politics'] } })
.lean()
.distinct('_id')
.exec((err, itemTagIds) => {
if (err) { console.error(err); }
Item.find({ tag: { $all: itemTagIds} }, (err, items) => {
console.log(items); // Items filtered by tagName
});
});
#aaronheckmann 's answer worked for me but I had to replace return doc.tags.length; to return doc.tags != null; because that field contain null if it doesn't match with the conditions written inside populate.
So the final code:
query....
.exec(function(err, docs){
docs = docs.filter(function(doc){
return doc.tags != null;
})
// do stuff with docs
});

Node mongoose populate with condition not returns the expected results

I am trying to use the query conditions in populate method.
if a condition is used then still all the records are populated but ones that don't satisfy the condition have the populated field is set to null,
For example:
var query = BookModel.find();
query.populate('author','name',{name:'author1'});
query.find(function(err,books){
console.log(books);
});
Then the output is:
[ { author: { _id: 4ea0db52e09aa6aad2e831fe, name: 'author1' },
title: 'book1',
_id: 4ea0dbebc191848704000005 },
{ author: null,
title: 'book2',
_id: 4ea0dbebc191848704000006 } ,
{ author: null,
title: 'book3',
_id: 4ea0dbebc191848704000007 } ,
{ author: null,
title: 'book4',
_id: 4ea0dbebc191848704000008 } ]
However, I expect only the 1st record in the output result. How can i solve this problem?
I had same problem which you are facing and I tried the above code but it did not help me solve the problem. I found a way to do it just like you want. You cannot do this kind of filter by using populate, you have to use raw query of mongodb to filter out your data.
// db['result'] is the name of database
db['result'].aggregate([
// this is just like populate this populate the user field
{
$lookup:{
from:'users',
localField:'user',
foreignField:'_id',
as:'user'
}
},
// unwind convert the array to objects to apply filter on it
{
$unwind:{
preserveNullAndEmptyArrays : true, // this remove the object which is null
path : "$user"
}
},
// in match you have to define your condition this check if the user has role equals to 3
{
$match:{
"user.role": 3
}
},
{
// this provide pagination
$facet: {
edges: [
{ $skip: sk },
{ $limit: lm },
],
/* pageInfo: [
{ $group: { _id: null, count: { $sum: 1 } } },
],*/
},
}
], function (err, result) {
if (err) {
// this res is send if there is some error
console.log(err);
callback(500, err)
} else {
// you get the data
console.log(result);
callback(200, result)
}
});
Look if this can helps you, I need more information about the problem, that's a possible solution.
BookModel.find()
.populate({
path: 'author',
match: { author: 'author1' },
select: 'name' // I'm suppossing you want to select "name" field, if not, delete this line.
}).find(function(err,books){
console.log(books);
});
or you can do this:
var query = BookModel.find({name: 'author1'});
query.populate('author','name');
query.find(function(err,books){
console.log(books);
});
You could add an extra query in your results to filter out the nulls:
var query = BookModel.find();
query.populate('author','name',{name:'author1'});
query.find(function(err, books){
books = books.filter(function(b){ return b.author; });
console.log(books, null, 4);
});
Console output
[
{
"author": {
"_id": "4ea0db52e09aa6aad2e831fe",
"name": "author1"
},
"title": "book1",
"_id": "4ea0dbebc191848704000005"
}
]

Categories