Moongoose update subdocument array object - javascript

I want to update one object of an subdoc array with findByIdAndUpdate by parent id and subdoc object id. When executing this code, I got this error:
The positional operator did not find the match needed from the query.
When I use updateOne with filter parameter, it works. But I would like to get the updated document to return as json for rest api.
Is there any way to get the updated document?
My Code:
Subject.findByIdAndUpdate(
{ _id: req.params.subjectId, "bookmarks._id": req.params.bookmarkId },
{
$set: {
"bookmarks.$.uri": req.body.uri
}
},
{ new: true }
)
Schema:
{
"_id": "5e7fbfc05ff6be1446b51af7",
"user_id": "5e7e68c3fd5e9404ce6a14a3",
"title": "Hello World",
"date": "2020-03-28T21:21:04.434Z",
"bookmarks": [
{
"date": "2020-03-28T21:21:20.806Z",
"_id": "5e7fbfd05ff6be1446b51afa",
"uri": "lorem ipsum"
},
{
"date": "2020-03-28T21:21:21.433Z",
"_id": "5e7fbfd15ff6be1446b51afb",
"uri": "lorem ipsum"
}
]
}

You should be using .findOneAndUpdate() :
As your req.params.subjectId and req.params.bookmarkId are strings & respective fields in your DB will be of type ObjectId() - So convert strings to ObjectId() using below code :
const mongoose = require('mongoose');
const _id = mongoose.Types.ObjectId(req.params.subjectId);
const bookmarkId = mongoose.Types.ObjectId(req.params.bookmarkId);
Subject.findOneAndUpdate(
{ _id: _id, "bookmarks._id": bookmarkId },
{
$set: {
"bookmarks.$.uri": 'new new'
}
},
{ new: true }
)
Your issue should be mongoose's .findByIdAndUpdate() does takes in just one string value & internally converts it into {_id : ObjectId(req.params.subjectId)} to use .findOneAndUpdate(), it's just kind of wrapper.

Related

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.

Target First Object in Array with Passed MongoDB Query

I am using this mongoDB query passed via a POST request to return a subset of data:
body: any = {"services.history": { "$elemMatch": { "status": "orientation", "enrolled" : true } }};
This is working as expected. However, we're changing our implementation to target a status and a check that it's the first item in the "history" array. How could I accomplish this with a modification of the above query?
I thought this might work, but no go:
body: any = {"services.history[0]": { "$elemMatch": { "status": "orientation", "enrolled" : true } }};
I also tried dot notation, still no go:
body: any = {"services.history.0": { "$elemMatch": { "status": "orientation", "enrolled" : true } }};
How can I target just the first item in the array with this kind of query in a POST request? In other words, I only want it to return true if the elemMatch matches AND it's the first item in the "history" array. I only need to run this check on the first object on the history array, since I know that'll be the latest (most relevant) data object in the backend array.
The data I'm querying looks like this:
"history": [
{
"status": "oritentation",
"endDate": "2012-09-26T06:00:00.000Z",
"startDate": "2011-03-26T06:00:00.000Z",
"_id": "1259c4d250502sa434788",
"enrolled": true,
}
]
If you do not have additional fields inside the subdocuments of your array you can do this:
body: any = {"services.history.0": { "status": "orientation", "enrolled" : true } }
If you do have additional fields you can do this instead:
body: any = { "services.history.0.status": "orientation", "services.history.0.enrolled": true }

Create ID with data of other fields

