Saving an object (with defaults) in Mongoose/node - node.js

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)

Related

MongoDB + Mongoose > How to correctly match a set of ISODates in populate()

I'm pretty new to MongoDB and I encounter a weird behavior when using populate. Here are my Schemas:
let userSchema = new Mongoose.Schema({
username: { type: String, required: true },
log: [{ type: Mongoose.Schema.Types.ObjectId, ref: 'Exercise' }],
count: { type: Number },
})
let exerciseSchema = new Mongoose.Schema({
id: { type: Mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
description: { type: String, required: true },
duration: { type: Number, required: true },
date: { type: Date },
})
This is a typical exercise document in my database :
{"_id":{"$oid":"63a9c694b46f8d2d050f7ffc"},
"id":{"$oid":"63a9c5b7b76534298015f2f2"},
"description":"Push ups",
"duration":{"$numberInt":"59"},
"date":{"$date":{"$numberLong":"1641081600000"}},
"__v":{"$numberInt":"0"}}
Everything is working as expected, except when filtering my populate() function by dates (via the match argument).
Here is the get request I am trying to work around :
app.get('/api/users/:_id/logs', function (req, res) {
let logId = req.params._id;
let fromDate = req.query.from ? new Date(req.query.from) : null;
let toDate = req.query.to ? new Date(req.query.to) : null;
let limit = req.query.limit ? { limit: req.query.limit } : {};
let matchQuery;
// Create From and To date options and store in matchQuery object if needed.
if (fromDate || toDate) {
matchQuery = `{ date : { ${fromDate ? "$gte: " + "ISODate(\"" + fromDate.toISOString() + "\")" : ""}${toDate && fromDate ? "," : ""} ${toDate ? "$lte: " + "ISODate(\"" + toDate.toISOString() + "\")" : ""} } }`
}
console.log(matchQuery);
User.find({ _id: logId }).populate({ path: 'log', select: 'description duration name date', match: matchQuery, options: limit }).select('username count _id log').exec(function (err, data) {
if (err) return console.error(err);
console.log(data)
res.json(data);
})
})
If i type in URL : http://localhost:3000/api/users/63a9c5b7b76534298015f2f2/logs,
i get the JSON i'm expecting with all the exercises populated :
[{"log":[{"_id":"63a9c694b46f8d2d050f7ffc","description":"Push ups","duration":59,"date":"2022-01-02T00:00:00.000Z"},{"_id":"63a9c6abb46f8d2d050f7fff","description":"Push ups","duration":45,"date":"2022-01-03T00:00:00.000Z"}],
"_id":"63a9c5b7b76534298015f2f2",
"username":"john",
"count":2}]
But if I start adding filters in the URL like http://localhost:3000/api/users/63a9c5b7b76534298015f2f2/logs?from=2000-01-01&to=2022-02-03 (which should not filter anything and let all results appear) i get:
[{"log":[],"_id":"63a9c5b7b76534298015f2f2","username":"john","count":2}]
So it seems my match request in the populate() function is not working correctly when using date as it just filters out everything whatever the date is. A match applied to other fields works fine (e.g. duration or description).
What am I missing here ? I'm afraid I might not be using ISODate correctly but I cannot be sure. Or maybe the "greater than" and "less than" operators are not formatted correctly ?
I tried all sorts of match expressions but I have to say the documentation on populate() + match + ISODates (and dates in general) is pretty vague so far.
Edit: It seems that the problem was my original query and that what I was passing to the match option was a string which was not parsed by mongodb properly. I made the following changes to pass my populate() query as an object and it works now.
let populateQuery = {
path: 'log',
select: 'description duration name date',
}
if (isMatchQuery) {
// finding alternative solution as Date match on populate doesn't seem to work properly
if (fromDate && toDate) {
populateQuery.match = { date: { $gte: fromDate, $lte: toDate } }
} else if (fromDate) {
populateQuery.match = { date: { $gte: fromDate } }
} else if (toDate) {
populateQuery.match = { date: { $gte: toDate } }
}
//matchQuery = `${fromDate ? "$gte: " + fromDate.toISOString() : ""}${toDate && fromDate ? " , " : ""}${toDate ? "$lte: " + toDate.toISOString() : ""}`
}
if (limit != {}) {
populateQuery.options = limit
}
that I then place in the populate() function like that :
User.find({ _id: logId }).populate(populateQuery).select('username count _id log').exec(function())
Try just without ISODate() (Mongoose should parse it properly):
date: { $gte: fromDate.toISOString(), $lte: toDate.toISOSTring() }

How can I Change value inside array of schemes inside schema with mongoose or mongodb query.?

i have the following schema type:
Paitnet:
var PaitentSchema = new mongoose.Schema({
username: String,
password: String,
name: String,
protocol: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Protocol'
},
treatmentTypes: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'TreatmentType'
}],
accesses: [AccessSchema],
reports: [ReportSchema],
}, { collection: ' Paitents' });
and the AccessSchema:
var AccessSchema = new mongoose.Schema({
stageBool: Boolean,
exerciseBool: [{ type: Boolean }]
});
and what I'm trying to do is to update the exerciseBool array for example change one of the values in the array from 'false' to 'true'.
I have tried this code and its work for me but the Problem is that I get the index from the client so I need to embed the indexes in dynamically way (not always 0 and 1)
here is what I did(not dynamically ):
const paitent = await Paitent.updateOne({ username: req.params.username },
{ $set: { "accesses.0.exerciseBool.1": true } });
I want to do something like this but in dynamically indexes way.
please someone can help me?
thanks.
As you said, indexes are known but values may change.
you can use the following to create your query.
const accessesIndex = 0;
const exerciseBoolIndex = 1;
const update = { $set: { [`accesses.${accessesIndex}.exerciseBool.${exerciseBoolIndex}`]: true } };
console.log(update);
//const paitent = await Paitent.updateOne({ username: req.params.username }, update); // run your update query like this
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals
Update
Check the index exists or not then only update the record.
add into your query "accesses.0.exerciseBool.1": { $exists: true } to make sure the accesses.0.exerciseBool.1 exists in the record.
const accessesIndex = 0;
const exerciseBoolIndex = 1;
const username = 'abc';
const key = `accesses.${accessesIndex}.exerciseBool.${exerciseBoolIndex}`;
const query = { username, [key]: { "$exists": true } };
console.log('query:', query);
const update = { $set: { [key]: true } };
console.log('update:', update);
Update Working demo - https://mongoplayground.net/p/GNOuZr3wqqw
No update demo - https://mongoplayground.net/p/nsTC8s-ruyo
If you are using MongoDB version >= 4.4. you can use $function along with update-with-aggregation-pipeline to update array dynamically. Try this:
let index1 = 0;
let index2 = 1;
db.patients.updateOne(
{ username: "dhee" },
[
{
$set: {
accesses: {
$function: {
body: function(accesses, index1, index2) {
if (accesses[index1] != null
&& accesses[index1].exerciseBool[index2] != null) {
accesses[index1].exerciseBool[index2] = true;
}
return accesses;
},
args: ["$accesses", index1, index2],
lang: "js"
}
}
}
}
]
);

