Search code examples
node.jsmongodbmongoosegeojsonmultilinestring

Mongoose 5.3.8 - Can't Extract Geo Keys with MultiLineString and $push


I am attempting to $push a coordinate pair into a document that contains a GeoJSON MultiLineString that is nested like this: [[[Number]]]

When I findOneAndUpdate like so:

req.body.point = [-123.347, 48.43649]

Route.findOneAndUpdate({name: req.body.name},
{'$push': { "route.coordinates.0": req.body.point}}, {new: true})
.then(doc => {
    return res.send(doc)
}).catch(err => {
    res.send(err)
})

I receive the error message:

errmsg": "Can't extract geo keys: { _id: ObjectId('5be9eef393311d2d2c6130fd').......
Point must only contain numeric elements",
"code": 16755,

My coordinates are valid [long, lat] format as mongo expects. is there something I'm missing here?


This is my MultiLineString Schema:

const MultiLineStringSchema = new Schema({
  type: {
    type: String,
    enum: ['MultiLineString'],
    required: true,
    default: 'MultiLineString'
  },
  coordinates: {
    type: [[[Number]]],
    required: true
  }
});

This is my route schema:

var { MultiLineStringSchema } = require('./GeoJson-Schemas')

const RouteSchema = new mongoose.Schema({
  name: String,
  route: {
    type: MultiLineStringSchema,
    required: true
  }
});
RouteSchema.index({ route: "2dsphere" });

EDIT 2: Here is the Route document stored that persisted the error. I have re-created the error with this document and have updated the matching _id in the error message above.

{
    "_id": {
        "$oid": "5be9eef393311d2d2c6130fd"
    },
    "name": "A Route",
    "route": {
        "type": "MultiLineString",
        "coordinates": [
            [
                [
                    -123.3867645,
                    48.4813423
                ],
                [
                    -123.3785248,
                    48.4592626
                ],
                [
                    -123.3766365,
                    48.4527165
                ],
                [
                    -123.3756924,
                    48.4523749
                ],
                [
                    -123.3722591,
                    48.4549366
                ],
                [
                    -123.3704567,
                    48.4559612
                ]
            ]
        ],
        "_id": {
            "$oid": "5be9eef393311d2d2c6130fe"
        }
    },
    "__v": 0
}

Moving ahead one step I have added the {_id: false} option to the MultiLineString subdocument and reinitialized a fresh collection. Upon storing the new document like so:

{
    "_id": "5be9f076e1caaa23682d80de",
    "name": "A Route",
    "route": {
        "type": "MultiLineString",
        "coordinates": [
            [
                [
                    -123.3867645,
                    48.4813423
                ],
                [
                    -123.3785248,
                    48.4592626
                ],
                [
                    -123.3766365,
                    48.4527165
                ],
                [
                    -123.3756924,
                    48.4523749
                ],
                [
                    -123.3722591,
                    48.4549366
                ],
                [
                    -123.3704567,
                    48.4559612
                ]
            ]
        ]
    },
    "__v": 0
}

I attempt to findOneAndUpdate with the exact syntax:

Route.findOneAndUpdate({name},
    {'$push': { "route.coordinates.0": point}}, {new: true})
    .then(doc => {
        return res.send(doc)
    }).catch(err => {
        res.send(err)
    })

And recive the same error:

"errmsg": "Can't extract geo keys: { _id: ObjectId('5be9f076e1caaa23682d80de')...
Point must only contain numeric elements",
"code": 16755,

EDIT 3:

Moving ahead once more I have updated the RouteSchema removing the subdocument entirely to be like so:

const RouteSchema = new mongoose.Schema({
  name: String,
  route: {
    type: {
      type: String,
      enum: ['MultiLineString'],
      required: true,
      default: 'MultiLineString'
    },
    coordinates: {
      type: [[[Number]]],
      required: true
    }
  }
});

I store the document like so:

{
    "_id": {
        "$oid": "5be9f3915fc64e3548603766"
    },
    "route": {
        "type": "MultiLineString",
        "coordinates": [
            [
                [
                    -123.3867645,
                    48.4813423
                ],
                [
                    -123.3785248,
                    48.4592626
                ],
                [
                    -123.3766365,
                    48.4527165
                ],
                [
                    -123.3756924,
                    48.4523749
                ],
                [
                    -123.3722591,
                    48.4549366
                ],
                [
                    -123.3704567,
                    48.4559612
                ]
            ]
        ]
    },
    "name": "A Route",
    "__v": 0
}

and FindOneAndUpdate again with the exact syntax, and receive the same error.

Route.findOneAndUpdate({name},
    {'$push': { "route.coordinates.0": point}}, {new: true})
    .then(doc => {
        return res.send(doc)
    }).catch(err => {
        res.send(err)
    })


"errmsg": "Can't extract geo keys: { _id: ObjectId('5be9f3915fc64e3548603766')
Point must only contain numeric elements",
"code": 16755,

EDIT 4:

mLab hosted instance mongo version 3.6.6 indexes on Route collection PHOTO

Running db.routes.getIndexes() ON shell connection provides the following.

 [
            {
                    "v" : 2,
                    "key" : {
                            "_id" : 1
                    },
                    "name" : "_id_",
                    "ns" : "testbench.routes"
            },
            {
                    "v" : 2,
                    "key" : {
                            "route" : "2dsphere"
                    },
                    "name" : "route_2dsphere",
                    "ns" : "testbench.routes",
                    "background" : true,
                    "2dsphereIndexVersion" : 3
            }
    ]

