I have a Mongoose model that looks like this:
var ProjectSchema = new Schema({
name: String,
slug: String,
dateCreated: { type: Date, default: new Date() },
dateUpdated: { type: Date, default: new Date() },
createdByUserId: Schema.Types.ObjectId,
screens: [Schema.Types.Mixed]
});
I have a class method that looks like this:
ProjectSchema.statics.saveElementProperties = function(slugName, screenIndex, elementId, props, callback) {
var Project = mongoose.model('Project');
var updateProject = function(project) {
// Init empty objects if missing
project.screens[screenIndex] = project.screens[screenIndex] || {};
project.screens[screenIndex].elements = project.screens[screenIndex].elements || {};
project.screens[screenIndex].elements[elementId] = project.screens[screenIndex].elements[elementId] || {};
// Apply properties
project.screens[screenIndex].elements[elementId] = "Dummy Project Data";
console.log('elements before save:', project.screens[screenIndex].elements);
project.save(callback);
};
Project.findOne({ slug: slugName }, function(err, project) {
if (!project) {
project = new Project({ name: slugName, slug: slugName });
}
updateProject(project);
});
};
This happens when I call the method saveElementProperties:
The first time I run this method, it works like it should; a new object is added to project.screens[screenIndex].elements both in runtime (the 'elements before save:' log statement) and when checking the MongoDB database with the mongo client.
The second time, a 2nd object is added to project.screens[screenIndex].elements in runtime, but this object is never persisted to MongoDB.
The third time, object 1 and 3 are visible in project.screens[screenIndex].elements in runtime, but the 3rd object is never persisted to MongoDB.
What causes this behavior?
MAJOR UPDATE: I rewrote the entire persistence mechanism to run less frequently, and instead replace the entire project.screens[screenIndex].elements object with an updated structure:
ProjectSchema.statics.saveScreenProperties = function(slugName, screenIndex, elements, callback) {
console.log('saveScreenProperties:', slugName, screenIndex);
var Project = mongoose.model('Project');
var updateProject = function(project) {
// Init empty objects if missing
project.screens[screenIndex] = project.screens[screenIndex] || {};
project.screens[screenIndex].elements = elements;
// Mark as modified and save
project.markModified('screens.' + screenIndex);
project.save(callback);
};
Project.findOne({ slug: slugName }, function(err, project) {
if (!project) {
project = new Project({ name: slugName, slug: slugName });
console.log(' creating new project');
}
updateProject(project);
});
};
However, it still shows the same behavior - it stores the initial elements object, but not later changes to it.
The problem is that you are manipulating the objects just as you would in standard code but mongoose is not a 'persistence engine` it's an ODM. As such you need to use the provided methods to update the datastore.
You will need to use the update operators to $push onto the array. Mongoose has some analogous methods as well. The most explainatory docs are on the MongoDB site.
http://docs.mongodb.org/manual/reference/operator/update-array/
http://docs.mongodb.org/manual/reference/operator/update/
http://docs.mongodb.org/manual/reference/method/db.collection.update/#db.collection.update
Related
I have been given some code to modify. It is a Node.js app using Mongoose to interact with a MongoDb instance. In Mongoose several schemas were already set up and I've added a few. Among those are these two schemas which break apart a previously existing schema (which was working fine with small data):
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var MapConvertedProjectSchema = new Schema(
{
project_id : {
type: String,
default: ""
},
dataset_id : {
type: String,
default: ""
},
properties:{
type: {},
default: {}
}
});
MapConvertedProjectSchema.pre('save', function(next) {
next();
});
mongoose.model('MapConvertedProject', MapConvertedProjectSchema);
and
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var MapConvertedLayerSchema = new Schema(
{
parent_id:
{
type: mongoose.Schema.Types.ObjectId
},
class:
{
type: String,
default: 'MapLayer',
trim: true
},
properties:
{
type: {},
default: {}
}
});
//Hook a pre save method to clean date
MapConvertedLayerSchema.pre('save', function(next) {
next();
});
mongoose.model('MapConvertedLayer', MapConvertedLayerSchema);
I use the MapConvertedLayer schema like so:
var mongoose = require('mongoose');
var LayerConverted = mongoose.model('MapConvertedLayer');
var newLayer = new LayerConverted();
//newLayer._id is automatically populated with a value
//... add other properties
newLayer.save(function(err)
{
if(err)
{
//...
}
});
This works without any issues that I can discern. However if I try similar code with MapConvertedProject I get an error:
var mongoose = require('mongoose');
var ProjectConverted = mongoose.model('MapConvertedProject');
var map_converted = new ProjectConverted();
//map_converted._id is undefined
//I tried adding the comment below to create an _id manually, but it didn't make a difference when I tried to save
//map_converted._id = mongoose.Types.ObjectId();
console.log("Project Converted ID: " + map_converted._id);
//... fill out the other properties on the schema
map_converted.save(function(err)
{
if(err)
{
//...
}
});
The save generates this error:
ValidationException: One or more parameter values were invalid: Missing the key _id in the item
Does anyone know what is causing this?
I figured this out. There was another place in the code that had a dynamoose model with the same name that was messing things up. I was able to remove all references to dynamoose since it doesn't appear to be used anymore and that cleared up this issue.
I would like to know the best approach to solve the current scenario.
I've got a node API which uses mongoose and bluebird. And some Android clients will post "movement" entities to it.
(Question at the end).
Let's say movement-model.js exports the Schema, and looks like this:
"use strict";
const mongoose = require('mongoose');
const _movementSchema = {
movementId: { type: Number, requried: true },
relMovementId: Number,
_party: { type: mongoose.Schema.Types.ObjectId, required: true, ref: 'Party' }
}
module.exports = mongoose.Schema(_movementSchema, {collection: 'movement'});
And related exported Schema on party-model.js is as follows:
"use strict";
const mongoose = require('mongoose');
const _partySchema = {
active: { type: Boolean, default: true },
name: { type: String, trim: true, required: true },
logo: { type: Buffer },
coordenates: { lat: Number, long: Number },
startOn: { type: Date, required: true },
endOn: { type: Date, required: true }
}
module.exports = mongoose.Schema(_partySchema, {collection: 'party'});
Android client would send the JSON with ObjectId and not full populated object. So when the POST comes, I'm using it directly (i.e: let _movement = req.body;) and on the movement-dao.js I've got the createNew method and I'm exporting the Model:
"use strict";
const mongoose = require('mongoose');
const Promise = require('bluebird');
mongoose.Promise = Promise;
const movementSchema = require('../model/movement-model');
movementSchema.statics.createNew = (movement) => {
return new Promise((resolve, reject) => {
if (!_.isObject(movement)) {
return reject(new TypeError('Movement is not a valid object.'));
}
let _something = new Movement(movement);
_something.save((err, saved) => {
err ? reject(err)
: resolve(saved);
});
});
}
const Movement = mongoose.model('Movement', movementSchema);
module.exports = Movement;
What I want to accomplish is to: save the movement collection with the _party as the full party document is at the moment of the save, I mean an embedded document of a copy of the Party document, which will not be affected by the updates done to the Party document in the future.
While I cannot change the Android Client, so I will still be getting only the ObjectId from it.
JSON example of what Android client will post: {"movementId":1, "relMovementId":4138, "_party":"58dbfe26194cfc5a9ec9b7c5"}
I'm confused now, and not sure if due to the way Android is posting the JSON, I need two schemas; one for the object received (i.e: with ObjectId and ref to Party) and a second one for the object persisted (i.e: with the schema referenced _party: Party.Schema) or if I could do something simpler as some populate prior to save... or what.
For the sake of closing this up:
I've implemented one of the approaches I had in mind while writing the question. Movement schema changed so that: _party: Party.Schema
When I get a POST to create a new movement I do a getById and use the result of that exec to populate the value as an embedded doc.
We have a requirement to store a copy of a Mongo document, as an embedded subdocument in another document. It should have a reference to the original document. The copied document needs to be a deep copy, like a snapshot of the original.
The original document's schema (defined with Mongoose) is not fixed -
it currently uses a type of inheritance to allow different additions to the Schema depending on "type".
Is there a way to such a flexible embedded schema within a Mongoose model?
Is it something that needs to be injected at runtime, when we can know
the schema?
The models / schemas we have currently look like this:
///UserList Schema: - this should contain a deep copy of a List
user: {
type: ObjectId,
ref: 'User'
},
list: {
/* Not sure if this is a how we should store the reference
type: ObjectId,
ref: 'List'
*/
listId: ObjectId,
name: {
type: String,
required: true
},
items: [{
type: ObjectId,
ref: 'Item'
}]
}
///List Schema:
name: {
type: String,
required: true
},
items: [{
type: ObjectId,
ref: 'Item'
}],
createdBy: {
type: ObjectId,
ref: 'User'
}
The code we currently have uses inheritance to allow different item types. I realise this technique may not be the best way to achieve the flexibility we require and is not the focus of my question.
///Item Model + Schema
var mongoose = require('mongoose'),
nodeutils = require('util'),
Schema = mongoose.Schema,
ObjectId = Schema.Types.ObjectId;
function ItemSchema() {
var self = this;
Schema.apply(this, arguments);
self.add({
question: {
type: String,
required: true
}
});
self.methods.toDiscriminator = function(type) {
var Item = mongoose.model('Item');
this.__proto__ = new Item.discriminators[type](this);
return this;
};
}
nodeutils.inherits(ItemSchema, Schema);
module.exports = ItemSchema;
I think you just need to create an empty {} object for the document in your parent mongoose schema. This way you´ll be able to store any object with a hardcopy of all it´s data.
parentobj : {
name: Sring,
nestedObj: {}
}
I think at this point, what you´ll need is to mark your nested objet as modified before you save it. Here is an example of my mongoose code.
exports.update = function(req, res) {
User.findById(req.params.id, function (err, eluser) {
if (err) { return handleError(res, err); }
if(!eluser) { return res.send(404); }
var updated = _.merge(eluser, req.body);
//This makes NESTEDDATA OBJECT to be saved
updated.markModified('nestedData');
updated.save(function (err) {
if (err) { return handleError(res, err); }
return res.json(200, eluser);
});
});
};
In addition, if you need an array of different documents in nestedDocument, the right way is this one:
parentobj : {
name: Sring,
nestedObjs: [Schema.Types.Mixed]
}
Please check Mongoose Schema Types carefully
EDIT
As you said, I´ll add you final solution as including ItemSchema in the nestedObj array definition to clarifythe type of the object to a determined one..
var ItemSchema = new Schema({
item1: String,
item2: String
});
var parentobj = new Schema({
name: Sring,
nestedObj: [ItemSchema]
});
EDIT 2:
Remember adding new Items to the nestedArray, must be done with nestedArray.push(item)
regards!!
Say I have the following schemas:
var promoGroupSchema = new Schema({
title: String,
offers: [{Schema.Types.ObjectId, ref: 'Offer']
});
and
var offerSchema = new Schema({
type: String
});
How do you initialize a promoGroup with new offers? The following won't work since save() is asynchronous. Now, I know I could put a function as a parameter of the save function, but that gets ugly with more offers.
var offer1 = new offerSchema({
type: "free bananas!"
});
var offer2 = new offerSchema({
type: "free apples!"
});
offer1.save();
offer2.save();
var newPromoGroup = new promoGroupSchema({
title: "Some title here",
offers: [offer1._id, offer2._id]
});
From what I read, Mongoose gives the object an _id as soon as you create them, can I rely on those?
You should access _id in the save callback. If you have a lot of offers to group, using a library like async will make your life easier.
var myOffers = [...]; // An array with offers you want to group together
// Array of functions you want async to execute
var saves = myOffers.map(function(offer) {
return function(callback) {
offer.save(callback);
}
}
// Run maximum 5 save operations in parallel
async.parallelLimit(saves, 5, function(err, res) {
if(err) {
console.log('One of the saves produced an error:', err);
}
else {
console.log('All saves succeeded');
var newPromoGroup = new promoGroupSchema({
title: "Some title here",
offers: _.pluck(myOffers, '_id') // pluck: see underscore library
});
}
});
You could also try to use Promises.
I'm still getting the hang of Express/Angular and how they work together to post to the server. I'm using the MEAN stack in my sample application.
The Schema for the object I'm trying to post looks like this in Express.
First define the 'Version' Schema:
var VersionSchema = new Schema({
title: {
type: String,
default: '',
trim: true
},
content: {
type: String,
default: '',
trim: true
},
submitted: {
type: Date,
default: Date.now
},
user: {
type: Schema.ObjectId,
ref: 'User'
}
});
The Version Schema is used in my Draft Schema. When a user creates a draft, it becomes a Version item in the versions array. When the draft is updated, it creates a new array item so all versions of the draft are saved.
var DraftSchema = new Schema({
versions: [VersionSchema],
});
The Draft Schema is set up as an Angular Service and injected into my controller. The service is boilerplate code for creating a $resource though so shouldn't be necessary.
I have two controllers to create the Draft object and save to Mongo: one with Angular, one with Express.
Angular controller:
$scope.create = function() {
var version = {
title: this.title,
content: this.content
};
var draft = new Drafts({
versions: [version]
});
draft.$save(function(response) {
$location.path("drafts/" + response._id);
});
};
In the angular controller I'm passing the title and contents to be saved to the Draft. As I understand it, you should not use an Angular controller to pass user information to the server. So this is handled by Express in the controller below.
Express controller:
exports.create = function(req, res) {
var draft = new Draft(req.body);
draft.versions = [req.user]; // Here is where my problem is, trying to save user
draft.save(function(err) {
if (err) {
return res.send('users/signup', {
errors: err.errors,
draft: draft
});
} else {
res.jsonp(draft);
}
});
};
You can see in my comment above where my problem is. I've looked through docs, SO questions, etc for the correct way to set my User property to the Versions array item being created here.
Note that if I move the User from the Version Schema to the Draft Schema and use:
draft.user = req.user;
To save the user then it works just fine. But I want the user to be saved into the VersionSchema.
To address Erick's comment below, this is a JSON representation of what I want my Draft model to look like. Versions is an Array because I want a new array item to be created each time the draft is updated.
[{
_id: 4h55542j232555000441,
versions: [
{
title: “A really great title”,
body: “On that day the people….”,
user: 5234523452349589hh23523,
submitted: Date
}
]
}];
draft.versions = [req.user]
Does the following: replace the versions array containing a version object with a new array containing the user object. This is not what you want.
As I see it, draft.versions has the following content:
[
{
title: '...',
content: '...'
}
]
To add a user id you need to do the following steps:
get the version object from the versions array
assign a new property to the version object and give it a value of user._id
var version = draft.versions[0]
version['user'] = req.user._id