Mongoose Changing Schema Format - node.js

We're rapidly developing an application that's using Mongoose, and our schema's are changing often. I can't seem to figure out the proper way to update a schema for existing documents, without blowing them away and completely re-recreating them from scratch.
I came across http://mongoosejs.com/docs/api.html#schema_Schema-add, which looks to be right. There's little to no documentation on how to actually implement this, making it very hard for someone who is new to MongoDB.
I simply want to add a new field called enabled. My schema definition is:
var sweepstakesSchema = new Schema({
client_id: {
type: Schema.Types.ObjectId,
ref: 'Client',
index: true
},
name: {
type: String,
default: 'Sweepstakes',
},
design: {
images: {
type: [],
default: []
},
elements: {
type: [],
default: []
}
},
enabled: {
type: Boolean,
default: false
},
schedule: {
start: {
type: Date,
default: Date.now
},
end: {
type: Date,
default: Date.now
}
},
submissions: {
type: Number,
default: 0
}
});

Considering your Mongoose model name as sweepstakesModel,
this code would add enabled field with boolean value false to all the pre-existing documents in your collection:
db.sweepstakesModel.find( { enabled : { $exists : false } } ).forEach(
function (doc) {
doc.enabled = false;
db.sweepstakesModel.save(doc);
}
)

There's nothing built into Mongoose regarding migrating existing documents to comply with a schema change. You need to do that in your own code, as needed. In a case like the new enabled field, it's probably cleanest to write your code so that it treats a missing enabled field as if it was set to false so you don't have to touch the existing docs.
As far as the schema change itself, you just update your Schema definition as you've shown, but changes like new fields with default values will only affect new documents going forward.

I was also searching for something like migrations, but didn't find it. As an alternative you could use defaults. If a key has a default and the key doesn't exist, it will use the default.
Mongoose Defaults
Default values are applied when the document skeleton is constructed. This means that if you create a new document (new MyModel) or if you find an existing document (MyModel.findById), both will have defaults provided that a certain key is missing.

I had the exact same issue, and found that using findOneAndUpdate() rather than calling save allowed us to update the schema file, without having to delete all the old documents first.
I can post a code snippet if requested.

You might use mongo shell to update the existing documents in a specific collection
db.SweeptakesModel.update({}, {$set: {"enabled": false}}, {upsert:false, multi:true})

I had a similar requirement of having to add to an existing schema when building an app with Node, and only found this (long ago posted) query to help.
The schema I added to by introducing the line in the original description of the schema and then running something similar to the following line, just the once, to update existing records:
myModelObject.updateMany( { enabled : { $exists : false } }, { enabled : false } )
'updateMany' being the function I wanted to mention here.

just addition to what Vickar was suggesting, here Mongoose Example written on Javascript (Nodejs):
const mongoose = require('mongoose');
const SweeptakesModel = mongoose.model(Constants.SWEEPTAKES,sweepstakesSchema);
SweeptakesModel.find( { enabled : { $exists : false } }).then(
function(doc){
doc.enabled = false;
doc.save();
}
)

Related

How to insert Array of objects in mongoDB?

I am very new to MONGO DB so please bear with me.I am having a problem my array of objects is not working properly .
Here is my schema
const playerSchema = new mongoose.Schema({
name: String,
stats :{
wins:Number,
losses:Number,
xp:Number
},
achievement:[
{
name:String,
date: String
}
] });
Here is my document
const fluffy = new playerModel({
"name":"nic raboy",
"stats":{
"wins":5,
"losses":10,
"xp":300
},
"achievements":[
{"name":"Massive XP","date" :"25-08-21"},
{"name":"instant loss","date":"24-08-21"}
]
});
however in mongodb atlas its only showing array...and i cant see the objects inside...
SCREENSHOT
Your schema is correct, it seems your input is wrong,
In schema definition you named it achievement, whereas in input document it is achievements. Correct this everything will work as you expected.
Explanation
The schema is expecting achievement and you inserted achievements, that is why it is shown as an empty array in the database. To avoids this kind of typos in the future, use the required flag.
const playerSchema = new mongoose.Schema({
name: String,
stats: {
wins: Number,
losses: Number,
xp: Number
},
achievements: [
{
name: {
type: String,
required : true,
},
date: {
type: String,
required : true, // required informs for missing fields
}
}
]
})
Refer this link for more on validation
You can use insertMany see the doc here.
Of course, a while loop should work find calling multiple times insertOne, though I advise you to use the insertMany() method.
If you're new to MongoDB, I strongly encourage you to have a look at MongoDB University's MongoDB basics course as well as the MongoDB for JavaScript Developers course.

Storing site config as Mongoose model

I have website configuration (currently stored as JSON file), and I would like to move it to MongoDB and probably use Mongoose to handle read-write operations and perform validation through schemas.
Configuration is an object with limited amount of keys, similar to that:
{
siteOffline: false,
storeOffline: false,
priceMultipliers: {
a1: 0.96
a2: 0.85
},
...
}
Should it be made a collection with key-value entries? Not sure how to enforce Mongoose schema in this case.
Should it be made a collection with a single document? Not sure how to guarantee that there is only one document at a time.
Any other options?
Ok, one thing at a time :
if you want to use mongoose, you should have your full config as one document :
var siteConfig = new Schema({
siteOffline: Boolean,
storeOffline: Boolean,
priceMultipliers: {
a1: Number
a2: Number
}
});
Then, if you want a one document only collection, you can use MongoDB capped collections
I suggest you go through the multiple options that Mongoose allows, here
The Schema for your one-doc collection would be something like :
var config = new Schema({
siteOffline: Boolean,
storeOffline: Boolean,
priceMultipliers: {
a1: Number
a2: Number
}
}, {
collection:'config',
capped: { size: 1024, max: 1}
});
There's some irony in having a "collection" of only one document though :)
Another "hack" solution that could work better for you is to use a secured field (of which you cannot change the value), and add a unique index on that field. Read this for more info The field could be present in the document but not in the model (virtual). Again, this is hacky, but your use case is a bit strange anyway :)
The capped approach works but it doesn't allow updating fields.
There is a better solution using the immutable field property (>= mongoose v5.6).
In addition, you can add the collection option to your schema to prevent mongoose from pluralize the name of the collection
let configSchema = new Schema(
{
_immutable: {
type: Boolean,
default: true,
required: true,
unique : true,
immutable: true,
},
siteOffline: Boolean,
storeOffline: Boolean,
priceMultipliers: {
a1: Number
a2: Number
},
},
{
collection:'config',
}
);
var Config = mongoose.model( 'config', configSchema );
Config.updateOne( {}, { siteOffline: false });
I hope it will help

