Extra properties on Mongoose schema field? - node.js

Is it possible to add extra / custom attributes to the field in a Mongoose schema? For example, note the name: attribute on the following fields:
var schema = mongoose.Schema({
_id : { type: String, default: $.uuid.init },
n : { type: String, trim: true, name: 'name' },
ac : { type: Date, required: true, name: 'created' },
au : { type: Date, name: 'updated' },
ad : { type: Date, name: 'deleted' },
am : { type: String, ref: 'Member', required: true, name: 'member' }
});
We expect to have a large number of docs in our system and would like to conserve as much space as possible. In this example, we have abbreviated the name of the fields (n vs name, etc.). We would like to use the additional name field to hydrate a JSON object after a fetch.

You could create an instance method (which I called toMappedObject, but you're free to name it however you like) that could perform the conversion by checking the schema for each field to see if it has a name property:
schema.methods.toMappedObject = function() {
let obj = this.toObject();
Object.keys(obj).forEach(fieldName => {
let field = schema.tree[fieldName];
if (field.name) {
obj[field.name] = obj[fieldName];
delete obj[fieldName];
}
});
return obj;
}
// Example usage:
let doc = new Model({...});
let obj = doc.toMappedObject();
Alternatively, you can configure your schema to automatically transform the output generated by toJSON, although it's much more implicit so easy to overlook in case issues pop up (I haven't tested this very well):
schema.set('toJSON', {
transform : function(doc, obj) {
Object.keys(obj).forEach(fieldName => {
let field = doc.schema.tree[fieldName];
if (field.name) {
obj[field.name] = obj[fieldName];
delete obj[fieldName];
}
});
return obj;
}
});
// Example usage:
let doc = new Model({...});
console.log('%j', doc); // will call `doc.toJSON()` implicitly

Related

Concurrency problems updating another's collection stats

I'm trying to make a notation system for movies
A user can note a Movie in their List.
Whenever the user clicks on the frontend, the listId, movieId, note are sent to the server to update the note. The note can be set to null, but it does not remove the entry from the list.
But if the user clicks too much times, the movie's totalNote and nbNotes are completely broken. Feels like there is some sort of concurrency problems ?
Is this the correct approach to this problem or am I updating in a wrong way ?
The mongoose schemas related :
// Movie Schema
const movieSchema = new Schema({
// ...
note: { type: Number, default: 0 },
totalNotes: { type: Number, default: 0 },
nbNotes: { type: Number, default: 0 },
})
movieSchema.statics.updateTotalNote = function (movieId, oldNote, newNote) {
if (!oldNote && !newNote) return
const nbNotes = !newNote ? -1 : (!oldNote ? 1 : 0) // If oldNote is null we +1, if newNote is null we -1
return Movie.findOneAndUpdate({ _id: movieId }, { $inc: { nbNotes: nbNotes, totalNotes: (newNote - oldNote) } }, { new: true }).catch(err => console.error("Couldn't update note from movie", err))
}
// List Schema
const movieEntry = new Schema({
_id: false, // movie makes an already unique attribute, which is populated on GET
movie: { type: Schema.Types.ObjectId, ref: 'Movies', required: true },
note: { type: Number, default: null, max: 21 },
})
const listSchema = new Schema({
user: { type: Schema.Types.ObjectId, ref: 'Users', required: true },
movies: [movieEntry]
})
The server update API (add / Remove movieEntry are similar with $push and $pull instead of $set)
exports.updateEntry = (req, res) => {
const { listId, movieId } = req.params
const movieEntry = { movieId: movieId, note: req.body.note }
List.findOneAndUpdate({ _id: listId, 'movies.movie': movieId }, { $set: { 'movies.$[elem]': movieEntry } }, { arrayFilters: [{ 'elem.movie': movieId }] })
.exec()
.then(list => {
if (!list) return res.sendStatus(404)
const oldNote = list.getMovieEntryById(movieId).note // getMovieEntryById(movieId) = return this.movies.find(movieEntry => movieEntry.movie == movieId)
Movie.updateTotalNote(movieId, oldNote, movieEntry.note)
let newList = list.movies.find(movieEntry => movieEntry.movie == movieId) // Because I needed the oldNote and findOneAndUpdate returns the list prior to modification, I change it to return it
newList.note = movieEntry.note
newList.status = movieEntry.status
newList.completedDate = movieEntry.completedDate
return res.status(200).json(list)
})
.catch(err => {
console.error(err)
return res.sendStatus(400)
})
}
The entries I needed to update were arrays that could grow indefinitely so I had to first change my models and use virtuals and another model for the the list entries.
Doing so made the work easier and I was able to create, update and delete the entries more easily and without any concurrency problems.
This might also not have been a concurrency problem in the first place, but a transaction problem.

Update Mongoose Array