Solution

  • So the issue is indeed reproducible and it comes down to the schema definition. Right here:

    coordinates: {
      type: [[[Number]]],
      required: true
    }
    

    This is where you are diligently specifying the nested array "rings" required for the MultiLineString format of the document. It might seem correct and inserting new documents is not a problem, but pay attention of the notation MongoDB want's here:

    { $push: { 'route.coordinates.0': [-123.3701134, 48.4467389] } }
    

    So the "schema" has a "double array nest" as in [[[Number]]] but MongoDB wants just coordinates.0 and not coordinates.0.0 in order to be happy. This is where mongoose gets confused and "rewrites" the update statement:

    { '$push': { 'route.coordinates.0': [ [ -123.3701134 ], [ 48.4467389 ] ] } }
    

    So that's just wrong, and is the source of the error:

    errmsg: 'Can\'t extract geo keys: { _id: ObjectId(\'5be9f3915fc64e3548603766\'), route: { type: "MultiLineString", coordinates: [ [ [ -123.3867645, 48.4813423 ], [ -123.3785248, 48.4592626 ], [ -123.3766365, 48.4527165 ], [ -123.3756924, 48.4523749 ], [ -123.3722591, 48.4549366 ], [ -123.3704567, 48.4559612 ], [ [ -123.3701134 ], [ 48.4467389 ] ] ] ] }, name: "A Route", __v: 0 } Point must only contain numeric elements',

    Altering the schema to not be "so specific" stops mongoose from "mangling" the statement:

    coordinates: {
      type: []   // Just expect an array without any other definition
      required: true
    }
    

    Then the update proceeds as expected.


    For a full reproducible listing:

    const { Schema, Types: { ObjectId } } = mongoose = require('mongoose');
    
    // Sample connection
    const uri = 'mongodb://localhost:27017/geostring';
    const opts = { useNewUrlParser: true };
    
    // Sensible defaults
    mongoose.Promise = global.Promise;
    mongoose.set('useFindAndModify', false);
    mongoose.set('useCreateIndex', true);
    mongoose.set('debug', true);
    
    // Schema defs
    
    
    const routeSchema = new Schema({
      name: String,
      route: {
        type: {
          type: String,
          enum: ['MultiLineString'],
          required: true,
          default: 'MultiLineString'
        },
        coordinates: {
          type: [],
          // type: [[[Number]]], // this has a problem
          required: true
        }
      }
    });
    
    routeSchema.index({ route: '2dsphere' });
    
    const Route = mongoose.model('Route', routeSchema);
    
    // log helper
    const log = data => console.log(JSON.stringify(data, undefined, 2));
    
    // Main
    (async function() {
    
      try {
        const conn = await mongoose.connect(uri, opts);
    
        // clean data from all models
        await Promise.all(
          Object.entries(conn.models).map(([k,m]) => m.deleteMany())
        );
    
        // Insert data
    
        await Route.create({
          "_id": new ObjectId("5be9f3915fc64e3548603766"),
          "name": "A Route",
          "route": {
            "type": "MultiLineString",
            "coordinates": [
              [
                [
                  -123.3867645,
                  48.4813423
                ],
                [
                  -123.3785248,
                  48.4592626
                ],
                [
                  -123.3766365,
                  48.4527165
                ],
                [
                  -123.3756924,
                  48.4523749
                ],
                [
                  -123.3722591,
                  48.4549366
                ],
                [
                  -123.3704567,
                  48.4559612
                ]
              ]
            ]
          }
        });
    
        // Test operation
    
        let result = await Route.findOneAndUpdate(
          { name: 'A Route' },
          { $push: { 'route.coordinates.0': [-123.3701134, 48.4467389] } },
          { new: true }
        );
        log(result);
    
      } catch(e) {
        console.error(e);
      } finally {
        mongoose.disconnect();
      }
    
    })()
    

    And the "correct" output from corrected schema:

    Mongoose: routes.createIndex({ route: '2dsphere' }, { background: true })
    Mongoose: routes.deleteMany({}, {})
    Mongoose: routes.insertOne({ route: { type: 'MultiLineString', coordinates: [ [ [ -123.3867645, 48.4813423 ], [ -123.3785248, 48.4592626 ], [ -123.3766365, 48.4527165 ], [ -123.3756924, 48.4523749 ], [ -123.3722591, 48.4549366 ], [ -123.3704567, 48.4559612 ] ] ] }, _id: ObjectId("5be9f3915fc64e3548603766"), name: 'A Route', __v: 0 })
    Mongoose: routes.findOneAndUpdate({ name: 'A Route' }, { '$push': { 'route.coordinates.0': [ -123.3701134, 48.4467389 ] } }, { upsert: false, remove: false, projection: {}, returnOriginal: false })
    {
      "route": {
        "type": "MultiLineString",
        "coordinates": [
          [
            [
              -123.3867645,
              48.4813423
            ],
            [
              -123.3785248,
              48.4592626
            ],
            [
              -123.3766365,
              48.4527165
            ],
            [
              -123.3756924,
              48.4523749
            ],
            [
              -123.3722591,
              48.4549366
            ],
            [
              -123.3704567,
              48.4559612
            ],
            [
              -123.3701134,
              48.4467389
            ]
          ]
        ]
      },
      "_id": "5be9f3915fc64e3548603766",
      "name": "A Route",
      "__v": 0
    }