How to add timestamps to mongoose schema subfields?

Viewed 2130

I'm trying to add createdAt and updatedAt timestamps in subfields of otp in generate: {} and verify:{}

I know that using { timestamps: true } will add the createdAt and updatedAt timestamps to the whole schema.`

const userSchema = new mongoose.Schema({    
    email: { type: String, unique: true },
    name: { type: String }, 
    mobileNumber: {
        isVerified: {type: Boolean, default: false},
        otp: {
            generate: {
              attempts: {type: Number, default: 0},
              total: {type: Number, default: 0},
              createdAt: {type: Date},
              updatedAt: {type: Date}
            },
            verify: {
              attempts: {type: Number, default: 0},
              total: {type: Number, default: 0},
              createdAt: {type: Date},
              updatedAt: {type: Date}
            }
          }
    }
}, { timestamps: true });

What is correct solution to add individual timestamps to subfields? Is it correct to do the same by adding {timestamps: true} to the subfields?

            generate: {
              attempts: {type: Number, default: 0},
              total: {type: Number, default: 0},
              {timestamps: true}
            },
            verify: {
              attempts: {type: Number, default: 0},
              total: {type: Number, default: 0},
              {timestamps: true}
            }
4 Answers

You will have do define a separate schema for your subfields, then use that as the type of your subfields.

const otpSchema = new mongoose.Schema({    
    attempts: { type: Number, default: 0 },
    total: { type: Number, default: 0 }
}, {
    _id: false, // omit _id fields for subfields
    timestamps: true // timestamps options for subfields
});

const userSchema = new mongoose.Schema({    
    email: { type: String, unique: true },
    name: { type: String }, 
    mobileNumber: {
        isVerified: { type: Boolean, default: false },
        otp: {
            generate: otpSchema, // use the defined schema
            verify: otpSchema
        }
    }
}, { timestamps: true });

Okay, it seems like this answer became more popular, so a will extended it with full coverage.

What does {timesamps: true} do and how it does what it does?

The original {timesamps: true} code, from mongoose.js can be found here @ line:1150

How exactly timestamps: true} knows when and how it should update updatedAt field, and don't update createdAt ?

By this code:

  this.pre('save', function(next) {
    
    /**
    * SKIP CODE A BIT
    */

    if (!skipUpdatedAt && updatedAt && (this.isNew || this.isModified())) {
      let ts = defaultTimestamp;
      if (this.isNew) {
        if (createdAt != null) {
          ts = this.$__getValue(createdAt);
        } else if (auto_id) {
          ts = this._id.getTimestamp();
        }
      }
      this.set(updatedAt, ts);
    }

    next();
  });

So each time when mongoose driver triggers .save on MongooseDocument, this code got executed (if timestamps set to true, of course)

There is a big difference between MongooseDocument (object) and js-Object/JSON/result of find({}).lean()

You could cast various methods on MongooseDocument, like .isNew (this is exactly how mongoose understood that updatedAt field should be updated, and createdAt should not). or convert it .toObject() or .toJSON() Full list of methods can be found here.

Just to be sure: when you are using .find without .lean() option, you are dealing with MongooseDocument(s), but if enable it, you will receive plain JavaScript objects.

How to create you own implementation of {timestamps: true} for your own schema?

It's easy to achieve the same results, via default values and with using setters in your schema:

  createdAt: {type: Date, default: Date.now},
  updatedAt: {type: Date, default: Date.now, set: v => v.Date.now()}

You could read more about setters here.

Also, it could be any function you want, for example you could modify value each time on any insert || modify operation (and update, too)

..Or you could avoid setters and update updatedAt field manually in code, every time, via: model.findAndUpdate({your_field: value}, {your_field: value, updatedAt: Date.now()) each time.

So, in the end, using setters (or manual query update), will gave you the same result as timestamps: true option, but you could apply it to every sub-document in your schema.

I don't think such feature fall in the scope of the database capabilities to provide, nor mongoose to enable.

You may want to create two other entities - Attribute and AttributeValue with OneToMany relationship, to track values changes timestamp.

Well, that's how we tackled the issue on my main current project.

I ran into the same problem and mine was a pretty straightforward solution.

const userSchema = mongoose.Schema({ email: String }, { timestamps: true });

my result with createdAt and updatedAt automatically added:

{
   "_id" : ObjectId("60ba2cb3bca3572f6ca1b525"),
   "title" : "First post with timestamp",
   "content" : "Test post timestamp",
   "createdAt" : ISODate("2021-06-04T13:37:55.025Z"),
   "updatedAt" : ISODate("2021-06-04T13:37:55.025Z"),
   "__v" : 0
}
Related