so basically I have this and I am trying to update the STATUS part of an array.
However, everything I try does nothing. I have tried findOneAndUpdate also. I am trying to identify the specific item in the array by the number then update the status part of that specific array
(Sorry for formatting, I have no idea how to do that on the site yet ...) (Full code can be found here: https://sourceb.in/0811b5f805)
Code
const ticketObj = {
number: placeholderNumber,
userID: message.author.id,
message: m.content,
status: 'unresolved'
}
let tnumber = parseInt(args[1])
let statuss = "In Progress"
await Mail.updateOne({
"number": tnumber
}, { $set: { "status": statuss } })
Schema
const mongoose = require('mongoose')
const mailSchema = new mongoose.Schema({
guildID: { type: String, required: true },
ticketCount: { type: Number, required: true },
tickets: { type: Array, default: [] }
}, { timestamps: true });
module.exports = mongoose.model('Mail', mailSchema)
You need to use something like Mail.updateOne({"guildID": message.guild.id}, {$set: {`tickets.${tnumber}.status`: statuss}})
or for all objects in array:
Mail.updateOne({"guildID": message.guild.id}, {$set: {'tickets.$[].status': statuss}})
Also, you need to create a schema for the tickets, as it is described in docs:
one important reason to use subdocuments is to create a path where there would otherwise not be one to allow for validation over a group of fields

How to update mixed type field in Mongoose without overwriting the current data?

I have the following schema
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var ShopSchema = new Schema({
name: Schema.Types.Mixed,
country: {
type: String,
default: ''
},
createdAt: {
type: Date,
default: Date.now
},
defaultLanguage: {
type: String
},
account: {type : Schema.ObjectId, ref : 'Account'},
});
mongoose.model('Shop', ShopSchema);
"name" field is multilingual. I mean, I will keep the multilingual data like
name: {
"en": "My Shop",
"es": "Mi Tienda"
}
My problem is, in a controller, I am using this code to update the shop:
var mongoose = require('mongoose')
var Shop = mongoose.model('Shop')
exports.update = function(req, res) {
Shop.findByIdAndUpdate(req.params.shopid, {
$set: {
name: req.body.name
}
}, function(err, shop) {
if (err) return res.json(err);
res.json(shop);
});
};
and it is obvious that new data overrides the old data. What I need is to extend the old data with the new one.
Is there any method to do that?
You should to use the method .markModified(). See the doc http://mongoosejs.com/docs/schematypes.html#mixed
Since it is a schema-less type, you can change the value to anything else you like, but Mongoose loses the ability to auto detect and save those changes. To "tell" Mongoose that the value of a Mixed type has changed, call the .markModified(path) method of the document passing the path to the Mixed type you just changed.
person.anything = { x: [3, 4, { y: "changed" }] };
person.markModified('anything');
person.save(); // anything will now get saved
Use "dot notation" for the specific element:
Shop.findByIdAndUpdate(req.params.shopid, {
"$set": {
"name.en": req.body.name
}
}, function(err, shop) {
if (err) return res.json(err);
res.json(shop);
});
});
That wil either only overwrite the "en" element if that is what you want to do or "create" a new element with the data you set it to. So if you used "de" and that did not exist there will be the other elements and a new "de" one with the value.

Mongoose fail to set ref -Schema.Types.ObjectId- to other document