Mongoose find with default value

I have a mongoose model: (With a field that has a default)
var MySchema= new mongoose.Schema({
name: {
type: String,
required: true
},
isClever: {
type: Boolean,
default: false
}
});
I can save a model of this type by just saving a name and in mongoDB, only name can be seen in the document (and not isClever field). That's fine because defaults happen at the mongoose level. (?)
The problem I am having then is, when trying to retrieve only people called john and isClever = false:
MySchema.find({
'name' : 'john',
'isClever': false
}).exec( function(err, person) {
// person is always null
});
It always returns null. Is this something related to how defaults work with mongoose? We can't match on a defaulted value?
According to Mongoose docs, default values are applied when the document skeleton is constructed.
When you execute a find query, it is passed to Mongo when no document is constructed yet. Mongo is not aware about defaults, so since there are no documents where isClever is explicitly true, that results in empty output.
To get your example working, it should be:
MySchema.find({
'name' : 'john',
'isClever': {
$ne: true
}
})

Mongoose Find $in Query Not Working

I have some code that looks like this
return courseModel.findQ({ autoReview: true}).then(function(autoCourses){
autoCourses = _.pluck(autoCourses, '_id');
console.log("autoCourses", autoCourses);
return userCoursesModel.findQ({
activityId: {
$in: autoCourses
}
}).then(function(ucs) {
var saves = [];
console.log("ucs", ucs);
When i log "autoCourses" i see a few documents printed, and the lodash pluck should pluck out the _id's of each one. I confirmed this is working correctly.
I am using mongoose-q:
https://github.com/iolo/mongoose-q
And i tried the normal "find" as well, but no luck.
When I do a findQ({}) and find all the docs, I can see ones that match the activityId. For example:
autoCourses [ 55d3b57e395d0105a4828f18,
55d4e0b8df5d4cdc23c916e2,
55d23654642c1d05124c1ace,
55d238b0642c1d05124c1ad2,
55d23bb1ecce9f4b14f79c7b,
55d257cf70721efc16db1c24,
55d2682532eace3118376a95,
55d2694a32eace3118376aad,
Also, when I log each user course activityId with a findAll, I can see that there are matches:
ucs.activityId 55de1dedc7c10dfc3bf5d9ae
ucs.activityId 55d3a91d46206dde7a46ed94
ucs.activityId 55df9974d8dfddb877a372f9
ucs.activityId 55d3b57e395d0105a4828f18
What is happening here? Why isn't the $in query working?
UPDATE:
As requested this is what the user course schema looks like:
var UserCourseSchema = new Schema({
activityId: {
type: Schema.Types.ObjectId,
ref: 'courses',
required: true
},
and the course
var CourseSchema = new Schema({
autoReview: {
type: Boolean,
default: false
},
Creating the userCourse document:
var newUserCourse = {
userId : userId,
activityId : activity._id,
steps : []
}
// copy steps from activity
activity.steps.forEach(function(step){
newUserCourse.steps.push({partId: step._id});
})
return UserCourses.createQ(newUserCourse);
I contacted MongoDB support regarding this and I found that mongoose-q was using an older version of mongoose (< version 4) while my top level dependency was 4.0.5, this was causing bizarre behavior. As soon as I updated mongoose-q to 0.1.0 and mongoose to 4.1.6 and the find worked again.

Mongoose: Add new Schema property and update all current documents

I have at production a NodeJS application running using MongoDB and Mongoose, which inside has a Mongoose Schema like:
var Product = new Schema({
"name": { type: String, required: true }
, "description": { type: String }
});
Now, as a new requirement I'm adding a new property to the Schema at my local, like this:
var Product = new Schema({
"name": { type: String, required: true }
, "description": { type: String }
, "isPublic": { type: Boolean, default: true }
});
When I restart my NodeJS process, I was expecting an update to all of my current documents (products), so now every document have a property isPublic which value is true.
What happened is no document has that new property and if I do a someProduct.Save({ isPublic: true }) it gets added.
Question: is there a way to accomplish that?,
I know I can do a $set from command line with mongo client, but I want to know if there is a way where Mongoose will add the missing property after the Schema changed on a process restart.
What happened is no document has that new property and if I do a someProduct.Save({ isPublic: true }) it gets added.
That's because the mongoose default attribute works just for new documents. There're two workarounds:
Write your code to treat documents without the isPublic property as true;
Or, as you've mentioned above, set the property manually through mongodb console.
Your best bet is to use MongoSH. Adding a new property to a Mongoose Schema will never update existing documents unless the programmer does it.
Use the updateMany command in the Mongo Shell (MongoSH):
// Get all documents in the collection, and set the field to a value
db.users.updateMany( {} ,
{ $set: {"newlyCreatedField": "defaultValue"} }
);

Resources