I'm new to mongodb, and I'm using mongoose to validate and order the data (I'm open to change it to MySQL if this doesn't work).
The app will be an e-shop, to buy merchandising related to movies, games, ext.
My schema is as follows:
var productSchema = {
id: {
type: String,
required: true
},
name: {
type: String,
required: true
},
img: {
type: String,
required: true
},
price: {
type: Number,
required: true
},
stock: {
type: Number,
required: true
},
category: {
object: {
type: String,
required: true
},
group: {
type: String,
required: true
},
name: {
type: String,
required: true
}
}
};
This is what I would like to do:
If I have the following data in category:
category.object = "ring"
category.group = "movies"
category.name= "lord of the rings"
I want the id to be made of the first letters of every field in category and a number (the number of the last item added plus 1). In this case, It would be RMLOTR1.
What I'm doing right now
I'm adding a lot of data at the same time, so every time I do it, I made a function that iterates through all the items added and does what I want but...
My question is
Is there a built-in way to do this with mongodb or mongoose, adding the data and creating the id at the same time? I know I can do a virtual, but I want the data to be stored.
Extras
If it's not posible to do this with mongodb, is there a way to do this with MySQL?
Is doing this kind of thing considered a correct/wrong approach?
You are basically looking for a "pre" middleware hook on the "save" event fired by creating new documents in the collection. This will inspect the current document content and extract the "strings" from values in order to create your "prefix" value for _id.
There is also another part, where the "prefix" needs the addition of the numeric counter when there is already a value present for that particular "prefix" to make it distinct. There is a common technique in MongoDB used to "Generate an auto-incrementing sequence field", which basically involves keeping a "counters" collection and incrementing the value each time you access it.
As a complete and self contained demonstration, you combine the techniques as follows:
var async = require('async'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
mongoose.connect('mongodb://localhost/warehouse');
var counterSchema = new Schema({
"type": { "type": String, "required": true },
"prefix": { "type": String, "required": true },
"counter": Number
});
counterSchema.index({ "type": 1, "prefix": 1 },{ "unique": true });
counterSchema.virtual('nextId').get(function() {
return this.prefix + this.counter;
});
var productSchema = new Schema({
"_id": "String",
"category": {
"object": { "type": String, "required": true },
"group": { "type": String, "required": true },
"name": { "type": String, "required": true }
}
},{ "_id": false });
productSchema.pre('save', function(next) {
var self = this;
if ( !self.hasOwnProperty("_id") ) {
var prefix = self.category.object.substr(0,1).toUpperCase()
+ self.category.group.substr(0,1).toUpperCase()
+ self.category.name.split(" ").map(function(word) {
return word.substr(0,1).toUpperCase();
}).join("");
mongoose.model('Counter').findOneAndUpdate(
{ "type": "product", "prefix": prefix },
{ "$inc": { "counter": 1 } },
{ "new": true, "upsert": true },
function(err,counter) {
self._id = counter.nextId;
next(err);
}
);
} else {
next(); // Just skip when _id is already there
}
});
var Product = mongoose.model('Product',productSchema),
Counter = mongoose.model('Counter', counterSchema);
async.series(
[
// Clean data
function(callback) {
async.each([Product,Counter],function(model,callback) {
model.remove({},callback);
},callback);
},
function(callback) {
async.each(
[
{
"category": {
"object": "ring",
"group": "movies",
"name": "lord of the rings"
}
},
{
"category": {
"object": "ring",
"group": "movies",
"name": "four weddings and a funeral"
}
},
{
"category": {
"object": "ring",
"group": "movies",
"name": "lord of the rings"
}
}
],
function(data,callback) {
Product.create(data,callback)
},
callback
)
},
function(callback) {
Product.find().exec(function(err,products) {
console.log(products);
callback(err);
});
},
function(callback) {
Counter.find().exec(function(err,counters) {
console.log(counters);
callback(err);
});
}
],
function(err) {
if (err) throw err;
mongoose.disconnect();
}
)
This gives you output like:
[ { category: { name: 'lord of the rings', group: 'movies', object: 'ring' },
__v: 0,
_id: 'RMLOTR1' },
{ category:
{ name: 'four weddings and a funeral',
group: 'movies',
object: 'ring' },
__v: 0,
_id: 'RMFWAAF1' },
{ category: { name: 'lord of the rings', group: 'movies', object: 'ring' },
__v: 0,
_id: 'RMLOTR2' } ]
[ { __v: 0,
counter: 2,
type: 'product',
prefix: 'RMLOTR',
_id: 57104cdaa774fcc73c1df0e8 },
{ __v: 0,
counter: 1,
type: 'product',
prefix: 'RMFWAAF',
_id: 57104cdaa774fcc73c1df0e9 } ]
To first understand the Counter schema and model, you are basically defining something where you are going to look up a "unique" key and also attach a numeric field to "increment" on match. For convenience this just has a two fields making up the unique combination and a compound index defined. This could just also be a compound _id if so wanted.
The other convenience is the virtual method of nextId, which just does a concatenation of the "prefix" and "counter" values. It's also best practice here to include something like "type" here since your Counter model can be used to service "counters" for use in more than one collection source. So here we are using "product" whenever accessing in the context of the Product model to differentiate it from other models where you might also keep a similar sequence counter. Just a design point that is worthwhile following.
For the actual Product model itself, we want to attach "pre save" middleware hook in order to fill the _id content. So after determining the character portion of the "prefix", the operation then goes off and looks for that "prefix" with the "product" type data in combination in the Counter model collection.
The function of .findOneAndUpdate() is to look for a document matching the criteria in the "counters" collection and then where a document is found already it will "increment" the current counter value by use of the $inc update operator. If the document was not found, then the "upsert" option means that a new document will be created, and at any rate the same "increment" will happen in the new document as well.
The "new" option here means that we want the "modified" document to be returned ( either new or changed ) rather than what the document looked like before the $inc was applied. The result is that "counter" value will always increase on every access.
Once that is complete and a document for Counter is either incremented or created for it's matching keys, then you now have something you can use to assign to the _id in the Product model. As mentioned earlier you can use the virtual here for convenience to get the prefix with the appended counter value.
So as long as your documents are always created by either the .create() method from the model or by using new Product() and then the .save() method, then the methods attached to your "model" in your code are always executed.
Note here that since you want this in _id, then as a primary key this is "immutable" and cannot change. So even if the content in the fields referenced was later altered, the value in _id cannot be changed, and therefore why the code here makes no attempt when an _id value is already set.

Change MongoDB find results dynamically

I'm using mongoose with node.js.
Let's say I have 'Posts' DB where each document in it is a post.
Each post has a 'ReadBy' array which holds names of users that had read this post.
When I'm searching for documents in this DB, I want to "change" the 'ReadBy' value to show by Boolean value if the user that is searching for it is in this array or not.
For example, let's say these are 2 documents that are in this DB:
{ "PostName": "Post Number 1", "ReadBy": ["Tom", "John", "Adam"] }
{ "PostName": "Post Number 2", "ReadBy": ["John", "Adam"] }
If I'm user 'Tom', I want to get the results like this:
[
{
"PostName": "Post Number 1",
"ReadBy": true,
},
{
"PostName": "Post Number 2",
"ReadBy": false,
}
]
Now, I know that I can get the documents and go over each one of them with forEach function, and then use forEach again on the "ReadBy" array and change this field.
I'm asking if there is more efficient way to do it in the mongoDB query itself, or some other way in the code.
If there is another way with mongoose - even better.
Using mongoDb $setIntersection in aggregation you get the result like this :
db.collectionName.aggregate({
"$project": {
"ReadBy": {
"$cond": {
"if": {
"$eq": [{
"$setIntersection": ["$ReadBy", ["Tom"]]
},
["Tom"]
]
},
"then": true,
"else": false
}
},
"PostName": 1
}
})
So above working first like this
{ $setIntersection: [ [ "Tom", "John", "Adam"], [ "Tom"] ] }, return [ "Tom"]
{ $setIntersection: [ [ "John", "Adam"], [ "Tom"] ] }, return [ ]
and $eq to check whether setIntersection results matched with ["Tom"] if yes then return true else false
You can try something similar to
var unwind = {"$unwind": "$ReadBy"}
var eq = {$eq: ["$ReadBy", "Bob"]}
var project = {$project: {PostName: 1, seen: eq}}
db.posts.aggregate([unwind, project])
Just notice that you solution is highly inefficient. Both for storing the data ( growing array) and for searching.

How to get embedded document in an array in MongoDB (with Mongoose)

I have a BSON object like this saved in MongoDB:
{
"title": "Chemistry",
"_id": "532d665f89ae4ae703b29730",
"__v": 0,
"sections": [
{
"week": 1,
"_id": "532d665f89ae4ae703b29731",
"assignments": [
{
"created_date": "2014-03-22T10:30:55.621Z",
"_id": "532d665f89ae4ae703b29733",
"questions": []
},
{
"created_date": "2014-03-22T10:30:55.621Z",
"_id": "532d665f89ae4ae703b29732",
"questions": []
}
],
"materials": []
}
],
"instructor_ids": [],
"student_ids": []
}
What I wish to do is to retrieve the 'assignment' with _id 532d665f89ae4ae703b29731. It is an element in the assignments array, which, in turn, is an element in the sections array.
I am able to retrieve the entire document with the query
{ 'sections.assignments._id' : assignmentId }
However, what I want is just the assignment subdocument
{
"created_date": "2014-03-22T10:30:55.621Z",
"_id": "532d665f89ae4ae703b29733",
"questions": []
}
Is there a way to accomplish such query? Should I resolve to have assignment in a different collection?
As of mongoose version 6.x, the accepted answer is not valid any more because $elemMatch cannot be used any more on nested documents, instead, aggregate should be used.
if you want ti use an _id to find the document you should convert the _id you get as argument to native mongoDb _id format otherwise it will be constructed as a string and an error will occur.
const native_id = mongoose.Types.ObjectId(id);
const assignment = await <your_model_here>.aggregate([
{ $unwind: "$sections" },
{ $unwind: "$sections.assignments" },
{ $match: { "sections.assignments._id": native_id } },
{ $project: { _id: true, sections: "$sections.assignments" } }
]
)
console.log(assignment) // you have what you want
you can do a aggregate query like this :
db.collection.aggregate(
{$unwind: "$sections"},
{$unwind: "$sections.assignments"},
{$match: {"sections.assignments._id": "532d665f89ae4ae703b29731"}},
{$project: {_id: false, assignments: "$sections.assignments"}}
)
However, I recommends you to think about creating more collections, like you said.
More collections seems to me a better solution then this query.
To retrieve a subset of the elements of an array, you'll need to use the $elemMatch projection operator.
db.collection.find(
{"sections.assignments._id" : assignmentId},
{"sections.assignments":{$elemMatch:{"_id":assignmentId}}}
)
Note:
If multiple elements match the $elemMatch condition, the operator returns the first matching element in the array.

Categories

Resources