Mongoose async requests managment

I'm actually trying to convert mongodb references into those references' documents value (info.value) using mongoose in javascript.
Tried that by using map, for/forEach, and nothing did the job since mongoose requests are async.
Not really used to this kind of code, I feel a bit lost after all those things I tried.
Maybe someone would like to give me a hint about this by taking a look at the code below.
Just for information, no need to worry about loading templates, connecting to mongo, ... since everything else is working just fine.
That's the closest I got to the expected result, but still, it throws me errors when I try to "console.log(cond[c]);/console.log(info);" (cond[c] and info are null and undefined)
Well this function also needs to be prepared to be recursive since I plan to put sub-blocks in the "content" property of the bloc objects.
Thanks a lot for your time guys.
// Input condition
"H1Vf3KTef || false"
// Expected result
"1 || false"
// Buggy Function
var execIfBlock = function recursExec (query, callback) {
IfBlockModel.findOne(query, function(err, ifBlock) {
if (!err) {
var cond = ifBlock.condition.split(" ");
//console.log('Block : ' + ifBlock);
//console.log('Condition : ' + cond);
var calls = new Array();
for (var c = 0, len = cond.length; c < len; c++) {
if (shortId.isValid(cond[c])) {
calls.push(function() {
InfoModel.findOne({ _id: cond[c] }, function(err, info) {
console.log(cond[c]);
console.log(info);
cond[c] = info.value;
});
});
}
}
async.parallel(calls, function(err, result) {
console.log(result);
// Do some job using the final expected result : "1 || false"
});
}
});
};
// Info template
{
"_id": "H1Vf3KTef",
"value": "1"
}
// Bloc template
{
"_id": "rkRBtLTef",
"content": [],
"condition": "H1Vf3KTef || false"
}
// Info schema
var InfoSchema = new Schema({
_id: { type: String, unique: true, required: true, default: shortId.generate },
value: { type: String, default: "0" }
});
// Bloc schema
var IfBlockSchema = new Schema({
_id: { type: String, unique: true, required: true, default: shortId.generate },
condition: { type: String, required: true, default: true },
content: [{ type: String, required: true, default: '', ref: 'block' }]
});
Use promises and break your code in small functions :
var execIfBlock = function recursExec(query, callback) {
IfBlockModel.findOne(query, function (err, ifBlock) {
if (!err) {
var cond = ifBlock.condition.split(" ");
updateMultipeInfo(cond)
.then(values => {
console.log(values) // [values1, values ,...]
});
}
});
};
function updateMultipeInfo(cond){
return Promise.all(cond.map(updateInfo))
}
function updateInfo(id){
if (shortId.isValid(id)) {
return InfoModel
.findOne({ _id: id })
.then(info => info.value);
} else {
return Promise.reject("invalid id");
}
}

Extra properties on Mongoose schema field?

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

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'}]

Resources