Mongoose js validation on sub object multiple fields - node.js

I am attempting to validate an entire object (multipel fields) within mongoose.
I have the following custom schema definition:
module.exports.ItemQuantityPair = (ItemModel, strictStacking)=>{
return {
type: {
itemID: { type:Number, required:true },
quantity: { type:Number, min:1, default:1 },
},
validate: {
validator: async value => { //stacking
if(!strictStacking || value.quantity===1) //if no strict stacking, or only quantity of 1 don't validate
return true;
let item = await ItemModel.findOne({itemID:value.itemID}); //otherwise get the item and check if its stackable
if(!item.stackable)
return false; //invalid, quantity > 1 and not stackable
else
return true; //valid, quantity > 1 and stackable
},
message: props=>`${props.value} this item was supplied with a quantity larger than 1, but the item was found to not be stackable. Ensure only stackable items have quantity > 1`
},
}
}
Then later in my schema I use this custom object like so:
const AccountSchema = new mongoose.Schema({
inventory: [ItemQuantityPair(ItemModel, true)] //inventory is a list of ItemQuantity pairs
});
When I try this, I'm presented with the error TypeError: Invalid schema configuration: 'undefined' is not a valid type within the array 'inventory' I assume this is because I can't assign an object tp the type field since these are the only built-in types: https://mongoosejs.com/docs/schematypes.html#what-is-a-schematype. Normally I would validate each field separately, but this specific validation requires both fields' values (quantity and itemID) to be determined as valid or not.
How can I cleanly accomplish validation on multiple fields?
Thanks

Related

Unique key only in array of subschemas?

I have a schemas. One of its keys takes the type of an array of schemas.
let instanceSchema = new Mongoose.Schema({
name: { type: String, required: true, unique: true }
})
let mainSchema = new Mongoose.Schema({
instances: { type: [instanceSchema], required: true }
})
So the mainSchema has a key that is an array of instanceSchemas. I want the behavior to be such that I can have instanceSchemas with the same name if they are members of different mainSchemas, like so:
let MainModel = mongoose.model("MainModel", mainSchema);
// Succeeds
main1 = new MainModel({
"instances": [ {"name": "Instance1"}, {"name": "Instance2"} ];
});
// Succeeds
main2 = new MainModel({
"instances": [ {"name": "Instance1"}, {"name": "Instance2"} ];
});
As shown, we have two named "Instance1" and two named "Instance2" but they are members of different documents (main1 and main2) so my target behavior is that this should be allowed. However, using unique prevents this from happening as Mongoose checks all instanceSchema models. Is there a way to allow duplicates as long as they are members of different documents?
Short answer: no, unique doesn't work like that.
When you tag a field unique in a mongoose schema, it will create a unique index on that field in MongoDB.
MongoDB index entries are made per-document, not per array entry.
For example, if you have an index on {field :1} and insert the document {field:["a","b","a","b","c","d"]}, then the entries in that index for this document will be:
"a"
"b"
"c"
"d"
In MongoDB, when an index is created with the unique: true option, it enforce that any value only appears once #in the index#. This means that the above document would be perfectly acceptable even if the index on {field: 1} were unique.
To quickly demonstrate this, I used your model defined above, and executed:
res1 = await (new MainModel({instances:[{name:"1"},{name:"2"},{name:"1"}]})).save();
console.log("Inserted: ",JSON.stringify(res1));
res2 = await MainModel.collection.getIndexes({full: true});
console.log("Indexes: ",JSON.stringify(res2));
res3 = await (new MainModel({instances:[{name:"3"},{name:"2"},{name:"4"}]})).save();
console.log("Inserted: ",JSON.stringify(res3));
This logged:
Inserted: {"_id":"60d273060ffb1ac8e48359e5","instances":[{"_id":"60d273060ffb1ac8e48359e6","name":"1"},{"_id":"60d273060ffb1ac8e48359e7","name":"2"},{"_id":"60d273060ffb1ac8e48359e8","name":"1"}],"__v":0}
Indexes: [{"v":2,"key":{"_id":1},"name":"_id_"},{"v":2,"unique":true,"key":{"instances.name":1},"name":"instances.name_1","background":true}]
/Users/joe/temp/mongoose/node_modules/mongodb/lib/core/error.js:57
return new MongoError(options);
^
MongoError: E11000 duplicate key error collection: test.mainmodels index: instances.name_1 dup key: { instances.name: "2" }
at Function.create
... snip ... {
driver: true,
index: 0,
code: 11000,
keyPattern: { 'instances.name': 1 },
keyValue: { 'instances.name': '2' }
}
As you can see, it create a unique index on { 'instances.name': 1 }, permitted duplicate entries within a single document, and prohibited an identical entry in the other document.

