Pass API Filters to MongoDB Query - javascript

I have an node-express endpoint that accepts several url parameters as filters. For example
{{baseUrl}}/projects?page=1&name=myProject
The request is then translated to a mongoDB query.
Model:
const projectSchema = mongoose.Schema({
name: {
type: String,
required: true,
},
users: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }],
});
const Project = mongoose.model('Project', projectSchema);
const filter = pick(req.query, ['name']);
const options = pick(req.query, ['sortBy', 'limit', 'page']);
options.populate = 'users';
const result = await projectService.queryProjects(filter, options, req.user);
This works fine, but when i use mongoDB filter expressions like "/MyProject/" the filter is passed as a string and not as an expression.
What do i need to change to get the expected result?
#current result
console.log(filter);
#Output: { name: '/MyProject/' }
#expected result
const test = { name: /MyProject/ };
console.log(test);
#Output { name: /MyProject/ }

/MyProject/ (without quotes is a regular expression):
> typeof "/MyProject/"
'string'
> typeof /MyProject/
'object'
> /MyProject/ instanceof String
false
> /MyProject/ instanceof RegExp
true
Now if you want to construct a RegEx from a string you may want to consider using RegEx constructor:
new RegExp("MyProject")
/MyProject/
Or, you can read your filter as a string from your query parameters then use the other way of passing filters to MongoDB with $regex:
const filter = pick(req.query, ['name']);
const result = await projectService.queryProjects({"name": {"$regex": filter, "$options": "i"}, options, req.user);
WARNING
Either way please be careful when using such approaches as they may lead to ReDOS and/or NoSQL injection.

Related

Mongodb Searching in an array of ids using include does not work

I have this model:
const NeighborSchema = new Schema({
friends: [
{
type: Schema.Types.ObjectId,
ref: "users",
},
],
date: {
type: Date,
default: Date.now,
},
});
module.exports = Neighbor = mongoose.model("neighbor", NeighborSchema);
I am trying to see if a friend exists in friends of all neighbors:
const mongoose = require("mongoose");
const ObjectId = mongoose.Types.ObjectId;
const testIncludes = async () => {
let neighbors = await Neighbor.find();
let friends_ids = [];
neighbors.map((neighbor) => {
const { friends } = neighbor;
friends_ids = [...friends_ids, ...friends];
});
// Returns false for this
const element_to_search = ObjectId("60dcbb29118ea36a4f3ce229");
// Returns false for this
// const element_to_search = "60dcbb29118ea36a4f3ce229";
let is_element_found = friends_ids.includes(element_to_search);
};
// Returns false in both cases
testIncludes();
Even though, element_to_search was taken directly from list of returned friends_ids array, when I try to search it using include, it returns false for some reason, whether I search it as a String or as an ObjectId.
Any idea what's going on?
Array.prototype.includes compares each element against the sample until it finds a match. Objects are considered equal only if they reference the same instance of the class. When you call a constructor const element_to_search = ObjectId("60dcbb29118ea36a4f3ce229"); it creates a new instance which has never been in the array, even if its value is the same.
You need to compare scalars. Strings for example:
friends_ids.map(f => f.toString()).includes("60dcbb29118ea36a4f3ce229");
or cast it strings when you build up the friends_ids at the first place to avoid the extra loop over the array.

Remove all elements in string array Mongoose schema

I'm trying to remove all of strings that match occurrences in the array of 'interestedStudents' in a Mongoose schema.
My Mongoose schema looks like this:
// Create a schema.
const schema = new mongoose.Schema<Post>({
interestedStudents: {
type: [{
type: String,
unique: true
}],
required: false,
},
})
//Create model
export const PostModel = model<Post>('Post', schema);
I'm trying to remove by using:
await PostModel.updateMany({ interestedStudents: { $pullAll : [userId]}})
But I'm getting the following error:
"CastError: Cast to [string] failed for value "[ { '$pullAll': [ '62854109cf9a6db1fcf0393b' ] } ]" (type string) at path "interestedStudents.0" because of "CastError"\n at model.Query.exec
What am I doing wrong? Is my Schema set up wrong? Maybe it's not an array of string?
It was as easy as this for anyone else coming here:
const { modifiedCount } = await PostModel.updateMany({}, { $pull: { interestedStudents: userId } })

Sequelize Where clause with optional params

How do I write Sequelize queries with optional params?
Let's say, I have a following query:
const result : SomeModel[] = await SomeModel.findAll(
{where:
{
id: givenId,
type: someType
}
});
How do I rephrase the same query in case someType is null?
If someType is null, then it should be removed from the where clause and results should be return only on basis of 'id'.
How about defining the where clause outside...
let where = { id: givenId };
if(someType)
where.type = someType;
...and using it in your query like so
await SomeModel.findAll({where});

SimpleSchema match any type but null

I'm planning to make a collection to hold different app-wide settings, like, say, amount of logged in users today, Google analytics tracking ID, etc. So I made a schema like this:
options_schema = new SimpleSchema({
key: {
type: String,
unique: true
},
value: {
},
modified: {
type: Date
}
});
Now the main problem is that I want value to be of any type: Number, String, Date, or even custom Objects. Though it has to be present, can't be null.
But of course it gets angry about not specifying the type. Is there a workaround for this?
You can use Match patterns for your fields' type which allow you to do pretty much anything :
const notNullPattern = Match.Where(val => val !== null)
value : {
type : notNullPattern
}
(See Arrow functions)
Note that this will allow everything but null, including undefined.
Defining patterns this way allow you to use them everywhere in your application including in check :
check({
key : 'the key',
modified : Date.now(),
value : {} // or [], 42, false, 'hello ground', ...
}, optionsSchema)
Match.test(undefined, notNullPattern) //true
Match.test({}, notNullPattern) //true
Match.test(null, notNullPattern) //false
A more general solution to exclude one value would simply be:
const notValuePattern =
unwantedValue => Match.Where(val => val !== unwantedValue))
The use of which is similar to the above:
Match.test(42, notValuePattern(null)) // true
Note that due to the use of the identity operator === it will notably fail for NaN:
Match.test(NaN, notValuePattern(NaN)) // true :(
A solution could be:
const notValuePattern =
unwantedValue => Match.Where(val => Number.isNaN(unwantedValue)?
!Number.isNaN(val)
: val !== unwantedValue
)
Should you want a solution to exclude some specific values in a schema (kind of the contrary of Match.OneOf), you could use the following:
const notOneOfPattern = (...unwantedValues) =>
Match.Where(val => !unwantedValues.includes(val)
)
This uses Array.prototype.includes and the ... spread operator. Use as follow:
Match.test(42, notOneOfPattern('self-conscious whale', 43)) // true
Match.test('tuna', notOneOfPattern('tyranny', 'tuna')) // false
Match.test('evil', notOneOfPattern('Plop', 'kittens')) // true
const disallowedValues = ['coffee', 'unicorns', 'bug-free software']
Match.test('bad thing', notOneOfPattern(...disallowedValues)) // true

