Adding Objects to Array of Objects in a Document using Mongoose.js - javascript

I've looked in stackoverflow, however I've not found the answer. I'm trying to add Object to a New(empty) Array in my local mongodb that is not a duplicate. I also want to update this Array with other Objects.
I've looked at $push and $addToSet, the examples are using an "id" (_id) which wouldn't be created until I add my first Object.
I'm using Node.js, Mongoose.js, Mongodb, Express.js.
My Schema is:
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var barSchema = new Schema({
location: [{
name: String,
city: String,
total: Number
}]
});
var Bar = mongoose.model('Bar', barSchema);
module.exports = Bar;
I've tried to use this;
var newBar = bar({
location: [{ "name": req.body.bar, "city": req.body.city, "total": 0 }] });
newBar.save(function(err) {
if (err) throw err;
});
I've also used the $push with success but in this case I've not got an "id"
user.findByIdAndUpdate(req.user._id, { $push: {
barlist: { "name": req.body.bar,
"rsvp": true } } },
function(err, user) { });
Which gives back this;
{
"_id" : ObjectId("######"),
"location" : [
{
"name" : "1st Bar",
"city" : "Boston",
"total" : 0,
"_id" : ObjectId("#########")
}
],
"__v" : 0
}
{
"_id" : ObjectId("######"),
"location" : [
{
"name" : "2nd Bar",
"city" : "Boston",
"total" : 0,
"_id" : ObjectId("#########")
}
],
"__v" : 0
}
However I am trying to get this;
{
"_id" : ObjectId("#######"),
"location" : [
{
"name" : "Biddy Early's",
"city" : "Boston",
"total" : 0
},
{
"name" : "Some Bar Name",
"city" : "Boston",
"total" : 0
}
]
}

Please know there may be better ways to do this, but... The first thing you want to do in this case is create a new instance of your schema or 'model'. Based on what your code looks like you might want to consider something like this;
var newBar = bar({
"city": req.body.city,
location: [{
"bar": req.body.bar,
"total": 0
}]
});
newBar.save(function(err) {
if (err) throw err;
});
Since if you are looking for bars or restaurants in the same city you might want to have a common key/value pair to '.find()' or '.update()' depending on what you want to do with it.
From here, you will want to look into what you mentioned before with '$addToSet' so that you wouldn't be adding duplicate bars to your 'location' array. So for instance;
bar.findOneAndUpdate({'city':req.body.city},
{'$addToSet': { location: {
'bar': req.body.bar, 'total': 0 } } },
function(err, b) {
if (err) throw err;
console.log(b);
});
Consider using a strategy like if/else to determine if the city name exists, if it does then you would utilize the '$addToSet'. Else, if it didn't exist you would utilize the new model like the example I used.

Related

How to update a document to embed an existing document from a different collection in mongoose

