Search code examples
mongodbmeteor

MongoError: can’t convert from BSON type string to Date in Meteor


In Meteor JS I want to find users whose birthday is today.I have this piece of code that runs fine on my computer (locally) but it fails in production :

let today = new Date()
let users = Meteor.users.find({
    "status.lastLogin": { $not: { $eq: null } },
    $expr: {
        $and: [
            {
                $eq: [
                    {
                        $dayOfMonth: {
                            date: "$profile.birthdate",
                            timezone: "Europe/Paris",
                        },
                    },
                    today.getDate(),
                ],
            },
            {
                $eq: [
                    {
                        $month: {
                            date: "$profile.birthdate",
                            timezone: "Europe/Paris",
                        },
                    },
                    today.getMonth() + 1,
                ],
            },
        ],
    },
})

My server is hosted on Galaxy and the DB on mongodb.com I checked the profile.birthdate type and it is Date on mongodb.com The error is :

MongoError: can’t convert from BSON type string to Date\n at Connection. (/app/bundle/programs/server/npm/node_modules/meteor/npm-mongo/node_modules/mongodb/lib/core/connection/pool.js:450:61)\n at Connection.emit (events.js:311:20)\n at Connection.EventEmitter.emit (domain.js:482:12)\n at processMessage (/app/bundle/programs/server/npm/node_modules/meteor/npm-mongo/node_modules/mongodb/lib/core/connection/connection.js:384:10)\n at TLSSocket.

Does anyone know why this is happening and how can I fix it?

Edit: By following @Jankapunkt advice to use aggregate and by reading this post, I was able to write a better (I think...) query and now it is working. This is the new code:

const today = new Date()
let users = Meteor.users.aggregate(
    {
        $project: {
            status: "$status",
            roles: "$roles",
            month: {
                $month: {
                    date: "$profile.birthdate",
                    timezone: "Europe/Paris",
                },
            },
            day: {
                $dayOfMonth: {
                    date: "$profile.birthdate",
                    timezone: "Europe/Paris",
                },
            },
        },
    },
    {
        $match: {
            "status.lastLogin": { $ne: null },
            roles: "candidate",
            month: today.getMonth() + 1,
            day: today.getDate(),
        },
    }
)

Solution

  • There are several issues here, I'd like to address:

    • $expr is usually only required in rare cases or when matching against a regular expression.

    • $dayOfMonth is an aggregate operator and not available in basic queries, but there are packages available

    • Meteor has builtin Date support through EJSON, which extends BSON by custom types (it abstracts the type conversion away for you):

    Meteor.publish('allbirthdays', function () {
      const today = new Date()
      // correct timezone here
      return Meteor.users.find({ '$profile.birthdate': today })
    }
    

    No need to convert Date to some mongo operators etc.

    • $and is a contradiction if differnt values are both required for the same field (birthdate can never be today and today in a month), did you intend to use $or?

    • { $not: { $eq: null } } can be written as { $ne: null }

    • Always disable the services field if you publish users! Services contains the (hashed) password and other oauth providers, including resume token, which could lead to serious security issues:

    Saving / querying Dates without aggregate

    The above methods allow only an exact Date matches, because MongoDB provides Date-specific query only through aggregate.

    Therefore, your options are:

    • A) Use the aggregate package to build $expr for $month and $dayOfMonth as in your example code

    • B) Create the birthday only as locale field (which makes it a String type):

    export const getDate = () => {
      // todo apply locale, timezone etc.
      return new Date().toLocaleString(undefined, {
        day: 'numeric', month: 'long' 
      })
    }
    

    and save it in the user's collection as a separate field (e.g. birthDay):

    Meteor.users.update(userId, {
      $set: {
        '$profile.birthDay': getDate() // "June 3"
      }
    })
    

    ans query only for this day:

    Meteor.publish('allbirthdays', function () {
      const today = getDate()
      return Meteor.users.find({ '$profile.birthDay': today })
    }
    
    • C) Save the month and day as Number types in separate fields:
    const today = new Date()
    Meteor.users.update(userId, {
      $set: {
        '$profile.birthDay': today.getDate()  // 3
        '$profile.birthMon': today.getMonth() // 6
      }
    })
    

    ans query only for this day:

    Meteor.publish('allbirthdays', function () {
      const today = new Date()
      return Meteor.users.find({
        '$profile.birthDay': today.getDate()
        '$profile.birthMon': today.getMonth()
      })
    })