Aggregate using external references in mongodb - javascript

I'd like to aggregate results in a mongodb query, however I am not able to accomplish that in the case modeled like the following example from mongodb.org:
{
_id: "oreilly",
name: "O'Reilly Media",
founded: 1980,
location: "CA"
}
{
_id: 123456789,
title: "MongoDB: The Definitive Guide",
author: [ "Kristina Chodorow", "Mike Dirolf" ],
published_date: ISODate("2010-09-24"),
pages: 216,
language: "English",
publisher_id: "oreilly"
}
{
_id: 234567890,
title: "50 Tips and Tricks for MongoDB Developer",
author: "Kristina Chodorow",
published_date: ISODate("2011-05-06"),
pages: 68,
language: "English",
publisher_id: "oreilly"
}
My result should have this structure:
{
publishers: [
{
_id: "oreilly",
name: "O'Reilly Media",
founded: 1980,
location: "CA"
books: [
{
_id: 123456789,
title: "MongoDB: The Definitive Guide",
author: [ "Kristina Chodorow", "Mike Dirolf" ],
published_date: ISODate("2010-09-24"),
pages: 216,
language: "English"
},
{
_id: 234567890,
title: "50 Tips and Tricks for MongoDB Developer",
author: "Kristina Chodorow",
published_date: ISODate("2011-05-06"),
pages: 68,
language: "English",
publisher_id: "oreilly"
}
]
}
]
}
But I cannot manage to use the $aggregate query on the books table to populate the publisher reference, and I don't even know if it is possible.
What are the proper strategies to get this kind of result?

One way to get the result is to simulate the join by iterate each publisher to find his books and then construct your result :)
sample in mongo shell:
var publishers = [];
var struct = {};
struct.publishers = publishers
db.publisher.find().forEach( function(publisher) {
publisher.books = db.books.find({publisher_id: publisher._id}).toArray()
publishers.push(publisher)
})
printjson(struct)
sample for drivers:
You can use db.eval to run a query as Server-side Javascript.
db.eval:
connection.db.eval(function construct(publisher){return struct}), arg ,function (e, result) {result});
db.eval function:
db.eval(function construct(publisher) {
var publishers = [];
var struct = {};
var query = publisher ? {_id:publisher} : {}
struct.publishers = publishers
db.publisher.find(query).forEach( function(publisher) {
publisher.books = db.books.find({publisher_id: publisher._id}).toArray()
publishers.push(publisher)
})
return struct
}
,null // argument to pass into function for filter, e.g. 'oreilly'
,{nolock:true})
sample with mongoose: (on collection name book)
var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/book');
mongoose.connection.on('connected', function() {
mongoose.connection.db.eval(function construct(publisher) {
var publishers = [];
var struct = {};
var query = publisher ? {_id:publisher} : {}
struct.publishers = publishers
db.publisher.find(query).forEach( function(publisher) {
publisher.books = db.books.find({publisher_id: publisher._id}).toArray()
publishers.push(publisher)
})
return struct
}
,'oreilly'
,{nolock:true}, function(e,result) {
if(e) console.log(e);
console.log(JSON.stringify(result));
})
})

Related

Recursive Fetch Request

