Can't save JavaScript objects with Mongoose - javascript

I'm doing a MMO Real-Time Browser game, and I'm storing data using Mongoose (MongoDB).
First of all, I'll show you the structure of my object:
var playerSchema = new Schema({
name: { type: String, required: true, trim: true, index: { unique: true } },
resources: {
wood: { type: Number, required: true, default: 500},
stone: { type: Number, required: true, default: 300},
iron: { type: Number, required: true, default: 0},
cereal: { type: Number, required: true, default: 0}
},
resourcesPerHour: {
woodPerHour: { type: Number, required: true, default: 40},
stonePerHour: { type: Number, required: true, default: 20},
ironPerHour: { type: Number, required: true, default: 0},
},
res: {type: Array, required:true, default: []},
buildings: { type: Array, required: true, default: []},
researches: { type: Array, required: true, default: []}
});
As you can see, res, buildings, and researches are arrays. I'm gonna show you one of them (they all have the same structure):
var buildingSchema = new Schema({
id: {type: String, requried: true},
name: { type: String, required: true, trim: true, index: { unique: true } },
level: { type: Number, required: true, default: 0},
scalingValue: {type: Number, required: true, default: 2},
costs: {
wood: { type: Number, required: true, default:0},
stone: { type:Number, required:true, default:0},
iron: {type:Number, required:true, default:0},
cereal: {type:Number, required:true, default:0}
}
});
OK, imagine I have a player, with all data initializated. When I try to update something, I can only update information out of that lists. Look this example:
player.findOne({name:req.session.name}, function(err, doc){
doc.resources.wood -= 200;
doc.buildings[id%100].costs.wood *= 2;
doc.save(function(err){
if(err)console.log(err);
});
}
When I look the model on database, it only stored the resources.wood, not the building[i].costs.wood. I don't know why it's failing, but all objects inside the arrays are created using a new variable, where variable is a Schema (like buildingSchema).
One more thing, I added a console.log(doc.buildings[i].costs.wood); just before the doc.save() and it's ok. So it means all the data is modified fine, but in the doc.saveit only save the 'not-in-list' data.
EDIT: console.log(err); doesn't print nothing, so it means the save function worked.

When you use the Array type in your schema (same as the Mixed type in the docs), you need to explicitly mark the field as modified or Mongoose won't save any changes you make to it.
doc.markModified('buildings');
doc.save(function(err){
if(err)console.log(err);
});
The better approach may be to declare buildings as containing an array of buildingSchema:
buildings: [buildingSchema],
That way Mongoose will be able to track the changes you make to the buildings array automatically.

https://groups.google.com/forum/#!topic/mongoose-orm/5m7ZPkhFyrc
Subdocuments that are not inside of an array are not supported by mongoose. My recommendation is to drop mongoose completely and just use direct queries to the db. You'll find the node native driver is quite easy to use.

Related

MongoDB data modelling performance

I'm currently trying to figure out at mongodb what's the best way in terms of performance cost and redundancy the best way of building a big document data schema. The final JSON from my rest -> app will be likely how it is structured.
Now internally the data will not be used as many to many that's why i binded it into a single document. Only the id will be used as a reference in another collections.
What you guys think, is it better to spit as relational way, with multiple collection to store the content inside of deliverable and use reference or just embedded. (since NoSQL has no joins i though this way will speed up)
Current using mongoose at node app
The Schema:
projectSchema = new Schema({
name: {
type: String,
required: true,
minlength: 3,
maxlength: 50
},
companyId: {
type: mongoose.Types.ObjectId,
ref: 'companies',
required: true
},
deleted: {
type: Number,
enum: [0, 1],
default: 0
},
predictedStartDate: {
type: Date,
default: ""
},
predictedEndDate: {
type: Date,
default: ""
},
realStartDate: {
type: Date,
default: ""
},
realEndDate: {
type: Date,
default: ""
},
//not final version
riskRegister: [{
name: String,
wpId: {
type: mongoose.Types.ObjectId,
ref: 'projects.deliverables.workPackages.id',
required: true
},
probability: String,
impact: String,
riskOwner: String,
response: String,
duration: String,
trigger: String,
status: String,
plannedTimming: String
}],
deliverables: [{
body: String,
workPackages: [{
body: String,
activities: [{
body: String,
tasks: [{
content: String,
properties: [{
dependecies: Array,
risk: {
type: Number,
enum: [0,1],
required: true
},
estimatedTime: {
type: Number,
required: true
},
realTime: {
required: true,
default: 0,
type: Number
},
responsible: {
id: {
type: Number,
default: -1
},
type: {
type: String,
enum: [0, 1], //0 - user, 1 - team
default: -1
}
},
materialCosts: {
type: Number,
default: 0
},
status: {
type: Number,
default: 0
},
approval: {
type: Number,
default: 0
},
startDate: {
type: Date,
default: ""
},
finishDate: {
type: Date,
default: ""
},
endDate: {
type: Date,
default: ""
},
userStartDate: {
type: Date,
default: ""
},
endStartDate: {
type: Date,
default: ""
},
taskNum: {
type: Number,
required: true
},
lessonsLearn: {
insertedAt: {
type: Date,
default: Date.now
},
creatorId: {
type: mongoose.Types.ObjectId,
ref: 'users',
required: true
},
situation: {
type: String,
required: true
},
solution: {
type: String,
required: true
},
attachments: Array
}
}]
}]
}]
}]
}]
})
The only concern I would raise would be regarding deliverables. If in the future there is a use case to do some CRUD operation regarding activities or tasks on the workPackage, the mongodb position operator $ does not support inner arrays, so you would be forced to extract all the deliverables and in memory iterate over all and only after update the deliverables.
My sugestion would be to support only arrays in the first level on the object. The inner objects should be moduled in separate collection ( activities and tasks ). In latest versions of mongodb you now have support to transactions so you can implement ACID on your operations against database, so the manipulation of all this information can be done in an atomic way.

Mongoose: Join Operation, populate isn't possible

I'm currently struggling with a project of mine.
I've got a collection called "Games" and one "Participation".
When a user loggs in, he/she should be able to see the games and the individual participation status.
Therefore, I want to join these two collections but I can't use .populate() because I can't enter the neccessary Participation ID in the Games collection due to the fact, that I don't have the participation ID at the time I create a game. (So I would need to save the participation, remember the created ID and insert THIS id in the games collection afterwards)
The other way round would be a good solution (to populate Games in Participation) but initially, there are no participations in these collection, until a user clicks "Participate" or "Not Participate".
Basically I need a SQL query like that:
select * from games g, participation p where g.gamesId = p.gamesId AND p.username = [...]
Is there any possibility to achieve this?
Otherwise I would need to save every participation as a "Game", having the dependent username and his/her participation status in it.
Wouldn't prefer this solution at all.
Thanks in advance!
In case it helps:
Here are my two collections:
Game:
var gameSchema = mongoose.Schema(
{
isHome: { type: Boolean, required: true },
time: { type: String, required: true, max: 100 },
uzeit: { type: String, required: true, max: 100 },
clubId: { type: mongoose.Types.ObjectId, required: true },
enemy: { type: String, required: true, max: 100 },
place: { type: String, required: true, max: 100 },
(participation: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Teilnahme' }])
});
Participation:
var participationSchema = mongoose.Schema(
{
playerAvailable: { type: Boolean, required: true },
clubId: { type: mongoose.Types.ObjectId, required: true },
gameId: { type: mongoose.Types.ObjectId, required: true, ref: 'Game' },
memberName: { type: String, required: true },
}
);

Mongoose not saving changes to a document

I'm sorry if this might be a duplicate question but I'm quite having a hard time understanding Mongoose. I am working on a Node.js project that implements Mongoose and MongoDB. What I want to accomplish is to modify and save some users' data through a call from a specific endpoint.
Mongoose Schema looks like this
var UserSchema = new Schema({
isAdmin: {type: Boolean, default: false},
name: String,
surname: String,
nickname: { type: String },
email: { type: String, lowercase: true, required: true, trim: true, unique: true, dropDubs: true },
password: { type: String, required: true },
salt: { type: String },
verified: { type: Boolean, default: false },
bio: {
type: { type: String, enum: [0,1] }, // 0='Appassionato', 1='Giocatore'
birthday: String,
height: Number,
number: Number,
role: { type: String, enum: [0,1,2,3] }, // 0='Playmaker', 1='Ala', 2='Guardia', 3='Centro'
team: String,
city: String,
aboutMe: String,
},
newsletter: {type: Boolean, default: false},
lastCheckin: {type: mongoose.Schema.Types.ObjectId, ref: 'Checkin'},
follows: [{type: mongoose.Schema.Types.ObjectId, ref: 'Structure'}],
score: { type: Number, default: 0 },
profilePicture: String,
lastLogin: {type: Date},
facebook: {
id: String,
accessToken: String,
profileImage : String
}
}, {
collection: 'users',
retainKeyOrder: true,
timestamps: true,
}).plugin(mongoosePaginate);
Following is the code for when the endpoint gets interrogated
exports.updateUser = (req,res) => {
var userId = req.params.userId;
var updates = req.body;
User.findOneAndUpdate({_id: userId}, {$set: updates}, (err, saved) => {
if (!err) {
console.log("Ritorno questo: " + saved);
return res.status(202).json(saved);
} else {
return res.status(500).json(saved);
}
});
};
As far as I understood, the method findOneAndUpdate exposed by Mongoose should find the document I'm looking for and then modify it and save it. This doesn't happen though.
Through PostMan I'm sending this JSON
{"bio.aboutMe":"Hello this is just a brief description about me"}
But PostMan is responding with the non-modified object. What am I missing here?
What you need to do is to add {new:true}, it give you back the updated document.
In the documentation :
If we do need the document returned in our application there is
another, often better, option:
> Tank.findByIdAndUpdate(id, { $set: { size: 'large' }}, { new: true },
> function (err, tank) { if (err) return handleError(err);
> res.send(tank); });
This is something I don't really like as there is another option if we don't want to have the document → update
So what you need to do is :
User.findOneAndUpdate({_id: userId}, {$set: updates}, {new:true}.....

Returning specific fields with mongoose

I'm trying to accomplish something really easy but still manage to fail.
What I am trying to do is when I get a get request on my server I want to return all documents BUT just the specific fields populated.
My schema goes as follows
var clientSchema = new Schema({
name:{
type: String,
required: true
},
phone:{
type: String,
required: true
},
email:{
type: String,
required: true
},
address: {
type: String,
required: false
}
});
var orderDetailsSchema = new Schema({
//isn't added to frontend
confirmed:{
type: Boolean,
required: true,
default: false
},
service:{
type: String,
required: true
},
delivery:{
type: String,
required: false
},
payment:{
type: String,
required: false
},
status:{
type: String,
required: true,
default: "new order"
},
});
var orderSchema = new Schema({
reference:{
type: String,
required: true
},
orderdetails: orderDetailsSchema,
client: clientSchema,
wheelspec: [wheelSchema],
invoice:{
type: Schema.Types.ObjectId,
ref: 'Invoice'
}
});
What I want is to return only client.phone and client.email plus orderdetails.status but still retain reference field if possible
I have tried using lean() and populate() but had no luck with them. Is there anything utterly simple I am missing? Or what I am trying to achieve is not that easy?
Thanks!
You can specify the fields to return like this:
Order.findOne({'_id' : id})
.select('client.phone client.email orderdetails.status reference')
.exec(function(err, order) {
//
});
Alternative syntax
Order.findOne({'_id' : id})
.select('client.phone client.email orderdetails.status reference')
.exec(function(err, order) {
//
});
I've made a number of assumptions here, but you should be able to see the idea.
Simply do like this :-
Order is model name which is registered in mongoose.
Order.findById(id) // set id you have to get
. populate('client')
.select('client.phone client.email orderdetails.status reference')
.exec(function(err, order) {
//
});
You can use projection.
await Order.findById(diaryId, {phone: 1, email: 1, status: 1})
If phone:1 is set to 1, it is included, if phone:0, then it's excluded.

Node.js and for loops acting weird

I am working on a project for my internship, where they are using node.js and mongoDB in their back-end. This set up really doesn't work so well at the moment, because we in fact have relational data and mongoDB really isn't the best for that.
My problem is, I have to get some data from the DB that require a "join" on two tables. MongoDB doesn't allow this, so I am trying to do it manually.
I have to find a gig, and for each job type (JobTypeLine) the gig has, find the info about staff members invited. RolodexMembersInterested is an array that contains IDs which allows us to find the staff info in the RolodexMember "table".
The json structures look like this:
Gig:
{
_id: {type: Schema.ObjectId},
CreatedAt: {type: Date, default: Date.now()},
EventName: {type: String, required: true},
Headline: {type: String, required: true},
Date: {type: Date, required: false},
EndDate: {type: Date, required: false},
Address: {type: String, required: false},
Description: {type: String, required: true},
CompanyName: {type: String, required: true},
CompanyID: {type: String, required: true},
Exposed: {type: Boolean, default: false},
HashTags: {type: []},
JobTypeLine: {type: [
{
JobType: {type: String, required: true},
Briefing: {type: String, required: false},
Quantity: {type: Number, required: true},
StartTime: {type: String, required: true},
EndTime: {type: String, required: true},
NumberOfHours: {type: String, required: false},
HourlySalary: {type: Number, required: false},
RolodexMembersInvited: {type: [], default: []},
RolodexMembersAssigned: {type: [], default: []},
RolodexMembersInterested: {type: [], default: []}
}
]},
},
RolodexMember:
{
_id:{type:Schema.ObjectId},
companyId: {type: String, required: true},
userId: {type: String, required: false},
userEmail: {type: String, required: true},
comment: {type: String, required: false},
payRate: {type: Number, required: false},
name: {type: String, required: false},
phone: {type: String, required: false},
pictureUrl: {type: String, required: false},
isHot: {type: Boolean, required: false},
isInPaySystem: {type: Boolean, required: false},
numberOfPreviousGigs: {type: Number, required: true, default: 0},
previousGigs: [],
otherAttributes: [
{
attributeName: {type: String, required: false },
attributeValue: {type: String, required: false }
}
],
})
So I wrote a method to "simulate" my join :
exports.getMembersInvitedToGig = function(req,res) {
var jobTypes = [];
var rolodexInvited = [];
gigsModel.findOne({_id: req.body.gigId}, function(err, gig) {
if(err) {
console.log(err);
res.send(404, "Couldn't find the gig with id: " + req.body.gigId);
} else {
for(var i=0;i<gig.JobTypeLine.length;i++){
rolodexInvited = [];
for(var j=0;j<gig.JobTypeLine[i].RolodexMembersInvited.length;j++) {
var userId = gig.JobTypeLine[i].RolodexMembersInvited[j];
getUserData(userId, function(data,success) {
if (success) {
rolodexInvited[j] = data;
}
});
}
jobTypes[i] = rolodexInvited;
}
res.send(200,jobTypes);
}
});
}
function getUserData (userId, callback) {
var RolodexItem = mongoose.model('RolodexItems');
RolodexItem.findOne({ _id: userId }, function (err, doc) {
if (err) {
console.log("There was an error in finding the rolodex item: " + err);
} else {
callback(doc,true);
}
});
}
My problem is that the method return and empty array filled other empty arrays (as many as I have jobTypes). I know the getUserData function works just fine and finds the right staff info etc. the problem is, it seems like node doesn't wait for the data to be returned inside the for(j) loop and just continues on (and eventually ends gets to the res.send(200,jobTypes) while jobTypes is actually still empty. I have tried adding a callback to the getUserData to solve that problem, but it didn't change anything.
(Now I know using a relational database would solve the problem all together, but I don't really get a say in this, so I have to make this work as it is). There might also be some really bad practices in there, as I just based myself on functions previously made by other people, and I have noticed they weren't really good at it. I have never worked with node.js previously though, so I haven't had time to look as some proper practices/etc. (Feel free to point out anything that is badly written, I'd love to learn the "good" way to do it)
The problem is that your getUserData function is asynchronous (because findOne is asynchronous). So your for loop does not wait for your getUserData function to complete.
You should use the async library (have a look to the each function) : https://github.com/caolan/async

Categories