Mongoose versioning broken? - node.js

I'm sort of at a loss here. I was expecting that anytime an updated an object in mongoose the version would increment on __v. This does not appear to be the case. Am I missing something or is this a bug?
var mongoose = require('mongoose');
mongoose.Promise = require('bluebird');
mongoose.connect('mongodb://localhost/mongooseTest');
var Cat = mongoose.model('Cat', {
name: String,
manualVersion: Number,
arr: []
});
var kitty = new Cat({
name: 'Zildjian',
manualVersion: 0,
arr: []
});
kitty.save()
.then(x => {
x.manualVersion = x.manualVersion + 1;
//x.arr.push(x.manualVersion); <-- pushing here makes '__v' be correct
return x.save();
})
.then(() => Cat.findOne({}))
.then(x => {
x.manualVersion = x.manualVersion + 1;
//x.arr.push(x.manualVersion); <-- pushing here makes '__v' be correct
return x.save();
})
.then(() => Cat.findOne({}))
.then(x => {
console.log(x.toObject());
// RESULT (from console.log and what is also in the DB):
// {
// _id: 565386b058b2632c0886b160,
// name: 'Zildjian',
// manualVersion: 2,
// __v: 0,
// arr: []
// }
});

x.increment().save();
in the above example fixes my problem. 'save()' appears to be idempotent as well so its cool to just call on every save as long as you know you are making changes. According to one of the authors of mongoose versioning is only meant to be used with arrays and only increments for those. Ugh.
http://aaronheckmann.tumblr.com/post/48943525537/mongoose-v3-part-1-versioning
At the very least this should be clearly spelled out in the docs IMO as it is currently very ambigious...
http://mongoosejs.com/docs/guide.html#versionKey

Related

How to list all documents in a mongodb collection in ascending order mongoose

I am trying to list the documents in my level collection by the lowest level to the highest, but that is not working. My code is
const lvl = await Level.find({}).sort({level: 1});
for the filter, and I list it over here:
lvl.forEach(entry => {
embed.addFields({ name: `Level ${entry.level}`, value: `Xp Required: ${entry.xp}`});
})
The code for the level schema is:
const mongoose = require("mongoose");
const LevelSchema = new mongoose.Schema({
level: mongoose.Schema.Types.String,
xp: mongoose.Schema.Types.String,
});
module.exports = mongoose.model("levels", LevelSchema);
This code is working completely fine, the only issue is that it comes in the order that the documents are in the collection, and not by the lowest level to the highest like I want it to. I did what the documentation told me to do in the .sort({level: 1}) part, but that didn't change anything either.
You can use mapping your array, lets re-construct your code for a bit:
this part of your code is okay:
await Level.find({}).sort({level: 1});
But you might need to do it as Array Object. So to work this out. Change the code line to
Level.find({}).sort([['level', 'ascending']]); //remove the await
//ascending = 1, descending = -1
Adding the .exec() will do the trick:
Level.find({})
.sort([['level', 'ascending']])
.exec((err, res) => {
if(res) {
//There's an existing data
//Let's use your method on embed.
let embed = new MessageEmbed()
.setDescription("OPTIONAL")
.setColor("YOUR_COLOR")
.setTitle("OPTIONAL")
.setTimestamp() //OPTIONAL
const result = res.map((value, index) => {
return(embed.addFields({name: `Level: ${res[index].level}`, value: `Xp Required: ${res[index].exp}`}))
}).join("\n");
result
message.channel.send({embeds: [embed]})
} else {
//No data message
}
})
Since the sorting detects only the first number, you need to add .collation({locale: "en_US", numericOrdering: true}).
from this:
Level.find({}).sort([['level', 'ascending']])
Turn it to this:
Level.find({}).sort([['level', 'ascending']]).collation({locale: "en_US", numericOrdering: true})

Saving array of children of a Mongoose Schema and then adding returned IDs to parent