I am trying to write a recursive fetch function. I am calling an endpoint that accepts a name param and returns their role and an array of direct-subordinates like so:
{
role: "CEO",
direct-subordinates: [ "john smith", "bob jones" ]
}
I then want to call the function again to request the same data for each subordinate.
Here is the code I have:
export const fetchEmployee = async (name) => {
let url = `https://url.com/to/employees/endpoint/${name}`
let req = await fetch(url)
let json = await req.json()
return json
}
export const recursiveFetchEmployees = async (initialName) => {
let json = await fetchEmployee(initialName)
const role = json[0]
const subordinates = json[1]
if (subordinates) {
return {
name: initialName,
role: role,
subordinates: subordinates['direct-subordinates'].map(async (subordinate) => {
let result = await recursiveFetchEmployees(subordinate)
return result
}),
}
} else {
return {
name: initialName,
role: role,
}
}
}
This almost works when called with recursiveFetchEmployees(employeeName).then((resp) => console.log(resp)) but the result is:
name: "robert robertson",
role: "CEO",
subordinates: (2) [Promise, Promise],
How do I change this so the function works its way down the employee hierarchy recursively producing a result like this:
{
name: "robert robertson",
role: "CEO",
subordinates: [
{
name: "john smith",
role: "Marketing Manager",
subordinates: [{
name: "mary doyle",
role: "employee",
}]
},
{
name: "bob jones",
role: "Development Manager",
subordinates: [{
name: "barry chuckle",
role: "Development Lead",
subordinates: [{
name: "billy bob",
role: "Developer",
}]
}]
},
],
}
Thanks in advance for any help or advice.
EDIT / UPDATE
Thanks to the fine answer given by #trincot the problem I had was resolved but it introduced another problem. I need to check for and filter out duplicates in the returned results. I introduced a uniqueNameArray that gets initialised with an empty array and on every call, it adds the name of the current initialName param if it does not already exist in the array. Here is my code:
export const recursiveFetchEmployees = async (initialName, uniqueNameArray = []) => {
if (!uniqueNameArray.includes(initialName)) {
uniqueNameArray.push(initialName)
let json = await fetchEmployee(initialName)
const role = json[0]
const subordinates = json[1]
if (subordinates) {
return {
name: initialName,
role: role,
subordinates: await Promise.all(
subordinates['direct-subordinates'].map(
(subordinate) => subordinate && recursiveFetchEmployees(subordinate, uniqueNameArray)
)
),
}
} else {
return {
name: initialName,
role: role,
}
}
}
}
Unfortunately when there is a duplicate it still gets called in the map function resulting in a subordinates array that looks like this:
{
name: "bob jones",
role: "Development Manager",
subordinates: [
{
name: "barry chuckle",
role: "Development Lead",
subordinates: [{
name: "billy bob",
role: "Developer",
}]
},
{
name: "james jameson",
role: "Development Lead",
subordinates: [{
name: "joey joe joe junior",
role: "Developer",
}]
},
undefined, // <-- This is where there was a duplicate
]
},
Is there a way to omit it from the promise list? What ive done above should do that as far as I can tell so Im not sure why it still returns an undefined response.
As always, any help is appreciated, thanks!
It is no surprise that .map(async ... returns an array of promise objects, as an async function always returns a promise.
You could use Promise.all here:
subordinates: await Promise.all(
subordinates['direct-subordinates'].map(recursiveFetchEmployees)
),
Note also that you can just pass recursiveFetchEmployees as callback argument to .map. There is no need to create that wrapper function.

Looping through nested data and displaying object properties and values

In my React app, I'm looking for a clean way to loop through the following dynamic data structure and display the object properties and values.
Sample data:
data: {
company: [
{
company_name: "XYZ Firm",
company_email: "hello#xyz.com",
company_phone: 91982712,
}
],
shareholders: [
{
shareholder_name: "Lin",
percentage: 45
},
{
shareholder_name: "Alex",
percentage: 10
},
],
employees: [
{
employee_name: "May",
employee_email: "may#xyz.com"
},
]
}
The output I want is:
company_name: XYZ Firm
company_email: hello#xyz.com
company_phone: 91982712
shareholder_name: Lin
shareholder_percentage: 45
shareholder_name: Alex
shareholder_percentage: 10
employee_name: May
employee_email: may#xyz.com
This is what I've tried so far:
//data contains the entire object
const profileInfo = Object.keys(data).map(key => {
let profileSection = [];
for (let values of data[key]) { //retrieve the objects of each "section" e.g., company, shareholders
Object.keys(values).map(key => {
profileSection.push(<p>{key}: {values[key]}</p>);
})
}
return profileSection;
})
I'm able to achieve the intended results but I'm not sure if it's the best solution in terms of performance. Having nested Object.keys().mapseems a bit off to me.
Note: User will be able to add more shareholders/employees.
Here is a somewhat shorter version using Object.values() and Object.entries().
var data = { company: [ { company_name: "XYZ Firm", company_email: "hello#xyz.com", company_phone: 91982712, } ], shareholders: [ { shareholder_name: "Lin", percentage: 45 }, { shareholder_name: "Alex", percentage: 10 }, ], employees: [ { employee_name: "May", employee_email: "may#xyz.com" }, ] };
let profileInfo = [];
Object.values(data).flat().forEach((item) => {
Object.entries(item).forEach(([key, value]) => {
profileInfo.push(key + ": " + value);
});
});
console.log(profileInfo);

Empty return of variable

I have this code:
var results_ = ger.recommendations_for_person('movies', query.name, {actions: {likes: 1}});
var recommendations = results_.recommendations;
return recommendations;
ger.recommendations_for_person('movies', query.name, {actions: {likes: 1}});
is meant to return something as such.
{
"recommendations": [
{
"thing": "spiderman",
"weight": 1.6666666666666667,
"last_actioned_at": "2019-05-17T23:06:54+01:00",
"last_expires_at": "2020-06-06T01:00:00+01:00",
"people": [
"bob",
"alice"
]
},
{
"thing": "xmen",
"weight": 1.6666666666666667,
"last_actioned_at": "2019-05-17T23:06:54+01:00",
"last_expires_at": "2020-06-06T01:00:00+01:00",
"people": [
"alice",
"bob"
]
},
{
"thing": "barbie",
"weight": 1,
"last_actioned_at": "2019-05-17T23:06:54+01:00",
"last_expires_at": "2020-06-06T01:00:00+01:00",
"people": [
"alice"
]
},
{
"thing": "avengers",
"weight": 0.6666666666666667,
"last_actioned_at": "2019-05-17T23:06:54+01:00",
"last_expires_at": "2020-06-06T01:00:00+01:00",
"people": [
"bob"
]
}
],
"neighbourhood": {
"bob": 0.6666666666666667,
"alice": 1
},
"confidence": 0.002462038997842016
}
And it works perfectly if I just return results.
But why can't I return recommendations.It returns a blank screen.
My question is different from How do I return the response from an asynchronous call? because for one thing ,I am using nodejs not ajax.It is meant to be synchronous.
This is the full code for `
recc_.js:
var http = require('http'),
url = require('url');
// require the ger objects
http.createServer(function (request, response) {
var query = url.parse(request.url,true).query;
var g = require('../ger')
// Create an Event Store Manager (ESM) that stores events and provides functions to query them
var esm = new g.MemESM()
// Initialize GER with the esm
var ger = new g.GER(esm);
ger.initialize_namespace('movies')
.then( function() {
return ger.events([
////RECCOMMENDATION LISTS
{
namespace: 'movies',
person: 'bob',
action: 'likes',
thing: 'xmen',
expires_at: '2020-06-06'
},
{
namespace: 'movies',
person: 'bob',
action: 'likes',
thing: 'avengers',
expires_at: '2020-06-06'
},
{
namespace: 'movies',
person: 'bob',
action: 'likes',
thing: 'spiderman',
expires_at: '2020-06-06'
},
{
namespace: 'movies',
person: 'alice',
action: 'likes',
thing: 'xmen',
expires_at: '2020-06-06'
},
{
namespace: 'movies',
person: 'alice',
action: 'likes',
thing: 'spiderman',
expires_at: '2020-06-06'
},
{
namespace: 'movies',
person: 'alice',
action: 'likes',
thing: 'barbie',
expires_at: '2020-06-06'
},
////RECCOMMENDATION LISTS
])
})
.then( function() {
// What things might alice like?
var results_ = ger.recommendations_for_person('movies', query.name, {actions: {likes: 1}});
var recommendations = results_.recommendations;
//var results = results_[reccomendations.map(({ thing }) => thing)];
return recommendations;
})
.then( function(recommendations) {
response.end(JSON.stringify(recommendations,null,2))
response.end("\nRecommendations For 'alice'")
})
}).listen(8080);
console.log(' server running ok http://127.0.0.1:8080/');
The implementation for var results_ = ger.recommendations_for_person('movies', query.name, {actions: {likes: 1}}) is :
recommendations_for_person: (namespace, person, configuration = {}) ->
configuration = #default_configuration(configuration)
actions = configuration.actions
#first a check or two
#find_events(namespace, actions: Object.keys(actions), person: person, current_datetime: configuration.current_datetime, size: 100)
.then( (events) =>
return {recommendations: [], confidence: 0} if events.length < configuration.minimum_history_required
return #generate_recommendations_for_person(namespace, person, actions, events.length, configuration)
)
I GOT IT!!!
I just had to do:
.then( function() {
// What things might alice like?
return results_ = ger.recommendations_for_person('movies', query.name, {actions: {likes: 1}});
})
.then(function(results_) {
var recommendations = results_.recommendations;
//var results = results_[reccomendations.m ap(({ thing }) => thing)];
return recommendations;
})
.then( function(recommendations) {
response.end(JSON.stringify(recommendations,null,2))
response.end("\nRecommendations For 'alice'")
})
ie using Promises

Mongoose get collection, where ref [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
});

Build JS arrays by key into one - find a best solution

What's the best solution to mapping 2 multiple arrays to build one by key?
I have 1 array with users who have their profile data like
var users = [{id:5, name:'Alex'}, {id:17, name:'Tom'}, {id:11, name:'John'}];
Also I have another one array of cars with key user_id To determine which machine belongs to which user.
var cars = [{id:333, name:'Nissan', user_id:11}, {id:444, name:'Toyota', user_id:17}, {id:555, name:'BMW', user_id:999}];
So we can see that Tom have Toyota and John have Nissan.
So result should be
a new array with mapped result
[{
"profile": {
"id": 17,
"name": "Tom"
},
"car": {
"id": 444,
"name": "Toyota",
"user_id": 17
}
}, {
"profile": {
"id": 11,
"name": "John"
},
"car": {
"id": 333,
"name": "Nissan",
"user_id": 11
}
}]
My solution is use forEach throw users and sub forEach throw cars and there compare user.id with car.user_id
https://jsfiddle.net/r7qwke1f/37/
You could use a two loop approach instead of a nested loop approach by collecting first all users in a hash table anbd then iterate all cars and if a user is available, then create a new result set.
var users = [{ id: 5, name: 'Alex' }, { id: 17, name: 'Tom' }, { id: 11, name: 'John' }],
cars = [{ id: 333, name: 'Nissan', user_id: 11 }, { id: 444, name: 'Toyota', user_id: 17 }, { id: 555, name: 'BMW', user_id: 999 }],
hash = {},
result = [];
users.forEach(function (user) {
hash[user.id] = user;
});
cars.forEach(function (car) {
if (hash[car.user_id]) {
result.push({ profile: hash[car.user_id], car: car });
}
});
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Another solution
const mappedUsersCars = users.map((user) => ({
profile: user,
car: cars.filter((car) => car.user_id === user.id)[0]
}))
You can use reduce() and find() methods to get desired result.
var users = [{id:5, name:'Alex'}, {id:17, name:'Tom'}, {id:11, name:'John'}];
var cars = [{id:333, name:'Nissan', user_id:11}, {id:444, name:'Toyota', user_id:17}, {id:555, name:'BMW', user_id:999}];
var r = users.reduce(function(r, e) {
var car = cars.find(a => a.user_id == e.id);
if(car) r.push({profile: e, car: car});
return r;
}, [])
console.log(r)
There are basically two methods you would want to use. You want to map the users to the cars, so you want to find a car for the user you are referring to
const result = users.map((user) => {
const car = cars.find(car => car.user_id === user.id);
return {
profile: user,
car,
}
})

Categories

Resources