Mongoose find and findById inconsistency - node.js

I'm testing mongoose queries, and I found this bizarre behaviour. I have a document with _id: "5de64b376c79643fa847e86b", and if I call the findById method the document is returned just fine. But if I call the find method on the collection, giving as args an array with the same ID value than nothing is returned, while I expect an array of one element being the document. To summarize:
mongooseModel.findById(mongoose.Types.ObjectId("5de64b376c79643fa847e86b")) // Works
mongooseModel.find({ _id: { $in: [mongoose.Types.ObjectId("5de64b376c79643fa847e86b")] } }) //Doesn't work
What's the difference between the two, and why the second one doesn't work?
EDIT: This is kinda the code to access that method.
I define a DataSource in the ApolloServer configuration
const app = new ApolloServer({
...
dataSources: () => ({
source: new SourceAPI(DocumentModel)
})
...
});
where SourceAPI is the DataSource class and DocumentModel is the mongoose model.
SourceAPI is defined like this
class SourceAPI {
async get(ids) {
return await DocumentModel.find({
_id: {
$in: ids
}
});
}
}
Now, inside the GraphQL resolver I finally call the API method to get the documents, like this
const findResolver = () =>
DocumentSchema.get("$findById").wrapResolve(next => async rp => {
let ids = [];
ids.push(mongoose.Types.ObjectId(rp.args._id));
return await rp.context.dataSources.source.get(ids);
});
where DocumentSchema is the GraphQL Schema for the Document Model generated using graphql-compose-mongoose package. The get("$findById") and wrapResolve methods are also from that package. What I do is using these methods to get the GraphQL query parameters and pass them to the API method (in this case I'm just grabbing an ID for test).
If I change the API method to something like this
async get(id) {
return await DocumentModel.findById(id);
}
and the resolver method to this
const findResolver = () =>
DocumentSchema.get("$findById").wrapResolve(next => async rp => {
return await rp.context.dataSources.source.get(mongoose.Types.ObjectId(rp.args._id));
});
everything works

$in is used to find items which holds an array, but _id is just a JSON key/value pair. So you cannot use $in. If your object looks like this then you can use $in
{
id: [ObjectId("5de64b376c79643fa847e86b"),
ObjectId("5de64b376c79643fa847e86b")]
}

Related

Mongoose Model.find() methods query string not working

im trying to make simple rest api. I have a collection in mongodb and i connected my db to my app with mongoose pkg. I can access all items without query strings with Operator.find() but it doesn't work with query string ex: Operator.find({name:'Kapkan'}) it returns all of them. Also Operator.findOne({name:'Azami'}) doesn't work either. The query string returns the first element of the collection no matter what.
app.get('/api/operators',async(req,res) => {
let operators;
try{
if(req.query.name){
Operator.find({name:'Kapkan'}).then((data) => {
console.log(data);
});
}
else
operators = await Operator.find();
res.send(operators)
}catch(er){
console.log(er);
}
})
You are not assigning result of query with filter to operators. Unsure if you have {} around else or not but try refactoring the code as shown below:
app.get('/api/operators',async(req,res) => {
const filters = {};
if (req.query.name) {
filters.name = req.query.name;
}
const data = await Operator.find(filters);
console.log(data);
return res.json({ data });
})
I checked my Schema and i realize i forgotten put name field...
const OperatorSchema = new Schema({
_id:Number,
logo:String,
image:String,
unit:String,
*name:String,
side:String,
.
.
.
})

Prevent _id field from being supplied in a MongoDB query

I'm using Mongoose in NodeJS to control a MongoDB database.
I'm creating an API and for obvious security reasons, I want to prevent the auto generated document _id field from getting replaced by a manually generated one in the API request.
Schema:
{ name: String }
Creating a document:
const record = {
_id: '5e35517cc894c90327a34baf'
name: 'bob'
}
const insertRecords = async () => {
await Quiz.create(record);
};
insertRecords();
Results in the following document:
{
_id: '5e35517cc894c90327a34baf'
name: 'bob'
}
As can be seen, the _id supplied in the query, as long as it's a valid ObjectID, would replace the _id that was supposed to be auto generated by mongo.
Is there a way to check if this _id field is in the query so that I can reject the API request? The .create method triggers the pre save middleware hook which would always have the _id of the final document so I cannot depend on it to know whether the _id was in the query or it's the auto generated one.
The only option I found is to disable the _id field altogether but this does not make sense.
Solution #1 - Use .create() method with an explicit object.
It's actually easier than you think. This is self-explanatory - we only define what we want to allow. Mongoose will ignore anything that's not in the object.
const record = {
_id: '5e35517cc894c90327a34baf'
name: 'bob'
}
const insertRecords = async () => {
await Quiz.create({
name: record.name // only allow names.
});
};
insertRecords();
Solution #2 - Define a function to clear unwanted objects.
You can define a helper function to clear out unwanted fields.
const filterObj = (obj, ...allowedFields) => {
const newObject = {};
// If the current field is one of the allowed fields, keep them in the new object.
Object.keys(obj).forEach((el) => {
if (allowedFields.includes(el)) {
newObject[el] = obj[el];
}
});
return newObject;
};
How to use:
const filteredRecord = filterObj(record, 'name'); // arbitrary list of allowed fields. In this case, we'll only allow 'name'.
await Quiz.create(filteredRecord);

Mongoose query middleware/pre-find hooks, what does 'this' refer to?

I have been learning about mongoose query middleware and that when using this within a query middleware function it refers to the query object.
However, I am struggling to visualise what the query object actually is and how I can use it. For example, if I had the code:
let query = Model.findById(req.params.id);
If I were then to use query middleware:
tourSchema.pre(/^find/, function (next) {
console.log(this)
next();
});
What would this be?
As explained into docs
In query middleware functions, this refers to the query.
Also, if you do a console.log(this) you can check the object is like:
Query {
//a lot of stuffs
}
So, that is, this is the query.
Also, as another example, using update you can do this query:
model.update({},{$set:{field:"value"}})
And check this hook:
MongooseModel.pre('update', async function () {
const query = this.getUpdate();
console.log(query) // { '$set': { field: 'value' } }
const field = query.$set.field;
console.log(field) // value
});

Mongoose - remove _id from lean ( using toJSON() ?)

I'm having a NODE.JS project using mongoose 5.x
My model have toJSON method which removes the _id & __v fields perfectly
mySchema.method("toJSON", function toJSON() {
const {__v, _id, ...object} = this.toObject();
return {
id: _id,
...object
};
});
so when fetching data from the db:
const data = myModel.findOne({_id: id});
I get an object that when serialized to the user:
res.json(data);
It doesn't contain the _id and __v fields as required.
The problem is when I use lean():
const data = myModel.findOne({_id: id}).lean();
the data object contains those fields.
I can remove them manually when using lean
but I would prefer to find a way to sanitize the data object in both cases with the same mechanism.
any suggestions?
Thanks in advance.
Not sure if this is what you want but maybe:
const data = myModel.findOne({_id: id}).lean().then(res => {
delete res._id
return res
})
When JSON.stringify() is called on an object, a check if done if it has a property called toJSON. It's not specific to Mongoose, as it works on plain objects:
const myObj = {
_id: 123,
foo: 'bar',
// `toJSON() {...}` is short for `toJSON: function toJSON() {...}`
toJSON() {
// Because "this" is used, you can't use an arrow function.
delete this._id;
return this;
}
};
console.log(JSON.stringify(myObj));
// {"foo":"bar"}
Mongoose doesn't have an option to automatically inject a toJSON function into objects returned by lean(). But that's something you can add.
First, create a function that:
Takes an object with properties
Listens to when Mongoose runs a find query
Tells Mongoose that after the query, it should change the result
The change: merge the result with the object from step 1.
function mergeWithLeanObjects(props) {
// Return a function that takes your schema.
return function(schema) {
// Before these methods are run on the schema, execute a function.
schema.pre(['find', 'findOne'], function() {
// Check for {lean:true}
if (this._mongooseOptions.lean) {
// Changes the document(s) that will be returned by the query.
this.transform(function(res) {
// [].concat(res) makes sure its an array.
[].concat(res).forEach(obj => Object.assign(obj, props));
return res;
});
}
});
};
}
Now add it to your schema:
mySchema = new mongoose.Schema({
foo: String
});
mySchema.plugin(mergeWithLeanObjects({
toJSON() {
delete this._id;
delete this.__v;
return this;
}
}));
Then test it:
const full = await myModel.findOne();
const lean = await myModel.findOne().lean();
console.log(full);
// Logs a Mongoose document, all properties.
{ _id: new ObjectId("62a8b39466768658e7333154"), foo: 'bar', __v: 1 }
console.log(JSON.stringify(full));
// Logs a JSON string, all properties.
{"_id":"62a8b39466768658e7333154","foo":"bar","__v":1}
console.log(lean);
// Logs an Object, all properties.
{ _id: new ObjectId("62a8b39466768658e7333154"),
foo: 'bar', __v: 1, toJSON: [Function: toJSON] }
console.log(JSON.stringify(lean));
// Logs a JSON string, filtered properties.
{"foo":"bar"}
If you want to re-use the plugin with the same settings on multiple schemas, just save the function that mergeWithLeanObjects returns somewhere.
// file: clean.js
module.exports = mergeWithLeanObjects({
toJSON() {
delete this._id;
delete this.__v;
return this;
}
});
// file: some-schema.js
schema1.plugin(require('./clean.js'));
// file: some-other-schema.js
schema2.plugin(require('./clean.js'));
There's also mongoose.plugin() to add the function to all schemas.
Try this to retrieve the _id from a document
myModel.findOne({_id: id}, function(err, doc) {
if(err)
return 'do something with this err'
console.log(doc._id)
})

Implementing pagination with Mongoose and graphql-yoga

I am experimenting with graphql and have created a simple server using graphql-yoga. My Mongoose product model queries my database and both resolvers return data as expected. So far it all works and I am very happy with how easy that was. However, I have one problem. I am trying to add a way to paginate the results from graphQL.
What did I try?
1) Adding a limit parameter to the Query type.
2) Accessing the parameter through args in the resolver
Expected behaviour
I can use the args.limit parameter in my resolver and use it to alter the Mongoose function
Actual behaviour
I can't read the arg object.
Full code below. How do I reach this goal?
import { GraphQLServer } from 'graphql-yoga'
import mongoose from "mongoose"
import {products} from "./models/products.js"
const connection = mongoose.connect('mongodb://myDB')
const prepare = (o) => {
o._id = o._id.toString()
return o
}
const typeDefs = `
type Product {
_id: String
name: String
description: String
main_image: String
images: [String]
}
type Query {
product(_id: String): Product
products(limit: Int): [Product]
}
`
const resolvers = {
Query: {
product: async (_id) => {
return (await products.findOne(_id))
},
products: async (args) => {
console.log(args.name)
return (await products.find({}).limit(args.limit))
},
},
}
const server = new GraphQLServer({
typeDefs,
resolvers
})
server.start(() => console.log('Server is running on localhost:4000'))
The arguments for a field are the second parameter passed to the resolver; the first parameter is the value the parent field resolved to (or the root value in the case of queries/mutations). So your resolvers should look more like this:
product: (root, { _id }) => {
return products.findOne(_id)
}

Resources