i'm trying to loop over an array of objects, saving them to MongoDB and then add the returned ObjectIds to a parent Schema which then is also saved. I'm at a loss here.
Everything gets saved correctly but the Recipe (parent) apparently is saved before I get the returned ObjectIds of the Tags (children). I feel like I've used the async and await keywords a bit to often.
Can someone help? Code simplified, but I can post more if needed.
Parent Schema:
const recipe = new mongoose.Schema(
{
name: String,
ingredients: [
{
type: mongoose.SchemaTypes.ObjectId,
ref: "Ingredient",
},
],
}
);
Child Schema:
const ingredientSchema = new mongoose.Schema({
value: String,
label: String,
});
Payload:
{
name: "Rezept",
ingredients: [
{
label: "zutat",
value: "Zutat",
},
{
label: "schokolade",
value: "Schokolade",
},
],
};
My router:
recipesRouter.post("/", async (req, res) => {
const { body } = req;
const saveIngredients = async () => {
let ingredientIDs = [];
body.ingredients.map(async (ingredient) => {
const i = new Ingredient({
value: ingredient.value,
label: ingredient.label,
});
const savedIngredient = await i.save();
ingredientIDs.push(savedIngredient._id);
});
return ingredientIDs;
};
const recipe = new Recipe({
name: body.name,
ingredients: (await saveIngredients()) || [],
});
const savedRecipe = await recipe.save();
res.status(201).json(savedRecipe);
});
Returned recipe:
savedRecipe: {
name: 'asd',
ingredients: [],
_id: new ObjectId("62782b45a431e6efb7b8b1a7"),
}
As I said, both ingredients individually and the recipe is saved to the MongoDB after this but not the ingredient IDs in the recipe. The returned recipe has an empty array in ingredients. I guess the recipe is saved too soon before MongoDB can return ObjectIds for the ingredients.
Thanks for any help.
First of all, your post method is an async, so everything inside it is wrapped in a resolved promise automatically.
Do you really need to make your saveIngredients as an async? IMHO, it's better to let the saveIngredients not be in another async.
And then we can remove the empty list, and just wait for the saveIngredients() finish first.
const recipe = new Recipe({
name: body.name,
ingredients: await saveIngredients(),
});
Your guess is correct, the Recipe was saved first because all the conditions are fulfilled because it doesn't need to wait for the saveIngredients since you provided a [] as the default value. And your saveIngredients is run in parallel.
I got it smh. Turns out async in a .map or .foreach doesn't go well. I turned it into a simple for loop. It's still bloated/lot of steps imo but it works!
recipesRouter.post("/", async (req, res) => {
const { body } = req;
const saveIngredients = async () => {
let ingredientIDs = [];
for (let i = 0; i < body.ingredients.length; i++) {
const el = body.ingredients[i];
const ing = new Ingredient({
value: el.value,
label: el.label,
});
const savedIngredient = await ing.save();
ingredientIDs.push(savedIngredient._id);
}
return ingredientIDs;
};
const ingredientIDs = await saveIngredients();
const recipe = new Recipe({
name: body.name,
ingredients: ingredientIDs,
});
const savedRecipe = await recipe.save();
res.status(201).json(savedRecipe);
});

How to access a different schema in a virtual method?

I want to write a virtual (get) method for my MongoDb collection (Parts) which needs to access a different schema: I want it to assert if a document is 'obsolete' according to a timestamp available in a different (Globals) collection:
const partsSchema = new Schema({
...
updatedAt: {
type: Date,
},
...
}, {
toObject: { virtuals: true },
toJSON: { virtuals: true },
});
partsSchema.virtual('obsolete').get(async function() {
const timestamp = await Globals.findOne({ key: 'obsolescenceTimestamp' }).exec();
return this.updatedAt < timestamp.value;
});
But when I do a find, I always get a {} in the obsolete field, and not a boolean value...
const p = await parts.find();
...
"obsolete": {},
...
Is there some way to accomplish my goal?
You can do this, but there are a few obstacles you need to hurdle. As #Mohammad Yaser Ahmadi points out, these getters are best suited for synchronous operations, but you can use them in the way you're using them in your example.
So let's consider what's happening here:
partsSchema.virtual('obsolete').get(async function() {
const timestamp = await Globals.findOne({ key: 'obsolescenceTimestamp' }).exec();
return this.updatedAt < timestamp.value;
});
Since the obsolete getter is an async function, you will always get a Promise in the obsolete field when you query your parts collection. In other words, when you do this:
const p = await parts.find();
You will get this:
...
"obsolete": Promise { <pending> },
...
So besides getting the query results for parts.find(), you also need to resolve the obsolete field to get that true or false result.
Here is how I would write your code:
partsSchema.virtual('obsolete').get(async function() {
const Globals = mongoose.model('name_of_globals_schema');
const timestamp = await Globals.findOne({ key: 'obsolescenceTimestamp' });
return this.updatedAt < timestamp.value;
});
Then when querying it...
parts.findOne({_id: '5f76aee6d1922877dd769da9'})
.then(async part => {
const obsolete = await part.obsolete;
console.log("If obsolete:", obsolete);
})

