how should i store a price in mongoose? - node.js

I'm using mongoose schemas for node.js along with express-validator (which has node-validator santiziations and validators).
What's a good way to store price for an item?
I currently have
var ItemSchema = new Schema({
name : { type: String, required: true, trim: true }
, price : Number
});
Price is optional, so I have:
if ( req.body.price ) {
req.sanitize('price').toFloat();
req.assert('price', 'Enter a price (number only)').isFloat();
}
express-validator gives me isNumeric (allows 0 padding), isDecimal, and isInt...I'd rather just convert to decimal and strip all characters, so I'm always inserting 42.00 into db.
I want to allow them to enter $42.00, $42, 42, 42.00 and just store 42.00. How can I accomplish this? and still validate that I'm seeing something resembling a number, for example if they enter 'abc' I want to throw an error back to the form using req.assert.
Also, I suppose currency will eventually become an issue...
Update, I found this post which says to store price as integer in cents, so 4200
https://dba.stackexchange.com/questions/15729/storing-prices-in-sqlite-what-data-type-to-use
I just need a way to convert 4200 to $42.00 when I call item.price and also sanitize and convert the input into 4200.

This is what I ended up doing...
I stored price as cents in database, so it is 4999 for 49.99 as described here: https://dba.stackexchange.com/questions/15729/storing-prices-in-sqlite-what-data-type-to-use
the getPrice will convert it back to readable format, so I can use item.price in my views w/o modifying it.
the setPrice converts it to cents.
model:
var ItemSchema = new Schema({
name : { type: String, required: true, trim: true }
, price : {type: Number, get: getPrice, set: setPrice }
});
function getPrice(num){
return (num/100).toFixed(2);
}
function setPrice(num){
return num*100;
}
I opted to only allow digits and decimal in price field, without $.
So they can enter 49, 49.99, 49.00, but not 49.0 or $49
validation using regex:
if ( req.body.price ) {
req.assert('price', 'Enter a price (numbers only)').regex(/^\d+(\.\d{2})?$/);
}
I wish there was a way to allow the $ because I think its a usability issue, just let the user enter it, but strip it off. I'm not sure how to do that and still validate that we have a price and not a bunch of letters for example.

Hint: The method described here is basically just another implementation of chovy's answer.
Workaround for Mongoose 3 & 4:
If you have trouble to define getters and setters directly in the schema, you could also use the schema.path() function to make this work:
var ItemSchema = new Schema({
name: String,
price: Number
});
// Getter
ItemSchema.path('price').get(function(num) {
return (num / 100).toFixed(2);
});
// Setter
ItemSchema.path('price').set(function(num) {
return num * 100;
});

A bit late but...
The answer of chovy almost worked for me – I just needed to add
{ toJSON: { getters: true }} as an options parameter in the schema declaration.
Example:
import mongoose from 'mongoose'
const productosSchema = new mongoose.Schema(
{
name: String,
price: {
type: Number,
get: v => (v/100).toFixed(2),
set: v => v*100
}
},
{
toJSON: { getters: true } //this right here
}
);
export default mongoose.model('productos', productosSchema)
This works on Mongoose 6.0.14.
References: https://mongoosejs.com/docs/api.html#document_Document-toJSON