how to query mongoDb with array of boolean fields?

My mongoose model schema looks like -
{
email: { type: String},
Date: {type: Date},
isOnboarded: {type: Boolean},
isVip: {type: Boolean},
isAdult: {type: Boolean}
}
In my frontend I have 3 checkboxes for "isVip", "isOnboarded" and "isAdult" options. If they are checked I'm adding them to an array, which I'll pass to the server. Let's say if "isVip" and "isAdult" are checked, I will pass [isVip, isAdult] in post api to server. Now how can I write a query to get all the documents with the fields in array as true i.e in above example how can I retrieve all docs with {isVip: true, isAdult:true}
I'm having trouble because the array values keep changing, it can be only one field or 3 fields. I couldn't find a way to give condition inside mongoose query.
User.find(
{ [req.query.array]: true},
{ projection: { _id: 0 } }
)
User is my mongoose model.
I want something like this (documents with the value 'true' for the fields given in the array) and 'req.query.array' is the array with field names I passed from frontend.
You have to create your object in JS and pass then to mongo in this way:
var query = {}
if(isVip) query["isVip"] = true;
if(isOnboarded) query["isOnboarded"] = true;
if(isAdult) query["isAdult"] = true;
And then you can use the mongoose method you want, for example:
var found = await model.find(query)
And this will return the document that matches the elements.
Also, note that this is to create and object to be read by the query, so you can use this into an aggregation pipeline or whatever you vant
Check the output:
var query = {}
query["isVip"] = true;
query["isOnboarded"] = true;
query["isAdult"] = true;
console.log(query)
Is the same object that is used to do the query here
{
"isVip": true,
"isOnboarded": true,
"isAdult": true
}
Also, to know if your post contains "isVip", "isOnboarded" or "isAdult" is a javascript question, not a mongo one, but you can use something like this (I assume you pass a string array):
var apiArray = ["isVip","isAdult"]
var isVip = apiArray.includes("isVip")
var isAdult = apiArray.includes("isAdult")
var isOnboarded = apiArray.includes("isOnboarded")
console.log("isVip: "+isVip)
console.log("isAdult: "+isAdult)
console.log("isOnboarded: "+isOnboarded)

Mask data in Mongoose find operation

The requirement is to mask mobile number and show only last 4 digits. I do not want this to be performed at client instead mask it before sending the response. I am not sure how to modify transaction object to mask the data. I want to check if there is any mongoose function to do this. If not please suggest me the best way to mask a selected field.
Logic to fetch transactions
Transaction.find(query).populate('from','name mobile email').sort({ createdAt : -1 }).skip((page) * limit).limit(limit).exec((err, transaction) =>{
if(transaction){
Transaction.countDocuments({to:id,paymentStatus:"SUCCESS"},function(err,count){
return res.status(200).send({transaction,count:count});
});
}
else if(transaction==null) return res.status(200).send("No Transactions Found");
else if(err) return res.status(400).send("Error Occurred");
});
User.Model.ts - Merchant model is similar with some additional fields
var User= new mongoose.Schema({
email:{type:String,required:"E-Mail address cannot be empty",unique:true},
mobile:{type:String,required:"Mobile number cannot be empty",min : [10,"Please provide a valid 10 digit mobile number"],unique:true},
password:{type:String,required:"Password cannot be empty",minlength : [4,"Password must be more than 4 characters"]},
.......some unrelated fields...
});
Transaction.Model.ts
var transactionSchema = new mongoose.Schema({
from:{ type: mongoose.Schema.Types.ObjectId, required: true, ref: 'User' },
amount : {type:String,required:true},
to:{ type: mongoose.Schema.Types.ObjectId, required: true, ref: 'Merchant' },
paymentStatus:{type : String, default : "INCOMPLETE"},
.......some unrelated fields...
});
Current output
{"transaction":[{"paymentStatus":"SUCCESS","isDisputed":true,"_id":"5eb8e50b3e2adb3b74e85d4f","from":{"_id":"5eb8e50a3e2adb3b74e85d43","name":"John Doe","email":"test#gmail.com","mobile":"9999999999"},"amount":"40","to":"5eb8e50a3e2adb3b74e85d46"}],"count":1}
Expected output
{"transaction":[{"paymentStatus":"SUCCESS","isDisputed":true,"_id":"5eb8e50b3e2adb3b74e85d4f","from":{"_id":"5eb8e50a3e2adb3b74e85d43","name":"John Doe","email":"test#gmail.com","mobile":"*******999"},"amount":"40","to":"5eb8e50a3e2adb3b74e85d46"}],"count":1}
You can use string-masking to mask the fields after you fetch them.
Mongoose plugin, virtuals or getters would also involve you to iterate over the array so the end result is same.
let stringMasking = require('string-masking');
...
transactions = transactions.map(transaction => {
let mask = stringMasking(transaction.from.phone, 0);
transaction.from.phone = mask.response;
return transaction;
});
...
return res.status(200).send({transaction,count:transaction.length});
Also its better to make the password not included in all find queries if not needed. Can be done by :
password: {type: String,select: false}