Mongo's updateMany with rename doesn't update for some fields

Desctiption
MongoDB's updateMany method doesn't update any documents when used with $rename on some fields.
Example
I try to rename the fields blog.blog_ttile and blog.blog_cotnet. I checked for typos, there are none.
Example model:
User: {
name,
email,
blog: {
blog_ttile,
blog_cotnet
}
}
Code:
const mongoose = require('mongoose');
const User = mongoose.model('User');
const nameChanges = {
"blog.blog_ttile": 'blog.title',
'blog.blog_cotnet': 'blog.content',
};
async function performNameChanges() {
try {
const updatedDocuments = await User.updateMany({}, { $rename: nameChanges });
console.log({ updatedDocuments });
} catch(err) {
console.error(err);
}
}
returns:
{ updatedDocuments: { ok: 0, n: 0, nModified: 0 } }
Additional details
Some fields are correctly recognized. Let's say email in the above example. However, when I try to update the updated name, it doesn't work again. Interestingly, it still detects the original name.
Example:
Renaming email to personal_email works. Renaming personal_email to email afterwards doesn't and returns { ok: 0, n: 0, nModified: 0 }. Calling a rename on email a second time returns { n: <total_records>, nModified: 0, ok: 1 } although no documents have email anymore`.
What could be causing this?
Note:
This question applies for MongoDB without Mongoose with db.getCollection("User").updateMany instead of User.updateMany
I tried doing the same in MongoDB. It works as expected. In your case I'm suspecting Mongoose schema to be the cause of this weird behavior. Mongoose Schema has to have the field you are looking to rename. If the field doesn't exist, it returns nModified 0. The schema will need to have both the old and the new names. Old ones to allow the migration, and the new ones for the new logic in the code.
Your return result is:
{ updatedDocuments: { ok: 0, n: 0, nModified: 0 } }
How is this possible? n=0? for query {}. It is only possible when there are no elements in your collection. n means matched count, it must be equal to total number of records in your collection.
Renaming email to personal_email works
Before first update your schema is fine. But after rename (first update), you should update your schema to :
User: {
name,
personal_email,
blog: {
blog_tile,
blog_contnet
}
}
before running the second update (renaming back to email).
As said in the other answer, it's because your mongoose schema didn't contain the field you wanted to rename.
Instead of keeping the old field around while the migration occurs, you can also specify strict: false in the options, and mongoose will not discard unknown paths:
const mongoose = require('mongoose');
const User = mongoose.model('User');
const nameChanges = {
"blog.blog_ttile": 'blog.title',
'blog.blog_cotnet': 'blog.content',
};
async function performNameChanges() {
try {
const updatedDocuments = await User.updateMany(
{},
{ $rename: nameChanges },
{
// Strict allows to update keys that do not exist anymore in the schema
strict: false,
}
).exec();
console.log({ updatedDocuments });
} catch(err) {
console.error(err);
}
}
use like this thanks you
import { connect } from '../database';
export const renameFileds = async () => {
const db = connect();
//let userlist = await db.UserModel.find();
const updatedDocuments = await db.UserModel.updateMany(
{},
{ $rename: { image: 'picture' } },
{
// Strict allows to update keys that do not exist anymore in the schema
strict: false,
}
).exec();
console.log({ updatedDocuments });
};
renameFileds();

