Can not add additional element to (mongoose) object
Asked Answered
S

3

6

I have a nodejs express application with an api to return data from a mongodb database. This is my mongoose model:

const bookingSchema = new mongoose.Schema({
  timestamp: {
    type: Date,
    default: Date.now,
    required: true
  },
  tags: {
    type: [String],
    required: true
  },
  amount: {
    type: Number,
    required: true
  },
  type: {
    type: String,
    required: true,
    enum: ['expense', 'income']
  }
})

When I'm calling the api with the path /api/bookings/listbymonth/2019/1 this function inside the backend gets called:

const bookingsListByMonth = (req, res) => {
  const year = ("0000" + req.params.year).slice(-4)
  const month = ("0000" + req.params.month).slice(-2)
  const dateOfMonth = `${year}${month}01`
  const start = moment(dateOfMonth).startOf("month")
  const end = moment(dateOfMonth).endOf("month")

  bookingMongooseModel
    .find({
      timestamp: {
        $gt: start,
        $lt: end
      }
    })
    .sort({ timestamp: 1 })
    .exec((err, bookings) => {
      if (!bookings) {
        return res
          .status(404)
          .json({
            "message": "booking not found"
          })
      } else if (err) {
        return res
          .status(404)
          .json(err)
      }
      res
        .status(200)
        .json(processBookings(bookings));
    })
}

Instead of simply returning the json data, I want to preprocess the data and make a nice timestamp and currency field. That's why the json data runs through an additional processBookings function. For testing I tried to add another field timestamp2: 123:

const processBookings = (bookings) => {
  console.log("Bookings unsorted: \n" + bookings + "\n")

  const mainTags = [
    "Essen",
    "Essen gehen",
    "Notwendiges",
    "Luxus",
  ]

  let bookingsProcessed = []

  mainTags.forEach((tag) => {
    let singleTagBookings = bookings.filter(
      booking => booking.tags.includes(tag)
    )

    singleTagBookings.map((item) => {
      item.timestamp2 = "123"
      return item
    })

    let message = null;
    if (singleTagBookings.length === 0) {
      message = "No bookings found";
    }

    bookingsProcessed.push({
      name: tag,
      bookings: singleTagBookings,
      message: message
    })
  });

  console.log("Bookings sorted:")
  bookingsProcessed.forEach((item) => {
    console.log(item)
  })

  return bookingsProcessed
}

The objects in the bookings array should have another property timestamp2: "123", but they don't. Here's the output:

Bookings unsorted: 
{ tags: [ 'Luxus', 'voluptatem', 'atque', 'qui', 'sunt' ],
  _id: 5cb2c9e1ff6c9c6bef95f56f,
  timestamp: 2019-01-06T08:53:06.945Z,
  amount: 68.02,
  type: 'expense',
  __v: 0 },{ tags: [ 'Essen gehen', 'ut', 'unde', 'et', 'officiis' ],
  _id: 5cb2c9e1ff6c9c6bef95f56e,
  timestamp: 2019-01-09T20:35:06.411Z,
  amount: 33.77,
  type: 'income',
  __v: 0 }

Bookings sorted:     
{ name: 'Essen', bookings: [], message: 'No bookings found' }
{ name: 'Essen gehen',
  bookings: 
   [ { tags: [Array],
       _id: 5cb2c9e1ff6c9c6bef95f56e,
       timestamp: 2019-01-09T20:35:06.411Z,
       amount: 33.77,
       type: 'income',
       __v: 0 } ],
  message: null }
{ name: 'Notwendiges',
  bookings: [],
  message: 'No bookings found' }
{ name: 'Luxus',
  bookings: 
   [ { tags: [Array],
       _id: 5cb2c9e1ff6c9c6bef95f56f,
       timestamp: 2019-01-06T08:53:06.945Z,
       amount: 68.02,
       type: 'expense',
       __v: 0 } ],
  message: null }

As in the comments suggested I tried to use let bookings = [ {tags: ["Essen"]}]; as test data. Here it works. The output is:

