Mongoose update dependent field from another field's setter? - node.js

I have a scenario in node/express/mongoose where I have users who accumulate points, and when that point total crosses a certain threshold, they "level up" (think games with point-based levels).
I have created a custom setter on the points field that checks if the value has changed, and if so tries to update the level field. Levels are defined in another collection, but are saved as simple JSON objects when associated with user docs (hence the .lean() in the query). I did it this way vs a virtual field or population for efficiency.
Problem: this doesn't actually seem to update the user 'level' field when it should. What am I doing wrong?
// Define our user schema
var UserSchema = new mongoose.Schema({
...
points: {type: Number, default: 0, set: pointsChangeHandler},
level: {name: String, minPoints: Number, maxPoints: Number},
...
});
And the setter looks like so:
function goodPointsChangeHandler(newValue) {
var self = this;
if (newValue != self.goodPoints) {
console.log('Point change detected.');
// Find level that corresponds with new point total
Level.findOne({
'minPoints': {$lte : self.goodPoints},
'maxPoints': {$gt : self.goodPoints}}, '-_id').lean().exec(function(err, level) {
if (self.goodLevel == undefined || self.goodLevel.rank != level.rank) {
console.log('Level changed.');
self.goodLevel = level;
}
return newValue;
});
}
return newValue;
}

