Automattic / mongoose

MongoDB object modeling designed to work in an asynchronous environment.
https://mongoosejs.com
MIT License
26.87k stars 3.83k forks source link

$switch in $expr in filter query failed: Cannot read properties of undefined (reading 'map') #14751

Closed eherve closed 1 month ago

eherve commented 1 month ago

Prerequisites

Mongoose version

7.6.3

Node.js version

20.11.1

MongoDB server version

7

Typescript version (if applicable)

5.1

Description

when using $switch in filter in query, mongoose throw a type error:

TypeError: Cannot read properties of undefined (reading 'map')
        at _castExpression (node_modules/mongoose/lib/helpers/query/cast$expr.js:96:18)
        at castComparison (node_modules/mongoose/lib/helpers/query/cast$expr.js:213:12)
        at _castExpression (node_modules/mongoose/lib/helpers/query/cast$expr.js:105:18)
        at node_modules/mongoose/lib/helpers/query/cast$expr.js:103:36
        at Array.map (<anonymous>)
        at _castExpression (node_modules/mongoose/lib/helpers/query/cast$expr.js:103:27)
        at cast$expr (node_modules/mongoose/lib/helpers/query/cast$expr.js:75:10)
        at cast (node_modules/mongoose/lib/cast.js:92:13)
        at model.Query.Query.cast (node_modules/mongoose/lib/query.js:4909:12)
        at model.Query.Query._castConditions (node_modules/mongoose/lib/query.js:2232:10)

in the cast$expr.js file, when the presence of $switch is checked, there is an issue with the branches and default check :

  else if (val.$switch != null) {
    val.branches.map(v => _castExpression(v, schema, strictQuery));
    val.default = _castExpression(val.default, schema, strictQuery);
  }

val.$switch is defined but next lines the values used are val.branches and val.default and they are undefined. Te values should be val.$switch.branches and val.default

Steps to Reproduce

model.find({
  $expr: {
    $eq: [
      {
        $switch: {
          branches: [{case: {$eq: ['$$NOW', '$$NOW']}, then: true}],
          default: false,
        },
      },
      true,
    ],
  },
});

throw the error, but using te collection it does not

model.collection.find({
  $expr: {
    $eq: [
      {
        $switch: {
          branches: [{case: {$eq: ['$$NOW', '$$NOW']}, then: true}],
          default: false,
        },
      },
      true,
    ],
  },
}).toArray();

Expected Behavior

Should work as it does when using the collection instead of te mongoose model

eherve commented 1 month ago

fix:

} else if (val.$switch != null) {
    val.$switch.branches.map(v => _castExpression(v.case, schema, strictQuery));
    val.$switch.default = _castExpression(val.$switch.default, schema, strictQuery);
}
IslandRhythms commented 1 month ago
const mongoose = require('mongoose');

const testSchema = new mongoose.Schema({
    name: String,
    scores: [Number]
});

const Test = mongoose.model('Test', testSchema);

async function run() {
    await mongoose.connect('mongodb://localhost:27017');
    await mongoose.connection.dropDatabase();

    await Test.create({
        name: 'Test',
        scores: [87, 86, 78]
    });

    await Test.create({
        name: 'Test',
        scores: [71,64,81]
    });

    await Test.create({
        name: 'Test',
        scores: [91,84,97]
    });

    // docs run
    const test = await Test.aggregate( [
        {
          $project:
            {
              "name" : 1,
              "summary" :
              {
                $switch:
                  {
                    branches: [
                      {
                        case: { $gte : [ { $avg : "$scores" }, 90 ] },
                        then: "Doing great!"
                      },
                      {
                        case: { $and : [ { $gte : [ { $avg : "$scores" }, 80 ] },
                                         { $lt : [ { $avg : "$scores" }, 90 ] } ] },
                        then: "Doing pretty well."
                      },
                      {
                        case: { $lt : [ { $avg : "$scores" }, 80 ] },
                        then: "Needs improvement."
                      }
                    ],
                    default: "No scores found."
                  }
               }
            }
         }
      ] )

    console.log('what is test', test);

    const response = await Test.collection.find({
        $expr: {
            $eq: [
              {
                $switch: {
                  branches: [{case: {$eq: ['$$NOW', '$$NOW']}, then: true}],
                  default: 'Hello There',
                },
              },
              true,
            ],
          },
    }).toArray();

    console.log('what is response', response);

    const simplified = await Test.find({
        $expr: {
          $eq: ['$$NOW', '$$NOW']
        }
      });

      console.log('what is simplifed', simplified);

    // this also throws
    const cleaned = await Test.find({
        $expr: {
          $switch: {
            branches: [
              { case: { $eq: ['$$NOW', '$$NOW'] }, then: true }
            ],
            default: false
          }
        }
      });

      console.log('what is cleaned', cleaned);

    // this throws
    const res = await Test.find({
        $expr: {
            $eq: [
              {
                $switch: {
                  branches: [{case: {$eq: ['$$NOW', '$$NOW']}, then: true}],
                  default: false,
                },
              },
              true,
            ],
          },
    });

    console.log('what is res', res);
}

run();