8

I have a simple REST API and I would like to implement filtering on a specific endpoint.

Imagine I have an endpoint like this:

localhost:5000/api/items?color="red"

Which handles request like this:

const items = await Items.find({color: req.query.color})

This works when the color parameter is present. However, if the color parameter is omitted then the query searches for items where the color is undefined. This is not the behaviour I need.

In my case, I would like to add multiple filter parameters which are ignored if they are not present. Do I have to create a separate query for each case or is there an option to tell Mongoose not to search for a field (in this case color) if it is null or undefined?

Danny
  • 81
  • 7
JesterWest
  • 499
  • 1
  • 5
  • 13

7 Answers7

14

I just hit the same issue, if you are using ES6 supported node version, you should be able to use spread. it automatically takes care of the undefined

var filters = {
     colour: "red",
     size: undefined
}


Items.find({...filters}) 
// only colour is defined, so it becomes
Items.find({colour:"red"})

NOTE if you are using Babel. you might need to define a object first.

var query = {...filters}
Items.find(query)
devric
  • 3,555
  • 4
  • 22
  • 36
13

You can unpack the variables in req.query using a 'destructuring assignment'.

const { color } = req.query;
if(color) {
   const items = await Items.find({ color })
}

If you have multiple filters, you can use the variables from above. For example, you may have color and type parameters. With this, you can build up an object to pass to the find method.

const { color, type } = req.query;
let query = {};
if(color) {
   query.color = color;
}
if(type) {
   query.type = type;
}
const items = await Items.find(query);

If color or type is not in the original query, they will be undefined or falsy and so will skip the if statement for that parameter. Hope it works!

Danny
  • 81
  • 7
dnp1204
  • 471
  • 5
  • 14
  • This is what I hoped to avoid because if I have 5+ parameters I'll need to make a query for each possibility, which would be 32. – JesterWest Mar 22 '18 at 19:41
  • 1
    I have just made the update. Do you think this following solution will work for you? – dnp1204 Mar 22 '18 at 19:51
5

I'm also got this issue. this is my solution:

  • add the ignoreUndefined in the connect settings

  • I'm using nestjs

    MongooseModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: async (configService: ConfigService) => ({
        uri: configService.get<string>('MONGO_URI'),
        useCreateIndex: true,
        useNewUrlParser: true,
        useFindAndModify: false,
        useUnifiedTopology: true,
        ignoreUndefined: true, // add this to the option
        connectionFactory: (connection) => {
          connection.plugin(require('mongoose-autopopulate'));
          return connection;
        },
      })
  • this is the query log:
    Mongoose: entry.find({ field: undefined })
  • then I got all the entrys
Dharman
  • 30,962
  • 25
  • 85
  • 135
scott
  • 71
  • 1
  • 3
3

As stated by francisct, you can achieve this without performing any manual checks by using ignoreUndefined in connection options:

const mongoose = require('mongoose');
mongoose.connect(mongoDb, { ignoreUndefined: true });

Alternatively, you can use reduce to explicitly specify the options that you're looking for in the query as an array and construct the filter object with only the fields that are not undefined:

const query = { color: 'red', price: 2, hello: 'world' };
const fields = ['color', 'price', 'size'];

function getFilter(query, fields) {
  return fields.reduce((filter, field) => {

    if (query[field] !== undefined)
      return {
        [field]: query[field],
        ...filter,
      };

    return filter;
  }, {});
}

const filter = getFilter(query, fields); // { price: 2, color: 'red' }

In this case the function doesn't add the "hello" field to the object because "hello" is not in the array. It also ignores "size" because it's not in the query. You can use the returned object like this:

const items = await Items.find(filter);

You can shorten this function using implicit return and ternary operator:

const getFilter = (query, fields) => fields.reduce(
  (filter, field) => query[field] !== undefined ? {
    [field]: query[field],
    ...filter,
  } : filter, {}
);

Cool albeit arguably less readable.

tomek-ch
  • 117
  • 2
  • 3
2

You may remove undefined keys from object before passing that object.

Object.keys(filter).forEach(key => filter[key] === undefined && delete filter[key])

Put the above code inside util and reuse it everywhere.

Nishant Shah
  • 2,022
  • 22
  • 20
1

You can use ignoreUndefined in your connection options when connecting to MongoDB so that in your queries all undefined keys are skipped when they are serialized to BJSON. You can see the options on this page

francisct
  • 11
  • 1
0
const query = Object.fromEntries(Object.entries(req.query).filter(([_,value]) => !!value))

Items.find({...query})

This will create your a useful query object and also help in the situation where parameter is an empty string