I'm trying to save a document with mongoose according to the doc http://mongoosejs.com/docs/populate.html
First I call parent.save and inside the callback of parent.save I use child.save.
But when I check parent.childs I can see that no child has been added.
The parent schema is Home :
var HomeSchema = new Schema({
password : String,
draft : { type: Boolean, default: true },
edited : { type: Boolean, default: false },
guests : [{type : Schema.Types.ObjectId, ref : 'Guest'}],
_event : {type : Schema.Types.ObjectId, ref : 'Event'}
});
the child schema is Guest :
var GuestSchema = new Schema({
_home : {type : Schema.Types.ObjectId, ref : 'Home'},
firstname : String,
lastname : String,
coming : { type: String, default: 'dnk-coming' },
phone : String,
email : String,
address : String,
edited : { type: Boolean, default: false },
draft : { type: Boolean, default: true }
});
To avoid any misunderstanding, you have to know that this two Schema are included in my user schema :
var userSchema = mongoose.Schema({
homes:[homeSchema.HomeSchema],
events:[eventSchema.EventSchema],
guests:[eventSchema.guestSchema],
});
Now you should have all the required informations to completly understand the execution :
UserModel.findById(user._id, function(err, userFound) {
if (!err) {
/* cleaning draft*/
userFound.homes = that.clean(userFound.homes);
/* setting draft */
var HomeModel = mongoose.model("Home");
var homeModel = new HomeModel();
homeModel.draft = true;
if (userFound.homes === null) {
userFound.homes = [];
}
homeModel.save(function(err) {
if (!err) {
var GuestModel = mongoose.model("Guest");
var guestModel = new GuestModel();
guestModel._home = homeModel._id;
guestModel.save(function(err) {
if (!err) {
// #ma08 : According to the doc this line should'nt be required
//homeModel.guests.push(guestModel._id); so when I use this obviously the id is correctly set but when I try a populate after saving the populate do not work
userFound.homes.push(homeModel);
userFound.guests.push(guestModel);
userFound.save(function(err) {
if (!err) {
successCallback();
}
else {
errorCallback();
}
});
}
});
}
});
This treatement doesn't result in any error. But it doesn't work as intended when I stringify the user.guests I get :
guests:
[ { coming: 'dnk-coming',
edited: false,
draft: true,
_id: 53dcda201fc247c736d87a95,
_home: 53dce0f42d5c1a013da0ca71,
__v: 0 }]
wich is absolutly fine I get the _home id etc...
Then I stringify the user.homes and I get :
homes:
[ { draft: true,
edited: false,
guests: [],
_id: 53dce0f42d5c1a013da0ca71,
__v: 0 } ]
According to the doc guests should be setted, but it's not <-- this is my issue. Please help me to figure out what I'm doing wrong. I could set it manualy but according to the doc it's not suppose to work this way I think.
guestModel.save(function(err) {...
this is wrong because you are embedding the guests in the userSchema.
So skip the guestModel.save and just push the guestModel in userFound
An embedded document can never a reference. You can't point to it without obtaining the parent document. So you can't do both embedding and keeping a ref to the embedded document. You should choose between either embedding or adding a ref.
My suggestion would be to design your schemas like this. Store guests in a separate collection. Store the ref to guest in user and home schemas. If you want to store some relationship data you can store along with the ref like [{guestId:{type:Schema.Types.ObjectId,ref:'Guest'},field1:{type:...},field2:{...}..] and if you just want the ref [{type:Schema.Types.ObjectId,ref:'Guest'}]

Saving an object (with defaults) in Mongoose/node

I have an object passed to a function, and want to save a DB entry with those values, with the option to have some defaults.
In practical terms...
I have a schema like this in Mongoose:
var Log = new Schema({
workspaceId : { type: String, index: true },
workspaceName : { type: String, index: true },
loginSession : { type: String, index: true },
loginToken : { type: String, index: true },
logLevel : { type: Number, enum:[0,1] },
errorName : { type: String, index: true },
message : { type: String, index: true },
reqInfo : { type: String },
data : { type: String },
loggedOn : { type: Date, index: true },
});
mongoose.model('Log', Log);
To write things on this table, I have something like:
exports.Logger = function(logEntry){
var Log = mongoose.model("Log"),
req = logEntry.req;
log = new Log();
// Sorts out log.reqInfo
if ( logEntry.req){
log.reqInfo = JSON.stringify({
info : req.info,
headers: req.headers,
method : req.method,
body :req.body,
route :req.route,
params: req.params
});
} else {
logEntry.reqInfo = {};
}
// Sorts out all of the other fields with sane defaults.
// FIXME: improve this code, it's grown into something ugly and repetitive
log.workspaceId = logEntry.workspaceId ? logEntryworkspaceId. : '';
log.workspaceName = logEntry.workspaceName ? logEntry.workspaceName : '';
log.loginSession = logEntry.loginSession ? logEntry.loginSession : '';
log.loginToken = logEntry.loginToken ? logEntry.loginToken : '';
log.logLevel = logEntry.logLevel ? logEntry.logLevel : 0;
log.errorName = logEntry.errorName ? logEntry.errorName : '';
log.message = logEntry.message ? logEntry.message : '';
log.data = logEntry.data ? logEntry.data : {};
// Sorts out log.loggedOn
log.loggedOn = new Date();
log.save();
}
This is absolutely awful code. What's a better way of writing it, without the repetition?
I dont understand your code. So, if a value isn't set you want it to be set to empty string ''?
If you want defaults, easiest way is to just define them in your schema.
var Log = new Schema({
workspaceId : { type: String, index: true, default: 'your default here' },
//...
loggedOn : { type: Date, index: true, default: Date.now }
});
From docs
Something like this might be a bit more elegant:
Create a dictionary containing the default values
defaults = {
workspaceName: 'foo',
loginSession: 'bar',
loginToken: 'baz'
};
console.log(defaults)
Given some values
values = {
workspaceName: 'WNAME1',
loginSession: 'LS1',
somethingElse: 'qux'
};
console.log(values)
If values doesn't contain an entry for any of the specified defaults, add the default value to values
for(i in defaults){
if(!values[i]){
values[i] = defaults[i];
}
}
console.log(values)

Categories

Resources