mongoose recursive populate

I have been searching for a while and I didn't find any good answer. I have n-deep tree that I am storing in DB and I would like to populate all parents so in the end I get the full tree
node
-parent
-parent
.
.
-parent
So far I populate to level 2, and as I mentioned I need to get to level n.
Node.find().populate('parent').exec(function (err, items) {
if (!err) {
Node.populate(items, {path: 'parent.parent'}, function (err, data) {
return res.send(data);
});
} else {
res.statusCode = code;
return res.send(err.message);
}
});
you can do this now (with https://www.mongodb.com/blog/post/introducing-version-40-mongoose-nodejs-odm)
var mongoose = require('mongoose');
// mongoose.Promise = require('bluebird'); // it should work with native Promise
mongoose.connect('mongodb://......');
var NodeSchema = new mongoose.Schema({
children: [{type: mongoose.Schema.Types.ObjectId, ref: 'Node'}],
name: String
});
var autoPopulateChildren = function(next) {
this.populate('children');
next();
};
NodeSchema
.pre('findOne', autoPopulateChildren)
.pre('find', autoPopulateChildren)
var Node = mongoose.model('Node', NodeSchema)
var root=new Node({name:'1'})
var header=new Node({name:'2'})
var main=new Node({name:'3'})
var foo=new Node({name:'foo'})
var bar=new Node({name:'bar'})
root.children=[header, main]
main.children=[foo, bar]
Node.remove({})
.then(Promise.all([foo, bar, header, main, root].map(p=>p.save())))
.then(_=>Node.findOne({name:'1'}))
.then(r=>console.log(r.children[1].children[0].name)) // foo
simple alternative, without Mongoose:
function upsert(coll, o){ // takes object returns ids inserted
if (o.children){
return Promise.all(o.children.map(i=>upsert(coll,i)))
.then(children=>Object.assign(o, {children})) // replace the objects children by their mongo ids
.then(o=>coll.insertOne(o))
.then(r=>r.insertedId);
} else {
return coll.insertOne(o)
.then(r=>r.insertedId);
}
}
var root = {
name: '1',
children: [
{
name: '2'
},
{
name: '3',
children: [
{
name: 'foo'
},
{
name: 'bar'
}
]
}
]
}
upsert(mycoll, root)
const populateChildren = (coll, _id) => // takes a collection and a document id and returns this document fully nested with its children
coll.findOne({_id})
.then(function(o){
if (!o.children) return o;
return Promise.all(o.children.map(i=>populateChildren(coll,i)))
.then(children=>Object.assign(o, {children}))
});
const populateParents = (coll, _id) => // takes a collection and a document id and returns this document fully nested with its parents, that's more what OP wanted
coll.findOne({_id})
.then(function(o){
if (!o.parent) return o;
return populateParents(coll, o.parent))) // o.parent should be an id
.then(parent => Object.assign(o, {parent})) // replace that id with the document
});
Another approach is to take advantage of the fact that Model.populate() returns a promise, and that you can fulfill a promise with another promise.
You can recursively populate the node in question via:
Node.findOne({ "_id": req.params.id }, function(err, node) {
populateParents(node).then(function(){
// Do something with node
});
});
populateParents could look like the following:
var Promise = require('bluebird');
function populateParents(node) {
return Node.populate(node, { path: "parent" }).then(function(node) {
return node.parent ? populateParents(node.parent) : Promise.fulfill(node);
});
}
It's not the most performant approach, but if your N is small this would work.
Now with Mongoose 4 this can be done. Now you can recurse deeper than a single level.
Example
User.findOne({ userId: userId })
.populate({
path: 'enrollments.course',
populate: {
path: 'playlists',
model: 'Playlist',
populate: {
path: 'videos',
model: 'Video'
}
}
})
.populate('degrees')
.exec()
You can find the official documentation for Mongoose Deep Populate from here.
Just don't :)
There is no good way to do that. Even if you do some map-reduce, it will have terrible performance and problems with sharding if you have it or will ever need it.
Mongo as NoSQL database is really great for storing tree documents. You can store whole tree and then use map-reduce to get some particular leafs from it if you don't have a lot of "find particular leaf" queries. If this doesn't work for you, go with two collections:
Simplified tree structure: {_id: "tree1", tree: {1: [2, {3: [4, {5: 6}, 7]}]}}. Numbers are just IDs of nodes. This way you'll get whole document in one query. Then you just extract all ids and run second query.
Nodes: {_id: 1, data: "something"}, {_id: 2, data: "something else"}.
Then you can write simple recurring function which will replace node ids from first collection with data from second. 2 queries and simple client-side processing.
Small update:
You can extend second collection to be a little more flexible:
{_id: 2, data: "something", children:[3, 7], parents: [1, 12, 13]}
This way you'll be able to start your search from any leaf. And then, use map-reduce to get to the top or to the bottom of this part of tree.
This is a more straight forward approach to caub's answer and great solution. I found it a bit hard to make sense of at first so I put this version together.
Important, you need both 'findOne' and 'find' middleware hooks in place for this solution to work. *
* Also, the model definition must come after the middleware definition *
const mongoose = require('mongoose');
const NodeSchema = new mongoose.Schema({
children: [mongoose.Schema.Types.ObjectId],
name: String
});
const autoPopulateChildren = function (next) {
this.populate('children');
next();
};
NodeSchema
.pre('findOne', autoPopulateChildren)
.pre('find', autoPopulateChildren)
const Node = mongoose.model('Node', NodeSchema)
const root = new Node({ name: '1' })
const main = new Node({ name: '3' })
const foo = new Node({ name: 'foo' })
root.children = [main]
main.children = [foo]
mongoose.connect('mongodb://localhost:27017/try', { useNewUrlParser: true }, async () => {
await Node.remove({});
await foo.save();
await main.save();
await root.save();
const result = await Node.findOne({ name: '1' });
console.log(result.children[0].children[0].name);
});
I tried #fzembow's solution but it seemed to return the object from the deepest populated path. In my case I needed to recursively populate an object, but then return the very same object. I did it like that:
// Schema definition
const NodeSchema = new Schema({
name: { type: String, unique: true, required: true },
parent: { type: Schema.Types.ObjectId, ref: 'Node' },
});
const Node = mongoose.model('Node', NodeSchema);
// method
const Promise = require('bluebird');
const recursivelyPopulatePath = (entry, path) => {
if (entry[path]) {
return Node.findById(entry[path])
.then((foundPath) => {
return recursivelyPopulatePath(foundPath, path)
.then((populatedFoundPath) => {
entry[path] = populatedFoundPath;
return Promise.resolve(entry);
});
});
}
return Promise.resolve(entry);
};
//sample usage
Node.findOne({ name: 'someName' })
.then((category) => {
if (category) {
recursivelyPopulatePath(category, 'parent')
.then((populatedNode) => {
// ^^^^^^^^^^^^^^^^^ here is your object but populated recursively
});
} else {
...
}
})
Beware it's not very efficient. If you need to run such query often or at deep levels, then you should rethink your design
Maybe a lot late for that but mongoose has some documentation on this :
Ancestors Tree Array
Materialized Path Tree Array
I think the first one is more appropriate to you as you are looking to populate parents.
With that solution, you can with one regex query, search all the documents matching your designered output tree.
You would setup documents with this Schema :
Tree: {
name: String,
path: String
}
Paths field would be the absolute path in your tree :
/mens
/mens/shoes
/mens/shoes/boots
/womens
/womens/shoes
/womens/shoes/boots
For example you could search all the childrens of your node '/mens/shoes' with one query :
await Tree.find({ path: /^\/mens/shoes })
It would return all the documents where the path starts with /mens/shoes :
/mens/shoes
/mens/shoes/boots
Then you'd only need some client-side logic to arrange it in a tree structure (a map-reduce)

Resources