Based on #laggingreflex's comment, I tried modifying this within the scope of the model method (i.e. not in the Level.findOne() callback, and changes made that way were persisted without an explicit save() call.
Also, I had a pretty silly error where I was returning newValue from thefindOne` callback.. not sure what I was thinking there...
Long story short, and this may be obvious to node/express/mongoose experts, but you can modify fields other than the one whose setter method you're currently in, but the moment you find yourself in the callback of another async method, you'll have to do an explicit save() or your modifications to this will not be persisted.
So:
function myFieldSetterMethod(newValue) {
this.myField = "a";
this.myOtherField = "b";
return newValue;
// no save() necessary, this will update both fields
}
function myFieldSetterMethod(newValue) {
this.myField = "a";
SomeModel.findOne(..., function(err, doc) {
this.myOtherField = doc.somethingIWantFromThisDoc;
// now you'll need an explicit save
// this.save(...)
});
return newValue;
}

Related

Mongoose custom SchemaType toJSON/Transform

For an application I'm currently working on I need to store dates without a time. I've done this by creating a custom schema type that looks something like so:
var mongoose = require('mongoose');
/**
* Registers a new DateOnly type field. Extends the `Date` Schema Type
*/
function DateOnly(key, options) {
mongoose.SchemaTypes.Date.call(this, key, options, 'DateOnly');
}
DateOnly.prototype = Object.create(mongoose.SchemaTypes.Date.prototype);
DateOnly.prototype.cast = (originalValue) => {
try {
var value = originalValue;
if (typeof value === 'string' && !value.match(/^([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))(T00:00:00.000Z)?$/)) {
throw new Error('Date is invalid');
} else if (typeof value === 'number') {
value = new Date(value);
}
if (value instanceof Date) {
value = new Date(value.getFullYear(), value.getMonth(), value.getDate());
}
return mongoose.Schema.Types.Date._cast(value);
} catch (err) {
throw new mongoose.SchemaType.CastError('date', originalValue, this.path);
}
};
mongoose.Schema.Types.DateOnly = DateOnly;
module.exports = DateOnly;
Which allows the model to accept date strings (ex: 2020-01-01) and date objects. Now this will store all dates at midnight UTC time, that way I still get all the advantages of them being stored as dates in mongodb.
Where my issue comes in is that when I'm returning one of these dates to the API it gets returned in full ISO format (ex: 2020-01-01T00:00:00.000Z), which will get converted into the local user's time zone. In my timezone, this date will show up as 1 day earlier than desired.
So my question is, how can I make it so that when document.toJSON is called the date is transformed? I know that what I want to be returning is date.toISOString().substring(0,10).
I've tried inheriting from the Date class, but I discovered it isn't compatible with how mongoose and the mongodb driver work.
I know I could write a method to put in the toJSON.transform options, but then I'd have to do this for every field and model that uses the type.
A solution for this was added in mongoose 5.9.0, and can be done like so:
DateOnly.set('transform', (val) => {
return /* transformed value */;
});
Source: https://github.com/Automattic/mongoose/issues/8403

Why is my Mongoose find query not returning the expected data?

The following function is to return all menu items of a specific type. The types exist in the database but the query is returning no data. Without the query criterion: {type:menuItem}, it returns the data with the the type.
exports.get = async (menuItem) => {
console.log(chalk.green('menuItem', menuItem));
try {
const menu = await Menu.find({type:menuItem});
console.log('menu', menu);
return menu;
} catch(error) {
console.log(error)
}
}
MenuItem logged to the console as shown in the function above produces the following. The problem seem to be with the variable, menuItem, because when I hard-code, {type:'pizza'}, it works.
console.log(chalk.green('menuItem', menuItem));
menuItem 'pizza'
The anuglar query string looks like the following, is there a problem with this:
private menuUrl = '/api/menu?menuItem="pizza"';
Sample data from the database:
{ quantity: 1,
toppings: [ 'chipotle-steak', 'red-onions' ],
_id: 5d163a7ae7179a4e432b501a,
type: 'pizza',
name: 'CHIPOTLE STEAK',
price: 15,
img: 'Chipotle-Steak.png',
}
The key type is used to tell mongoose about the type of that particular field. thats why using type as a field in mongoose schema can sometimes behave ambiguously.
Kindly change type to kind and search with {kind : menuItem} and it should work for you.
Dont forget to update all the docs from type to kind.
To update :
Menu.update({},{$rename : {'type' : 'kind'}},{multi:true})
Then find using kind
const menu = await Menu.find({kind:menuItem});
Silly mistake. The Angular query string had single quotes around pizza:
private menuUrl = "/api/menu?menuItem='pizza'";
It should have been without quotes instead:
private menuUrl = "/api/menu?menuItem=pizza";

MongoDB and Mongoose - Creating custom, autoindexed secondary IDs?

I initialize my DB in the usual way:
mongoose.connect(`mongodb://uname:pword#127.0.0.1:port/dbname?authSource=admin`, {useNewUrlParser: true, autoIndex: false});
And I have a Schema, something like:
var materialSchema = new Schema({
bookID: {type: String, required: true},
active: Boolean,
name: {type: String, required: true},
stockLength: {type: Number, required: true}
});
module.exports = mongoose.model('material', materialSchema);
When I create a new material and add it to the database, it is automatically assigned the usual _id - which is a behaviour I want to maintain. BUT, I'd also like for bookID to be a unique, auto-incrementing index. This is for physical shelf storage, and not for queries or anything like that.
I'd like for bookID to increment in the following way:
A-001
A-002
A-003
...
A-098
A-099
A-100
B-001
...
B-100
...
Z-001
...
Z-100
In case the pattern above isn't clear, the pattern starts at A-001 and ultimately ends at Z-100. Each letter goes from 001 through 100 before moving to the next letter. Each new collection entry is just the next ID in the pattern. It is unlikely that the end will ever be reached, but we'll cross that bridge when we get there.
I've only ever used the default _id for indexing, and can't figure out how to make this pattern.
Thanks for any insight!
Edit #1
The best solution I've come up with so far is to have a separate .txt file with all of the IDs listed in order. As each new object is created, pop (... shift) the next ID off the top of the file. This might also have the added benefit of easily adding additional IDs at a later date. This will probably be the approach I take, but I'm still interested in the mongoose solution requested above.
Edit #2
So I think the solution I'm going to use is a little different. Basically, findOne sorted by bookID descending. Then use the value returned to set the next.
Material.findOne()
.sort({bookID : -1})
.exec((err, mat) => {
if(err) {
// Send error
}else if(!mat) {
// First bookID
}else {
// Indexes exist...
let nextId = getNextID(mat.bookID);
// ...
}
});
Still easy to modify getNextID() to add new/different IDs in the future (if/when "Z100" is reached)
Thanks again!
Ok, so to expand a little bit on Edit #2, I've come up with the following solution.
Within the model (schema) file, we add a schema pre() middleware, that executes when .save() is called, before the save occurs:
// An arrow function will not work on this guy, if you want to use the "this" keyword
materialSchema.pre('save', function(next) {
this.model('material').findOne() // Don't forget the .model(...) bit!
.sort({bookID : -1}) // All I need is the highest (i.e. most recent) bookID
.select('bookID') // Ditto above (not really necessary)
.exec((err, result) => {
if(err) {
return next(err); // Oopsies, an error!
}else if(!result) {
this.bookID = 'A-001'; // The case when collection is empty
}else {
this.bookID = getNextID(result.bookID); // Otherwise, increment ID
}
next(); // Don't forget this sucker! This is how you save
});
});
And that's about it! It isn't an in-built solution direct from Mongoose, but it works a treat.
Just for completeness, the getNextID function looks like:
function getNextID(curID) {
let letter = curID.split('-')[0];
let number = parseInt(curID.split('-')[1]);
if(number >= 100) { // Increase the letter and reset the number
letter = String.fromCharCode(letter.charCodeAt(0) + 1)
number = '001';
}else { // Only increase the number
number = ('' + (number + 1)).padStart(3, '0'); // Makes sure the numbers are always 3 digits long
}
return `${letter}-${number}`;
}
This'll do just dandy for now. Until we get to Z100. But I'll cross that bridge if/when it comes. No big deal at all.
And you don't need to do anything special to use it. Just save a new doc as normal, and it automatically fires:
new Material({
// New material properties
}).save((err, mat) => {
// Handle errors and returns ...
});

How to define a sort function in Mongoose