I have two schemas called Fruit and Person and their collections are fruits and people respectively. They both belong in the db called fruitsDB.
Fruit schema:
const fruitSchema = new mongoose.Schema({
name: String,
score: Number,
review: String
});
const Fruit = mongoose.model('Fruit', fruitSchema);
Person schema:
const personSchema = new mongoose.Schema({
name: String,
age: Number,
favouriteFruit: fruitSchema //creating a relationship with the fruitschema
});
const Person = mongoose.model('Person', personSchema);
I have already inserted some docs in to the fruits collection and this is how it looks like:
{ "_id" : ObjectId("5f0ef3efcb6ec717ecda8049"), "name" : "Apple", "score" : 8, "review" : "Pretty solid as a fruit.", "__v" : 0 }
{ "_id" : ObjectId("5f0efa8ee37b6309d8d968fe"), "name" : "Kiwi", "score" : 10, "review" : "The best fruit!", "__v" : 0 }
{ "_id" : ObjectId("5f0efa8ee37b6309d8d968ff"), "name" : "Orange", "score" : 4, "review" : "Too sour for me!", "__v" : 0 }
{ "_id" : ObjectId("5f0efa8ee37b6309d8d96900"), "name" : "Banana", "score" : 7, "review" : "It's nice", "__v" : 0 }
{ "_id" : ObjectId("5f11bc01574df316f4072a46"), "name" : "Pineapple", "score" : 8, "review" : "Great fruit!", "__v" : 0 }
And this is how the people collection looks like:
{
"_id" : ObjectId("5f11b3e1be240f0c50ca3b26"),
"name" : "John",
"age" : 37,
"__v" : 0
}
{
"_id" : ObjectId("5f11b62c2ae86609ece9287f"),
"name" : "Amy",
"age" : 12,
"__v" : 0
}
If "John" had a favourite fruit, I want to embed one 'fruit' doc (for eg: Kiwi) from the 'fruits' collection for the "John" field as the favouriteFruit field. Like this:
{
"_id" : ObjectId("5f11b3e1be240f0c50ca3b26"),
"name" : "John",
"age" : 37,
"favouriteFruit": {
"_id" : ObjectId("5f0efa8ee37b6309d8d968fe"),
"name" : "Kiwi",
"score" : 10,
"review" : "The best fruit!",
"__v" : 0
},
"__v" : 0
}
How can I do this in mongoose? How can I update the "John" document that way to embed an existing document "Kiwi" from the fruits collection?
I think there is a workaround for this problem. The whole problem can be broken into 2 parts.
Retrieving the desired fruit from fruits collection.
Updating the desired document with the retrieved fruit document from step one.
Both the step can be performed using Mongoose.
Fruit.findOne({name: "Kiwi"}, function(err, fruit) {
if(err) {
console.log("Error finding Fruit.");
}
else {
Person.updateOne({name: "John"}, {favouriteFruit: fruit }, function(err) {
if (err) {
console.log(err);
} else {
mongoose.connection.close();
console.log("Successfully updated document.");
}
});
}
});
This is not an answer, but I wanted to sort out a misunderstanding and the comment box is limited.
favouriteFruit: fruitSchema //creating a relationship with the fruitschema
This above is not a relationship, it's a subdocument using the fruitSchema.
If you were to add kiwi as a favourite fruit, then it would be a completely different kiwi, only existing as a subdocument
in the Person-document. Updates to the kiwi-document in the fruits-collection would not reflect to this kiwi.
While it's probably not what you want right now, it is still very often the most desirable pattern with mongodb, because you don't have to even think when retrieving data. Duplicate data doesn't matter much. MongoDb is non-relational.
But you can still get related documents by using mongoose populate:
// You make the schema like this:
const personSchema = new mongoose.Schema({
name: String,
age: Number,
favFruit: { type: ObjectId, ref: 'Fruit' } // This will store nothing but the object-id
});
You can still save it as you would normally
const kiwi = await Fruit.findOne({ name: "kiwi" }).exec();
const ben = new Person({ name: "Ben", age: 26 });
person.favFruit = kiwi;
await ben.save(); // The schema will filter away everything but the id from favFruit.
But later when you retrieve Ben again, you'll find that his fav fruit is nothing but an ObjectID!:
const ben = await Person.findOne({ name: "Ben" }).exec();
console.log( ben.favFruit ); // ObjectID("abc123...")
So now you need to populate the field when you want to use it:
const ben = await Person.findOne({ name: "Ben" }).populate("favFruit").exec();
console.log( ben.favFruit ); // { _id: ObjectID("abc123..."), name: "Kiwi", .... }
Mongoose will make two db-queries to make this work. (It's still very fast, but one should be aware.)
I used {$set : {favouriteFruit: kiwi}} instead of {favouriteFruit: kiwi} to update the document
Person.updateOne({name: 'John'}, {$set : {favouriteFruit: kiwi}}, (err)=>{
if(err){
console.log(err);
}
else{
console.log("successfully updated");
}
});

better way to limit properties of a field in aggregate call

So I am trying to perform an aggregate call in MongoDB to lookup a collection but at the same time limit how much data I get from the other collection. Here's my attempt that kind of works:
getFamilyStats: function (req, res) {
Families
.aggregate([
{ $match: { 'centralPath': req.body.centralPath }},
{ $lookup: {
from: 'models',
localField: 'centralPath',
foreignField: 'centralPath',
as: 'models'
}},
{ $unwind: '$models'},
{ $project: {
'models.openTimes.user': 1,
'_id': 1,
'centralPath': 1,
'totalFamilies': 1,
'unusedFamilies': 1,
'oversizedFamilies': 1,
'inPlaceFamilies': 1,
'families': 1
}}
]
).exec(function (err, response){
var result = {
status: 200,
message: response
};
if (err){
result.status = 500;
result.message = err;
} else if (!response){
result.status = 404;
result.message = err;
}
res.status(result.status).json(result.message);
});
},
So the thing that works pretty well is the fact that I can use lookup to "join" data from a different collection, in this case called models. Once I unwind it it pretty much looks like I want it with the exception that I am only interested in one field from that property: models.openTimes and in this particular case actually just one property of that field user. I tried to use project to limit hom much of data i pass through from models, but that forces me to spell out all other fields like so:
_id: 1,
centralPath: 1....
That's not ideal in case that my collection ever expands with new properties. I am looking for a way to limit data from models to just that one field/one property, but get all fields from families collection.
Ideas?
Sample data for families:
{
"_id" : ObjectId("5ae08c75d132ac4442520672"),
"centralPath" : "some path",
"totalFamilies" : 0,
"unusedFamilies" : 0,
"oversizedFamilies" : 0,
"inPlaceFamilies" : 0,
"families" : [],
"__v" : 0
}
Sample data for models:
{
"_id" : ObjectId("5ae08c74d132ac4442520638"),
"centralPath" : "some path",
"openTimes" : [
{
"value" : 8123,
"user" : "ks",
"createdOn" : ISODate("2018-04-25T14:11:00.853Z"),
"_id" : ObjectId("5ae08c74d132ac444252063a")
}
],
"synchTimes" : [
{
"value" : 208649,
"user" : "ks",
"createdOn" : ISODate("2018-04-25T16:42:42.933Z"),
"_id" : ObjectId("5ae0b0028c2e3b192a3e9dc5")
}
],
"modelSizes" : [
{
"value" : 21483520,
"user" : "ks",
"createdOn" : ISODate("2018-04-25T14:11:00.787Z"),
"_id" : ObjectId("5ae08c74d132ac4442520639")
}
],
"__v" : 0
}
Starting with MongoDB v3.6 you can limit the fields returned by a $lookup directly:
db.families.aggregate([
{ $match: { 'centralPath': 'some path' } },
{ $lookup: {
from: 'models',
let: { centralPath: '$centralPath' }, // remember the value of the "centralPath" field
pipeline: [
{ $match: { $expr: { $eq: [ '$centralPath', '$$centralPath' ] } } }, // this performs the "join"
{ $project: { _id: 0, 'openTimes.user': 1 } } // only retrieve the "openTimes.user" field
],
as: 'models'
}},
{ $unwind: '$models'},
]);
You can try below aggregation in 3.4 and above version.
$addFields to return the only field from joined collection while keeping existing fields and $project with exclusion to drop the joined collection data.
Replace the project stage with addFields and project stage.
Families.aggregate([
existing stages,
{"$addFields":{
"user":"$models.openTimes.user"
}},
{"$project":{"models":0}}
])

Count and Sort On Array Intersection

I have this schema
module.exports = function(conn, mongoose) {
// var autoIncrement = require('mongoose-auto-increment');
var UsersSchema = new mongoose.Schema({
first_name: String,
last_name:String,
sex: String,
fk_hobbies: []
}
, {
timestamps: true
}, {collection: 'wt_users'});
return conn.model('wt_users', UsersSchema);
};
And for example I have these users in data base
{
"_id" : ObjectId("5aca2ac25c1d8adeb4a2dab0"),
first_name:"Pierro",
last_name:"pierre",
sex:"H",
fk_hobbies: [
{
"_id" : ObjectId("5ac9f84d5c1f8adeb4a2da97"),
"name" : "Art"
},
{
"_id" : ObjectId("5ac9f84d5c8d8adeb4a2da97"),
"name" : "Sport"
},
{
"_id" : ObjectId("5ac9f84d9c1d8adeb4a2da97"),
"name" : "Fete"
},
{
"_id" : ObjectId("5acaf84d5c1d8adeb4a2da97"),
"name" : "Série"
},
{
"_id" : ObjectId("6ac9f84d5c1d8adeb4a2da97"),
"name" : "Jeux vidéo"
}
]
},
{
"_id" : ObjectId("5ac9fa075c1d8adeb4a2da99"),
first_name:"jean",
last_name:"mark",
sex:"H",
fk_hobbies: [
{
"_id" : ObjectId("5ac7f84d5c1d8adeb4a2da97"),
"name" : "Musique"
},
{
"_id" : ObjectId("5ac9f24d5c1d8adeb4a2da97"),
"name" : "Chiller"
},
{
"_id" : ObjectId("5ac9f84c5c1d8adeb4a2da97"),
"name" : "Papoter"
},
{
"_id" : ObjectId("5ac9f84d2c1d8adeb4a2da97"),
"name" : "Manger"
},
{
"_id" : ObjectId("5ac9f84d5c1d8adeb4a2da97"),
"name" : "Film"
}
]
},
{
"_id" : ObjectId("5aca0a635c1d8adeb4a2da9d"),
first_name:"michael",
last_name:"ferrari",
sex:"H",
fk_hobbies: [
{
"_id" : ObjectId("5ac9f84d5c1d8adeb4a2ea97"),
"name" : "fashion"
},
{
"_id" : ObjectId("5ac9f84d5c1e8adeb4a2da97"),
"name" : "Voyage"
},
{
"_id" : ObjectId("5ac9f84c5c1d8adeb4a2da97"),
"name" : "Papoter"
},
{
"_id" : ObjectId("5ac9f84d2c1d8adeb4a2da97"),
"name" : "Manger"
},
{
"_id" : ObjectId("5ac9f84d5c1d8adeb4a2da97"),
"name" : "Film"
}
]
},
{
"_id" : ObjectId("5ac9fa074c1d8adeb4a2da99"),
first_name:"Philip",
last_name:"roi",
sex:"H",
fk_hobbies:
[
{
"_id" : ObjectId("5ac7f84d5c1d8adeb4a2da97"),
"name" : "Musique"
},
{
"_id" : ObjectId("5ac9f24d5c1d8adeb4a2da97"),
"name" : "Chiller"
},
{
"_id" : ObjectId("5ac9f84c5c1d8adeb4a2da97"),
"name" : "Papoter"
},
{
"_id" : ObjectId("5ac9f84d2c1d8adeb4a2da97"),
"name" : "Manger"
},
{
"_id" : ObjectId("5ac9f84d5c1d8adeb4a2da97"),
"name" : "Film"
}
]
}
I want to create a mongoose query that match user getted by id, with others users in database according this :
the query will return firstly the users that have the max number of the same hobbies, that is 5, then the users that have the same 4 hobbies ...
I create a solution fully Javascipt / node js, Is there any query with mongo ?
this is my solution
//var user : the current user that search other similar users : jean mark : 5ac9fa075c1d8adeb4a2da99
//var users : all other users
var tab = []
async.each(users, function(item, next1){
var j = 0;
var hobbies = item["fk_hobbies"]
for(var i = 0; i < 5; i++)
{
var index = hobbies.findIndex(x => x["_id"] == user[0]["fk_hobbies"][i]["_id"].toString());
if(index != -1)
j++
}
if(j != 0)
tab.push({nbHob:j, user:item})
next1()
}, function ()
{
var tab2 = tab.sort(compare)
res.json({success:true, data:tab2})
})
function compare(a,b) {
if (a.nbHob > b.nbHob)
return -1;
if (a.nbHob < b.nbHob)
return 1;
return 0;
}
the displayed result is like this
nbHob : represents the number of similar hobbies
{"success":true,"data":[{"nbHob":5,"user":{"_id":"5ac9fa074c1d8adeb4a2da99","u_first_name":"Akram","u_last_name":"Cherif","u_email":"","u_login":"","u_password":"","u_user_type":0,"u_date_of_birth":"","u_civility":0,"u_sex":"H","u_phone_number":"","u_facebook_id":"","u_google_id":"","u_twitter_id":"","u_profile_image":"","u_about":"","u_profession":"","u_fk_additional_infos":[null],"u_budget":0,"u_address":{"country":"France","state":"Paris","city":"TM","zip":76001},"u_fk_hobbies":[{"name":"Musique","_id":"5ac7f84d5c1d8adeb4a2da97"},{"name":"Chiller","_id":"5ac9f24d5c1d8adeb4a2da97"},{"name":"Papoter","_id":"5ac9f84c5c1d8adeb4a2da97"},{"name":"Manger","_id":"5ac9f84d2c1d8adeb4a2da97"},{"name":"Film","_id":"5ac9f84d5c1d8adeb4a2da97"}]}},{"nbHob":3,"user":{"_id":"5aca0a635c1d8adeb4a2da9d","u_first_name":"Chawki","u_last_name":"Gasmi","u_email":"","u_login":"","u_password":"","u_user_type":0,"u_date_of_birth":"","u_civility":0,"u_sex":"H","u_phone_number":"","u_facebook_id":"","u_google_id":"","u_twitter_id":"","u_profile_image":"","u_about":"","u_profession":"","u_fk_additional_infos":[null],"u_budget":{"min":500,"max":850},"u_address":{"country":"","state":"","city":"","zip":0},"u_fk_hobbies":[{"name":"fashion","_id":"5ac9f84d5c1d8adeb4a2ea97"},{"name":"Voyage","_id":"5ac9f84d5c1e8adeb4a2da97"},{"name":"Papoter","_id":"5ac9f84c5c1d8adeb4a2da97"},{"name":"Manger","_id":"5ac9f84d2c1d8adeb4a2da97"},{"name":"Film","_id":"5ac9f84d5c1d8adeb4a2da97"}]}}]}
Your question data seems a bit messed up due to probably far to liberal copy/paste since every hobby has the same ObjectId value. But I can correct that with a full self contained example:
const { Schema } = mongoose = require('mongoose');
const uri = 'mongodb://localhost/people';
mongoose.Promise = global.Promise;
mongoose.set('debug', true);
const hobbySchema = new Schema({
name: String
});
const userSchema = new Schema({
first_name: String,
last_name: String,
sex: String,
fk_hobbies: [hobbySchema]
});
const Hobby = mongoose.model('Hobby', hobbySchema)
const User = mongoose.model('User', userSchema);
const userData = [
{
"first_name" : "Pierro",
"last_name" : "pierre",
"sex" : "H",
"fk_hobbies" : [
"Art", "Sport", "Fete", "Série", "Jeux vidéo"
]
},
{
"first_name": "jean",
"last_name" : "mark",
"sex" : "H",
"fk_hobbies" : [
"Musique", "Chiller", "Papoter", "Manger", "Film"
]
},
{
"first_name" : "michael",
"last_name" : "ferrari",
"sex" : "H",
"fk_hobbies" : [
"fashion", "Voyage", "Papoter", "Manger", "Film"
]
},
{
"first_name" : "Philip",
"last_name" : "roi",
"sex" : "H",
"fk_hobbies" : [
"Musique", "Chiller", "Papoter", "Manger", "Film"
]
}
];
const log = data => console.log(JSON.stringify(data, undefined, 2));
(async function() {
try {
const conn = await mongoose.connect(uri);
await Promise.all(
Object.entries(conn.models).map(([k,m]) => m.remove())
);
const hobbies = await Hobby.insertMany(
[
...userData
.reduce((o, u) => [ ...o, ...u.fk_hobbies ], [])
.reduce((o, u) => o.set(u,1) , new Map())
]
.map(([name,v]) => ({ name }))
);
const users = await User.insertMany(userData.map(u =>
({
...u,
fk_hobbies: u.fk_hobbies.map(f => hobbies.find(h => f === h.name))
})
));
let user = await User.findOne({
"first_name" : "Philip",
"last_name" : "roi"
});
let user_hobbies = user.fk_hobbies.map(h => h._id );
let result = await User.aggregate([
{ "$match": {
"_id": { "$ne": user._id },
"fk_hobbies._id": { "$in": user_hobbies }
}},
{ "$addFields": {
"numHobbies": {
"$size": {
"$setIntersection": [
"$fk_hobbies._id",
user_hobbies
]
}
},
"fk_hobbies": {
"$map": {
"input": "$fk_hobbies",
"in": {
"$mergeObjects": [
"$$this",
{
"shared": {
"$cond": {
"if": { "$in": [ "$$this._id", user_hobbies ] },
"then": true,
"else": "$$REMOVE"
}
}
}
]
}
}
}
}},
{ "$sort": { "numHobbies": -1 } }
]);
log(result);
mongoose.disconnect();
} catch(e) {
} finally {
process.exit();
}
})()
Most of that is just "setup" to re-create the data set, but simply put we're just adding the users and their hobbies and keeping a "unique" identifier for each "unique hobby" by name. This is probably what you actually meant in the question, and it's the sort of model you should be following.
The interesting part is all in the .aggregate() statement, which is how we "query" then "count" the matching hobbies and enable the "server" to sort the results before returning to the client.
Given a current user ( and the last one in the list you included has the most interesting matches ), we then focus on this section of the code:
// Simulates getting the current user to compare against
let user = await User.findOne({
"first_name" : "Philip",
"last_name" : "roi"
});
// Just get the list of _id values from the current user for reference
let user_hobbies = user.fk_hobbies.map(h => h._id );
let result = await User.aggregate([
// Find all users not the current user with at least one of the hobbies
{ "$match": {
"_id": { "$ne": user._id },
"fk_hobbies._id": { "$in": user_hobbies }
}},
// Add the count of matches, "optionally" we are marking the matched
// hobbies in the array as well.
{ "$addFields": {
"numHobbies": {
"$size": {
"$setIntersection": [
"$fk_hobbies._id",
user_hobbies
]
}
},
"fk_hobbies": {
"$map": {
"input": "$fk_hobbies",
"in": {
"$mergeObjects": [
"$$this",
{
"shared": {
"$cond": {
"if": { "$in": [ "$$this._id", user_hobbies ] },
"then": true,
"else": "$$REMOVE"
}
}
}
]
}
}
}
}},
// Sort the results by the "most" hobbies, which is "descending" order
{ "$sort": { "numHobbies": -1 } }
]);
I've commented those steps for you but let's expand on that.
Firstly we presume you have the current user already returned from the database by whatever means you have already done. For the purposes of the rest of the operations, all your really need from that user is the _id of the "User" itself and of course the _id values from each of that user's chosen hobbies. We can do a quick .map() operation as it shown here, but we keep a copy for ease of reference and not repeating that through the remaining code.
Then we get to the actual aggregate statement. The first condition there is the $match, this works like a standard query expression with all the same operators. We want two things from these query conditions:
Get all users except the current user for consideration;
AND where those users contain at least one match on the same hobbies, by _id value.
So the condition for "everyone else" is essentially to supply the $ne "not equal to" operator in argument to the _id value, comparing of course to the current user _id. The second condition to get only those with the same hobbies uses the $in operator against the _id field of the fk_hobbies array. In MongoDB query parlance we denote this as "$fk_hobbies._id" in order to match against the "inner" _id property values.
The $in operator itself takes a "list" as it's argument and compares each value in the list supplied to the property the condition is assigned to. MongoDB itself does not care that fk_hobbies is an array or a single value, and will simply look for an match for anything in the provided list. Think of $in as a short way of writing $or, except you don't need to explicitly include the same property name on every condition.
Now you have the correct documents selected and have discarded any users who do not share any of the same hobbies we can move on to the next stage. Note also that the whole $match considers it logical that you only want those "matching" users. If you actually wanted to see "all users" including those with "no matches", then you can simply omit the whole $match pipeline stage. Your code is discarding anything that was not counted, so this code simply doesn't bother to count anything which "must" have a 0 count.
The $addFields stage pipeline stage is a quick way to "add new fields" to the document returned in results. The main output you want here is the "numHobbies" in addition to the other user details, so this pipeline stage operator is the optimal way to do this, but if you're MongoDB server is a bit older then you can simply specify "all" fields you want to include in addition to any new ones using $project instead.
In order to "count" the number of hobbies in common we essentially use two aggregation operators, which are $setIntersection and $size. Both of these should be available in an MongoDB version you really should be using in production.
In respective order the $setIntersection operator "compares sets" which is in this case the list of _id values within fk_hobbies, both from the current selected user we stored earlier and from the present document being considered in the expression. The result from this operator is the list of values which are the "same" between both lists.
Naturally the $size operator looks at the returned list ( or set ) from $setIntersection and returns the number of entries in that list. This of course is the "matched count".
The next part involves projecting a "re-written" form of the fk_hobbies array. This is totally optional and by my own design for demonstration purposes. "If" you wanted to do what I am doing here as well, then what this bit of code does is adds an additional property to the objects of the fk_hobbies array to indicate where that particular hobby was one of those which matched the list.
I'm saying this is "optional" because I'm actually demonstrating two features available for MongoDB 3.6 only. These involve the usage of $mergeObjects on the inner array elements and the usage of Conditionally Exlcuding Fields.
Stepping through that, since fk_hobbies is an array we need to use the $map operator in order to "reshape" the objects inside it. This operator allows us to process each array member and return a new value based on the transformations we include as it's argument. It's usage is much the same as .map() for JavaScript or any other language which implements a similar operation.
Therefore for each object in the array ( $$this ) we apply the $mergeObjects operator which will "merge" the result of it's arguments. These are provided as the $$this for the current object as it already is, and the second argument in the expression which is doing something new and interesting.
Here we use the $cond operator, which is a "ternary" operator ( or if..then..else expression ) which considers a condition if and then returns either the then argument where that expression was true, or the else expression where it was false. The expression here is another form of $in used as an aggregation expression. In this form the first argument is a singular value $$this._id which will be compared to a list expression in the second argument. That second argument is of course the list of the current user hobby id's we kept earlier, and are using again for comparison.
That usage of $in alone would return either true or false where it was a match. But the extra demonstrated action here is that within the $cond expresion, our else condition for false returns the new and special $$REMOVE value. What this means is that with our "shared" property we are adding to each object in the array, rather than assigning it a value of false where there was no match, we actually don't include that property in the output document at all.
That "optional" part is really just there as a "nice touch" to indicate which "hobbies" were matched in the conditions, rather than simply returning the count. If you like it then use it, and if you don't have MongoDB 3.6 with those features you can simply do that same alteration in the returned documents from the aggregation output anyway:
let result = await User.aggregate([
{ "$match": {
"_id": { "$ne": user._id },
"fk_hobbies._id": { "$in": user_hobbies }
}},
{ "$addFields": {
"numHobbies": {
"$size": {
"$setIntersection": [
"$fk_hobbies._id",
user_hobbies
]
}
}
}},
{ "$sort": { "numHobbies": -1 } }
]);
// map each result after return
result = result.map(r =>
({
...r,
fk_hobbies: r.fk_hobbies.map(h =>
({
...h,
...(( user_hobbies.map(i => i.toString() ).indexOf( h._id.toString() ) != -1 )
? { "shared": true } : {} )
})
)
})
)
Either way, the main thing you wanted out of any $addFields or $project statement was the actual "numHobbies" value indicating the count. And the main reason we did that on the server was so that we can also $sort on the server, which would in turn allow you to add things like $limit and $skip to larger result sets for purposes of paging where it simply would not be practical to get all the results from the collection, even if they were filtered in the initial match or regular query.
Anyhow, from the small sample of documents in the question as also generated in the sample listing, we get a result like this:
[
{
"_id": "5ad6bbe63365bc3428feed8a",
"first_name": "jean",
"last_name": "mark",
"sex": "H",
"fk_hobbies": [
{
"_id": "5ad6bbe63365bc3428feed7d",
"name": "Musique",
"__v": 0,
"shared": true
},
{
"_id": "5ad6bbe63365bc3428feed7e",
"name": "Chiller",
"__v": 0,
"shared": true
},
{
"_id": "5ad6bbe63365bc3428feed7f",
"name": "Papoter",
"__v": 0,
"shared": true
},
{
"_id": "5ad6bbe63365bc3428feed80",
"name": "Manger",
"__v": 0,
"shared": true
},
{
"_id": "5ad6bbe63365bc3428feed81",
"name": "Film",
"__v": 0,
"shared": true
}
],
"__v": 0,
"numHobbies": 5
},
{
"_id": "5ad6bbe63365bc3428feed90",
"first_name": "michael",
"last_name": "ferrari",
"sex": "H",
"fk_hobbies": [
{
"_id": "5ad6bbe63365bc3428feed82",
"name": "fashion",
"__v": 0
},
{
"_id": "5ad6bbe63365bc3428feed83",
"name": "Voyage",
"__v": 0
},
{
"_id": "5ad6bbe63365bc3428feed7f",
"name": "Papoter",
"__v": 0,
"shared": true
},
{
"_id": "5ad6bbe63365bc3428feed80",
"name": "Manger",
"__v": 0,
"shared": true
},
{
"_id": "5ad6bbe63365bc3428feed81",
"name": "Film",
"__v": 0,
"shared": true
}
],
"__v": 0,
"numHobbies": 3
}
]
So there are two users that were returned and we counted the matching hobbies as 5 and 3 respectively and returned the one with the most matched first. You can also see the addition of the "shared" property on each of the matched hobbies to indicate which of the hobbies in each of the returned users lists were also shared with the original user they were compared with.
NOTE: You were probably just "trying things" but your usage of async.each() in your question was not really necessary since none of the inner code is actually "async" itself. Even in the listing here, the only thing you actually need to "await" as an async call after you have the current user to compare is the .aggregate() response itself.
So if at any part of this you were presuming you would be "awaiting requests within a loop", then you were mistaken. Simply ask the database for the results and await their return.
One request to the database is all that is required.
N.B It's also 2018, so you really should start to understand Promises and usage of async/await with them. The code is much cleaner that way and surely any newly developed application should be running in an environment with this support. So "callback helper" libraries like "node async", are a little "old hat" and outmoded in a modern context.

Update multiple documents in MongoDB by altering object in array

I have a simple application with registration/login and it is basically a coursera/udemy type, where the app lists specific courses and users can like them or enroll in them. I have been trying to make a mongodb function that updates a user in the database and since users can like the courses it has to update all courses too (courses have a field "usersLiked", which is an array and keep all user documents which have liked it).
The course structure is the following:
{
"_id" : ObjectId("5977662564aac9f6c8d48884"),
"title" : "Title",
"lecturer" : "Lecturer",
"length" : 30,
"coverPhoto" : "Photo",
"usersLiked": [
{
"_id" : ObjectId("597763e346a7a463cbb8f529"),
"fullname" : "Name",
"username" : "Username",
"password" : "Hash",
"city" : "City",
"street" : "Street",
"website" : "Website"
}
],
"lectures" : [
{
"title" : "Introduction",
"number" : 1,
"url" : "someURL"
}
]
}
And the user structure:
{
"_id" : ObjectId("597763e346a7a463cbb8f529"),
"fullname" : "Name",
"username" : "Username",
"password" : "Hash",
"enrolledCourses" : [
...
]
}
],
"city" : "City",
"street" : "Street",
"website" : "Website"
}
So now I am calling this function when I want to change. It changes the userCollection but in the courseCollection it does nothing, while it should get all courses and if some of them have an object with username(the user's username) in the "usersLiked" array it should modify the user there too.
const updateUser = (username, details) => {
usersCollection
.update({
username: username,
}, {
$set: {
fullname: details.fullname,
city: details.city,
street: details.street,
website: details.website,
},
});
coursesCollection
.updateMany(
{
usersLiked: {
$elemMatch: {
username: username,
},
},
},
{
$set: {
'usersLiked.username': details.username,
'usersLiked.city': details.city,
'usersLiked.street': details.street,
'usersLiked.website': details.website,
},
}
);
};
Your match on the course update looks valid but you are trying to set values into an array and according to the Mongo docs you need to provide an array indexer or a positional operator.
The following will allow the set command to operate on the first element within the usersLiked array which matches the given username.
coursesCollection.updateMany(
{
usersLiked: {
$elemMatch: {
username: username,
},
},
},
{
$set: {
'usersLiked.$.username': details.username,
'usersLiked.$.city': details.city,
'usersLiked.$.street': details.street,
'usersLiked.$.website': details.website
},
}
)
You could also choose which element in the usersLiked array to update e.g. usersLiked.1.username but I suspect each course only has one element in usersLiked for a given username in which case using the $ operator (which means: the first matching array element) should suffice.

Creating different types of circles like GooglePlus

Happy New year to all,
i am creating a social network for doctors using express,angular,node and MongoDb, fairly new to these technologies, i have been thinking to create different type circles for a particular user just like googleplus where we have different circles like friends and family, here i want to create circles of doctors with different specialities, and wondering how i should handle it on backend
Schema:
var NetworkSchema = new Schema({
UserID: {
type: Schema.Types.ObjectId,
ref: 'User'
NetworkList: [ ],
});
so far what i have been thinking is to insert different objects having field name of different specialities like- heartSpecialist, Cardiologists etc. inside NetworkList array, it should be generic so, i left NetworkList array - empty, may be i not much correct with this logic, here is document
{
"NetworkList" : [
HeartSpecialist: [ //ObjectId's of HeartSpecialist ],
CardioLogists: [//ObjectId's of CardioLogists ]
..
],
"UserID" : ObjectId("54aa46ef65c266341494a528"),
"_id" : ObjectId("54aa46ef65c266341494a529")
}
when i used discriminator for schema type like:
var portalSchema = new Schema(),
CircleType = new Schema({
ID: {
type: Schema.Types.ObjectId,
ref: 'User'
}
});
CIRCLE = Portal.discriminator('CIRCLE', CircleType);
function logger(label,content) {
console.log(
'%s:\n%s\n', label,
util.inspect( content, false, 8, false ) );
}
async.waterfall([
function(callback){
var circleObj = new CIRCLE({'ID': req.body.Fid});
circleObj.save(function(err,circleObj) {
logger('Circles', circleObj);
callback(err,circleObj);
});
},
function(circleObj,callback){
var get = {'UserID':req.body.UserID};
var up = {$push:{'NetworkList':circleObj}};
Network.update(get,up,{upsert:true},function(err,user){
if (err) {
console.log(err);
return err;}});
]);
Resulted Document:
{
"NetworkList" : [
{
"__v" : 0,
"ID" : ObjectId("54a6de049754e5940c97435a"),
"__t" : "CIRCLE",
"_id" : ObjectId("54aa48ccba36d7ac1810e832")
},
],
"UserID" : ObjectId("54aa46ef65c266341494a528"),
"_id" : ObjectId("54aa46ef65c266341494a529")
}
I know this logic is whole different of what i described above or what i want, so far i have came with this where i can differentiate with type doctor having '__t' in above document, may be i am not looking for code but a logic to achieve circle type functionality.
Thank you
EDIT:
problem:-when a doctor of same circle type is added, a new object of same circle type is being stored in database which leads to redundancy
{
"NetworkList" : [
{
"__v" : 0,
"ID" : ObjectId("54aa46ef65c266341494a528"),
"__t" : "CIRCLE", //two different objects of same circle
"_id" : ObjectId("54aa4a40bf50c41c14e389c4")
},
"54aa46ef65c266341494a528",
{
"__v" : 0,
"ID" : ObjectId("54a6de049754e5940c97435a"),
"__t" : "CIRCLE",
"_id" : ObjectId("54aa4add024eebd81fe009a2")
},
],
"UserID" : ObjectId("54a8291f8ed9a2b41eb164ff"),
"__v" : 0,
"_id" : ObjectId("54a8291f8ed9a2b41eb16500")
}

Categories

Resources