Why can't I delete a mongoose model's object properties?

When a user registers with my API they are returned a user object. Before returning the object I remove the hashed password and salt properties. I have to use
user.salt = undefined;
user.pass = undefined;
Because when I try
delete user.salt;
delete user.pass;
the object properties still exist and are returned.
Why is that?
To use delete you would need to convert the model document into a plain JavaScript object by calling toObject so that you can freely manipulate it:
user = user.toObject();
delete user.salt;
delete user.pass;
Non-configurable properties cannot be re-configured or deleted.
You should use strict mode so you get in-your-face errors instead of silent failures:
(function() {
"use strict";
var o = {};
Object.defineProperty(o, "key", {
value: "value",
configurable: false,
writable: true,
enumerable: true
});
delete o.key;
})()
// TypeError: Cannot delete property 'key' of #<Object>
Another solution aside from calling toObject is to access the _doc directly from the mongoose object and use ES6 spread operator to remove unwanted properties as such:
user = { ...user._doc, salt: undefined, pass: undefined }
Rather than converting to a JavaScript object with toObject(), it might be more ideal to instead choose which properties you want to exclude via the Query.prototype.select() function.
For example, if your User schema looked something like this:
const userSchema = new mongoose.Schema({
email: {
type: String,
required: true,
},
name: {
type: String,
required: true
},
pass: {
type: String,
required: true
},
salt: {
type: String,
required: true
}
});
module.exports = {
User: mongoose.model("user", userSchema)
};
Then if you wanted to exclude the pass and salt properties in a response containing an array of all users, you could do so by specifically choosing which properties to ignore by prepending a minus sign before the property name:
users.get("/", async (req, res) => {
try {
const result = await User
.find({})
.select("-pass -salt");
return res
.status(200)
.send(result);
}
catch (error) {
console.error(error);
}
});
Alternatively, if you have more properties to exclude than include, you can specifically choose which properties to add instead of which properties to remove:
const result = await User
.find({})
.select("email name");
The delete operation could be used on javascript objects only. Mongoose models are not javascript objects. So convert it into a javascript object and delete the property.
The code should look like this:
const modelJsObject = model.toObject();
delete modlelJsObject.property;
But that causes problems while saving the object. So what I did was just to set the property value to undefined.
model.property = undefined;
Old question, but I'm throwing my 2-cents into the fray....
You question has already been answered correctly by others, this is just a demo of how I worked around it.
I used Object.entries() + Array.reduce() to solve it. Here's my take:
// define dis-allowed keys and values
const disAllowedKeys = ['_id','__v','password'];
const disAllowedValues = [null, undefined, ''];
// our object, maybe a Mongoose model, or some API response
const someObject = {
_id: 132456789,
password: '$1$O3JMY.Tw$AdLnLjQ/5jXF9.MTp3gHv/',
name: 'John Edward',
age: 29,
favoriteFood: null
};
// use reduce to create a new object with everything EXCEPT our dis-allowed keys and values!
const withOnlyGoodValues = Object.entries(someObject).reduce((ourNewObject, pair) => {
const key = pair[0];
const value = pair[1];
if (
disAllowedKeys.includes(key) === false &&
disAllowedValues.includes(value) === false
){
ourNewObject[key] = value;
}
return ourNewObject;
}, {});
// what we get back...
// {
// name: 'John Edward',
// age: 29
// }
// do something with the new object!
server.sendToClient(withOnlyGoodValues);
This can be cleaned up more once you understand how it works, especially with some fancy ES6 syntax. I intentionally tried to make it extra-readable, for the sake of the demo.
Read docs on how Object.entries() works: MDN - Object.entries()
Read docs on how Array.reduce() works: MDN - Array.reduce()
I use this little function just before i return the user object.
Of course i have to remember to add the new key i wish to remove but it works well for me
const protect = (o) => {
const removes = ['__v', '_id', 'salt', 'password', 'hash'];
m = o.toObject();
removes.forEach(element => {
try{
delete m[element]
}
catch(O_o){}
});
return m
}
and i use it as I said, just before i return the user.
return res.json({ success: true, user: await protect(user) });
Alternativly, it could be more dynamic when used this way:
const protect = (o, removes) => {
m = o.toObject();
removes.forEach(element => {
try{
delete m[element]
}
catch(O_o){}
});
return m
}
return res.json({ success: true, user: await protect(user, ['salt','hash']) });

Categories

Resources