Bookings unsorted: 
[object Object]

Bookings sorted:
{ name: 'Essen',
  bookings: [ { tags: [Array], timestamp2: '123' } ],
  message: null }
{ name: 'Essen gehen',
  bookings: [],
  message: 'No bookings found' }
{ name: 'Notwendiges',
  bookings: [],
  message: 'No bookings found' }
{ name: 'Luxus', bookings: [], message: 'No bookings found' }

So I guess it has something to do with my mongoose model restricting to add any additional field. However If I put

console.log("EXTENSIBLE " + Object.isExtensible(bookings))
res
  .status(200)
  .json(processBookings(bookings));

into my bookingsListByMonth function I get:

EXTENSIBLE true

So in theory I should be able to add something to the bookings object?

As a workaround I added the timestamp2 field to my mongoose model:

const bookingSchema = new mongoose.Schema({
  timestamp: {
    type: Date,
    default: Date.now,
    required: true
  },
  timestamp2: {
    type: String,
    default: null
  },
  tags: {
    type: [String],
    required: true
  },
  amount: {
    type: Number,
    required: true
  },
  type: {
    type: String,
    required: true,
    enum: ['expense', 'income']
  }
})

This works, however it adds an additional useless data field into my database. How can I modify the bookings json object returned from the mongodb? If I can't modify it because it is a mongoose model, how can I make a copy that is editable?

Selfevident answered 14/4, 2019 at 5:26 Comment(8)
nope, the code works as expected ... by the way, your code will change the objects in singleTagBookings too, so, you could just use .forEach instead - are the objects in the Array singleTagBookings sealed perhaps?Millepore
You must be getting an error singleTagBookings.map is not a function if not than update question with proper code and dataUpheld
The objects are not sealed. Sadly I don't get an error. I have updated my question with the full code.Selfevident
are they extensible? - your code works just fine, adding timestamp2 ... see your code in actionMillepore
but if you Object.preventExtensions(obj) jsfiddle.net/ybdz3vjp/3 - also sealing and freezing will result in the same output - you can check if an object is able to have properties added by Object.isExtensible(obj)Millepore
As @JaromandaX reported the code works as posted - try copying it and using let bookings = [ {tags: ["Essen"]}]; for test data. Note that the input version of the bookings array is being updated and must not be frozen or sealed.Kaiulani
@traktor53 or made non-extensible eitherMillepore
Using let bookings = [ {tags: ["Essen"]}]; as test data works. I have elaborated my question.Selfevident
P
13

Documents returned from queries are mongoose documents which restricts addition of new fields. In order to convert mongoose documents into plain JavaScript objects there is a method/option lean in mongoose - https://mongoosejs.com/docs/tutorials/lean.html.

The find query would look something like this

bookingMongooseModel
.find({
  timestamp: {
    $gt: start,
    $lt: end
  }
})
.sort({ timestamp: 1 })
.lean()

Additionally, mongoose provides configurable options for you to set while defining schema. For instance, new Schema({..}, {timestamps: true}); would create two fields in your document createdAt and updatedAt. For more information see Options section here https://mongoosejs.com/docs/guide.html#options

Photosynthesis answered 18/4, 2019 at 1:33 Comment(0)
O
2

Just transform your mongoose object to standard javascript object, with toObject() function. Then you will be able to modify the datas / add new fields / change type of datas :

 const processBookings = (bookings) => {
     bookings = bookings.map(function(e){return e.toObject();});
     ...
Oliy answered 19/4, 2019 at 14:53 Comment(0)
M
0
  singleTagBookings.map((item) => {
      item.timestamp2 = "123"
      return item
    })

I think this is your problem..

  1. map returns a new array, so you need to assign it to a variable
  2. in the map callback function its better to return a new object than mutate it

so try this instead

singleTagBookings = singleTagBookings.map((item) => ({
       ...item,
       timestamp2: "123"
    }))
Microsurgery answered 17/4, 2019 at 8:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.