I'm developing a small NodeJS web app using Mongoose to access my MongoDB database. A simplified schema of my collection is given below:
var MySchema = mongoose.Schema({
content: { type: String },
location: {
lat: { type: Number },
lng: { type: Number },
},
modifierValue: { type: Number }
});
Unfortunately, I'm not able to sort the retrieved data from the server the way it is more convenient for me. I wish to sort my results according to their distance from a given position (location) but taking into account a modifier function with a modifierValue that is also considered as an input.
What I intend to do is written below. However, this sort of sort functionality seems to not exist.
MySchema.find({})
.sort( modifierFunction(location,this.location,this.modifierValue) )
.limit(20) // I only want the 20 "closest" documents
.exec(callback)
The mondifierFunction returns a Double.
So far, I've studied the possibility of using mongoose's $near function, but this doesn't seem to sort, not allow for a modifier function.
Since I'm fairly new to node.js and mongoose, I may be taking a completely wrong approach to my problem, so I'm open to complete redesigns of my programming logic.
Thank you in advance,
You might have found an answer to this already given the question date, but I'll answer anyway.
For more advanced sorting algorithms you can do the sorting in the exec callback. For example
MySchema.find({})
.limit(20)
.exec(function(err, instances) {
let sorted = mySort(instances); // Sorting here
// Boilerplate output that has nothing to do with the sorting.
let response = { };
if (err) {
response = handleError(err);
} else {
response.status = HttpStatus.OK;
response.message = sorted;
}
res.status(response.status).json(response.message);
})
mySort() has the found array from the query execution as input and the sorted array as output. It could for instance be something like this
function mySort (array) {
array.sort(function (a, b) {
let distanceA = Math.sqrt(a.location.lat**2 + a.location.lng**2);
let distanceB = Math.sqrt(b.location.lat**2 + b.location.lng**2);
if (distanceA < distanceB) {
return -1;
} else if (distanceA > distanceB) {
return 1;
} else {
return 0;
}
})
return array;
}
This sorting algorithm is just an illustration of how sorting could be done. You would of course have to write the proper algorithm yourself. Remember that the result of the query is an array that you can manipulate as you want. array.sort() is your friend. You can information about it here.

Creating incrementing numbers with mongoDB

We have an order system where every order has an id. For accounting purposes we need a way to generate invoices with incremening numbers. What is the best way to do this without using an sql database?
We are using node to implement the application.
http://www.mongodb.org/display/DOCS/How+to+Make+an+Auto+Incrementing+Field
The first approach is keeping counters in a side document:
One can keep a counter of the current _id in a side document, in a
collection dedicated to counters. Then use FindAndModify to atomically
obtain an id and increment the counter.
The other approach is to loop optimistically and handle dup key error code of 11000 by continuing and incrementing the id for the edge case of collisions. That works well unless there's high concurrency writes to a specific collection.
One can do it with an optimistic concurrency "insert if not present"
loop.
But be aware of the warning on that page:
Generally in MongoDB, one does not use an auto-increment pattern for
_id's (or other fields), as this does not scale up well on large database clusters. Instead one typically uses Object IDs.
Other things to consider:
Timestamp - unique long but not incrementing (base on epoch)
Hybrid Approach - apps don't necessarily have to pick one storage option.
Come up with your id mechanism based on things like customer, date/time parts etc... that you generate and handle collisions for. Depending on the scheme, collisions can be much less likely. Not necessarily incrementing but is unique and has a well defined readable pattern.
I did not find any working solution, so I implemented the "optimistic loop" in node.js to get Auto-Incrementing Interger ID fields. Uses the async module to realize the while loop.
// Insert the document to the targetCollection. Use auto-incremented integer IDs instead of UIDs.
function insertDocument(targetCollection, document, callback) {
var keepRunning = true;
var seq = 1;
// $type 16/18: Integer Values
var isNumericQuery = {$or : [{"_id" : { $type : 16 }}, {"_id" : { $type : 18 }}]};
async.whilst(testFunction, mainFunction, afterFinishFunction);
// Called before each execution of mainFunction(). Works like the stop criteria of a while function.
function testFunction() {
return keepRunning;
}
// Called each time the testFunction() passes. It is passed a function (next) which must be called after it has completed.
function mainFunction(next) {
findCursor(targetCollection, findCursorCallback, isNumericQuery, { _id: 1 });
function findCursorCallback(cursor) {
cursor.sort( { _id: -1 } ).limit(1);
cursor.each(cursorEachCallback);
}
function cursorEachCallback(err, doc) {
if (err) console.error("ERROR: " + err);
if (doc != null) {
seq = doc._id + 1;
document._id = seq;
targetCollection.insert(document, insertCallback);
}
if (seq === 1) {
document._id = 1;
targetCollection.insert(document, insertCallback);
}
}
function insertCallback(err, result) {
if (err) {
console.dir(err);
}
else {
keepRunning = false;
}
next();
}
}
// Called once after the testFunction() fails and the loop has ended.
function afterFinishFunction(err) {
callback(err, null);
}
}
// Call find() with optional query and projection criteria and return the cursor object.
function findCursor(collection, callback, optQueryObject, optProjectionObject) {
if (optProjectionObject === undefined) {
optProjectionObject = {};
}
var cursor = collection.find(optQueryObject, optProjectionObject);
callback(cursor);
}
Call with
insertDocument(db.collection(collectionName), documentToSave, function() {if(err) console.error(err);});

Resources