Mongoose Boolean false vs undefined?

In mongoose, what is the right way to check the field value is undefined, rather than false?
We have model, which has a Boolean property, but not be initially set. For this reason the possible values are actually: undefined | true | false. Here we need to make the distinction.
The operation is being done from with a NodeJS based application.
An example, as requested:
// Simply schema
const PreferencesSchema = new mongoose.Schema({
forUser: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }
allowActionX: {
type: Boolean,
default: undefined
}
});
const Preferences = mongoose.model<any>('Preferences', PreferencesSchema);
const preferences = Preferences.findOne({ user: user });
// if preference.allowActionX is not defined:
// use default behaviour
// else if preference.allowActionX is true
// do action X
// else
// don't do action X
If you want to return a document where a field exists AND is not null, use { a : {$ne: null}}
otherwise you can also check afterword with the === operator to determine if something is false vs undefined
The triple equals, ===, in JavaScript, tests for strict equality. This means both the type and the value we are comparing have to be the same.
if (myvar === false) {
//insert code
}
else if ( myvar === undefined){
//insert code
}
https://codeburst.io/javascript-double-equals-vs-triple-equals-61d4ce5a121a

array manipulation in node js and lodash

I have two arrays
typeArr = [1010111,23342344]
infoArr={'name':'jon,'age':25}
I am expecting following
[{'name:'jone','age':25,'type':1010111,'default':'ok'},{'name:'jone','age':25,'type':23342344,'default':'nok'}]
Code :
updaterecord(infoArr,type)
{
infoArr.type=type;
response = calculate(age);
if(response)
infoArr.default = 'ok';
else
infoArr.default = 'nok';
return infoArr;
}
createRecord(infoArr,typeArr)
{
var data = _.map(typeArr, type => {
return updaterecord(infoArr,type);
});
return (data);
}
var myData = createRecord(infoArr,typeArr);
I am getting
[{'name:'jone,'age':25.'type':23342344,'default':nok},{'name:'jone,'age':25.'type':23342344,'default':nok}]
with some reason the last record updates the previous one. I have tried generating array using index var but not sure what's wrong it keep overriding the previous item.
how can I resolve this
You are passing the entire infoArr array to your updaterecord() function, but updaterecord() looks like it's expecting a single object. As a result it is adding those properties to the array rather than individual members of the array.
It's not really clear what is supposed to happen because typeArr has two elements and infoArr has one. Do you want to add another to infoArr or should infoArr have the same number of elements as typeArr.
Assuming it should have the same number you would need to use the index the _map gives you to send each item from infoArr:
function createRecord(infoArr,typeArr) {
var data = _.map(typeArr, (type, i) => {
// use infoArr[i] to send one element
return updaterecord(infoArr[i],type);
});
return (data);
}
Edit:
I'm not sure how you are calculating default since it's different in your expected output, but based on one number. To get an array of objects based on infoArray you need to copy the object and add the additional properties the you want. Object.assign() is good for this:
let typeArr = [1010111,23342344]
let infoArr={'name':'jon','age':25}
function updaterecord(infoArr,type){
var obj = Object.assign({}, infoArr)
return Object.assign(obj, {
type: type,
default: infoArr.age > 25 ? 'ok' : 'nok' //or however your figuring this out
})
}
function createRecord(infoArr,typeArr) {
return _.map(typeArr, type => updaterecord(infoArr,type));
}
Result:
[ { name: 'jon', age: 25, type: 1010111, default: 'nok' },
{ name: 'jon', age: 25, type: 23342344, default: 'nok' } ]

Resources