Adds schema type "Currency" to mongoose for handling money. Strips out common characters automatically (",", "$" and alphabet chars)
https://github.com/paulcsmith/mongoose-currency
What it does:
Saves a String as an integer (by stripping non digits and multiplying by 100) to prevent rounding errors when performing calculations (See gotchas for details)
Strips out symbols from the beginning of strings (sometimes users include the currency symbol)
Strips out commas (sometimes users add in commas or copy paste values into forms, e.g. "1,000.50)
Only save from two digits past the decimal point ("$500.559" is converted to 50055 and doesn't round)
Strips [a-zA-Z] from strings
Pass in a string or a number. Numbers will be stored AS IS.
Assumes that if you set the value to an integer you have already done the conversion (e.g. 50000 = $500.00)
If a floating point number is passed in it will round it. (500.55 -> 501). Just pass in integers to be safe.
Hope it helps some1.

I've been researching for a while on this topic, because I want to store not only price, but version, which both may have trailing 0s that get chopped off when stored as a number. As far as I know, Mongoose/MongoDB can't save a number with trailing zeroes.
Unless you save the number as a string.
Aside from storing numbers in tens or thousands and dividing or parsing, you can also store it as a string. This means, you can always just print it out when you need to show "1.0" or "1.00" by just using the variable without any conversion. Due to JavaScript being untyped, you can still compare it to numbers (make sure it's on the left hand side). Var < 10, for example, will return the right evaluation, even when var is a string. If you're comparing two variables, you'd need to make sure that they're both numbers, though. When you need a number, you can multiply the string by one (var * 1 < var2 * 1), which will ensure that JavaScript treats the var as a number, although it will lose the trailing zeros.
On the one hand, storing it as a string means you need to do a conversion every time you want to use the variable as a number. On the other hand, you would presumably be doing a numeric conversion anyway (var / 100) every time you want to use a cents number as a dollar amount. This option would depend on how frequently you need to your value as a number. Also it may cause bigger bugs if you forget that your variable is a string than if you forget that your variable is in cents.
(However, it's a great fit for version numbers that would only ever be used for display and comparison.)

The numeral module will accomplish that:
http://numeraljs.com/
https://www.npmjs.com/package/numeral

Related

Mongo db decimal to string is not working as expected

I am using MongoDB to do some calculations as per below.
Creating one collection called "Numbers"
Insert a big number let's say 2^64 ( we can store this big as MongoDB support decimal128 as default data type)
// inserting
db.numbers.insertOne({newBig: 1298074200000000000000000000000000})
// converting
db.numbers.aggregate({$addFields:{test: {$toString: "$newBig"}}})
Now when I am converting the above number to a string, it's not giving expected results.
// getting following output
[{
_id: ObjectId("61c2c1557d78b0f039bcee5a"),
newBig: 1.2980742e+33,
test: '1.29807e+33'
}]
As pointers, M I missing something here?
Please acknowledge, thanks :)

How to effectively use the "Array" schema type to edit a single element and re-save the array

Like what the title said, I'm looking to find the best way to store an array in mongodb (using mongoose) and retrieve the array at anytime and manipulate it?
I tried storing the array as a string in mongoDB and converting it back to an array when I needed to edit it, but I feel that this way is impractical.
So my question is how can I store the array in mongoDB using the "array" Schema Type and what is the best and most effective way to retrive and edit and restore my array?
For example
eg = [0, 1, 2, 3]
I want to edit the array to this:
eg = [0, 1, 6, 3]
Is there a way i could easily access the array?
Although I am not sure exactly what are you trying to accomplish, I will assume that you wish to replace the 3rd value (index=2) in the array (value 2) with the new value 6.
Let's say that schema looks like:
let arrayTestSchema = mongoose.Schema(
{
name: {type: String, required: true},
values: [Number]
}
)
Insert can be done with:
arrayTest.create({name:"test1", values:[0,1,2,3]})
And the replacement with:
arrayTest.updateOne({name:"test1"}, {$set: {"values.2":6}})
Please note that 2 in values.2 represents the index in the array values.

Hapi/Joi Validation For Number Fails

I am trying to validate number value which will include integer as well as float values. Following is my implementation for the same.
Joi Schema.
const numcheckschema = Joi.object().keys({
v1:Joi.number().empty("").allow(null).default(99999),
v2:Joi.number().empty("").allow(null).default(99999),
v3:Joi.number().empty("").allow(null).default(99999)
})
Object
objnum={
v1:"15",
v2:"13.",
v3:"15"
}
objValidated = Joi.validate(objnum, numcheckschema);
console.log(objValidated);
When i execute the above mentioned code I get an error
ValidationError: child "v2" fails because ["v2" must be a number]
as per the documentation when we tries to pass any numeric value as a string it converts the values to number but here in this case my value is 13. which is not able to convert into number and throwing an error.
Is there any way by which we can convert this value to 13.0
You can use a regex in order to match numbers with a dot, for instance:
Joi.string().regex(/\d{1,2}[\,\.]{1}/)
And then combine both validations using Joi.alternatives:
Joi.alternatives().try([
Joi.number().empty("").allow(null),
Joi.string().regex(/\d{1,2}[\,\.]{1}/)
])
However, I think you may need to convert the payload to number using Number(string value). You need to check the payload type, if it isn't a Number, you need to convert it.
If you want to know more about the regex used in the example, you can test it in here: https://regexr.com/

Autocomplete function with underscore/JS

what is the proper way to implement an autocomplete search with undescore?
i have a simple array (cities) and a text input field ($.autocomplete). when the user enters the first letters in the auto-complete textfield, it should output an array with all the cities starting with the entered letters (term).
cities:
["Graz","Hamburg","Innsbruck","Linz","München","Other","Salzburg","Wien"]
eventlistener:
$.autocomplete.addEventListener("change", function(e){
var cities = cities_array;
var term = $.autocomplete.value;
var results = _.filter(cities, function (city){
return
});
console.log(results + "this is results");
});
I’ve tried it with _.contains, but it only returns the city when its a complete match (e.g Graz is only output when „Graz“ is entered but not when „Gr“ is entered).
the _.filter/._select collection at http://underscorejs.org/docs/underscore.html are not very clear for me and the closest i found here is filtering JSON using underscore.
but i don’t understand the indexOf part.
thx for any suggestions!
By using #filter and #indexOf, you can get quite close to a pretty decent autocomplete.
What #indexOf does is that it checks the string if it contains the inputVal.
If it does not contain it it'll return -1 therefore our predicate below will work without fail.
Another small trick here is that you (read I) wanted it to be possible to search for s and get a hit for Innsbruck and Salzburg alike therefore I threw in #toLowerCase so that you always search in lower case.
return _.filter(cities, function(city) {
return city.toLowerCase().indexOf(inputVal.toLowerCase()) >= 0;
});

Custom MongoDB search query

In my database I have documents which all contain the property foo. For each value of foo I have a function that either returns true or false. How can I query for all the documents for which the value of foo makes the function return true?
If you need to check if your string field's value is one of several, you need the $in modifier.
db.collection.find( { field : { $in : array } } );
It works fast and uses index (if possible).
If your field is an array and you pass a string, use this syntax.
db.collection.find({array_field : string_value});
It will check every element in the array and, if any of them matches your string, it will return the document.
You could use $where.
Example:
db.myCollection.find( { $where: "this.a > 3" });
db.myCollection.find( "this.a > 3" );
db.myCollection.find( { $where: function() { return this.a > 3;}});
Note, this is run in Javascript. This means two things.
You can put arbitrary Javacript into $where expression (the function form).
It'll be significantly slower than regular queries.
It really depends on what the function is and how you are using it. Is the function constant for any given record? Is it even a function you can evaluate on the database server? ...
In the extreme, if you need to check this value often, you might, for example, create a field that exists only when f(foo) is true and then create a sparse index on that field.
$where may well be the solution you are looking for, but depending on the access patterns there may be a better solution.

Resources