Mongoose: Default getter/setter for virtual fields

Created on 21 Mar 2018  ·  14Comments  ·  Source: Automattic/mongoose

Do you want to request a feature or report a bug?
Feature

Feature description
I find myself needing to type the following often, when I want to append a non-persistent virtual field to a model:

CustomerSchema.virtual('totalValue')
  .set(function(totalValue) {
    this._totalValue = totalValue;
  })
  .get(function() {
    return this._totalValue;
  });

This is fine if you need to do it once or twice, but when you have a lot of those properties it begins to feel a bit verbose.

It'd be create if this could be the default for a virtual field, so that I could simply write:

CustomerSchema.virtual('totalValue');
CustomerSchema.virtual('numOrders');
//etc

The default getter/setter would then be implied if you don't provide any.

new feature

Most helpful comment

5.2.0 will add getters/setters for you if there aren't any getters or setters defined. By default, the default virtual will be equivalent to:

CustomerSchema.virtual('totalValue');

CustomerSchema.virtual('totalValue2') // Equivalent getters/setters
  .set(function(totalValue) {
    this.$totalValue = totalValue;
  })
  .get(function() {
    return this.$totalValue;
  });

All 14 comments

Hi @adamreisnz You can achieve this now with a schema plugin. Here's an example of a schema plugin applied globally to all schemas, but you can also add them to individual schemas. The docs are here.

EDIT:

changing example to a more relevant one to this request. It's meant to show that with a schema plugin, just about anything is possible. The only real difference that comes to mind between what you're asking for and what this does is that the virtuals need to be defined before the plugin is added.

EDIT 2:

added a check for setters/getters and only add default if no setters/getters exist already

EDIT 3:

making the example a global plugin.

mongoosetters/index.js

'use strict'

module.exports = function (schema, options) {
  let opts = options || {}
  if (!opts.prefix) {
    opts.prefix = '_'
  }

  let paths = Object.keys(schema.virtuals)
  paths.forEach((path) => {
    let numSetters = schema.virtuals[path].setters.length
    let numGetters = schema.virtuals[path].getters.length
    let property = opts.prefix + path
    if (numSetters === 0) {
      schema.virtual(path)
        .set(function (val) {
          this[property] = val
        })
    }
    if (numGetters === 0) {
      schema.virtual(path)
        .get(function () {
          return this[property]
        })
    }
  })
}

index.js

#!/usr/bin/env node
'use strict'

const defaulter = require('mongoosetters')
const mongoose = require('mongoose')
mongoose.plugin(defaulter)
const Schema = mongoose.Schema

const person = new Schema({
  name: String,
  age: Number,
  siblings: Array
})

const car = new Schema({
  make: String,
  model: String
})

person.virtual('nickName')
person.virtual('placeHolder')
person.virtual('setAndGetAlreadySet').set(function (val) {
  this.somethingElse = val
}).get(function () {
  return this.somethingElse
})

car.virtual('owners')
car.virtual('dealer')
car.virtual('miles').set(function (val) {
  this.mileage = val
}).get(function () {
  return this.mileage
})

const Person = mongoose.model('person', person)
const Car = mongoose.model('car', car)

const billy = new Person({
  name: 'William',
  age: 25,
  siblings: [ 'Mary' ]
})

const beater = new Car({
  make: 'Ford',
  model: 'Taurus'
})

billy.nickName = 'billy'
console.log(`${billy._nickName} = ${billy.nickName}`)

billy.placeHolder = 'place held.'
console.log(`${billy._placeHolder} = ${billy.placeHolder}`)

billy.setAndGetAlreadySet = 'test'
console.log(`${billy.somethingElse} = ${billy.setAndGetAlreadySet}`)

beater.owners = 'Martha'
console.log(`${beater._owners} = ${beater.owners}`)

beater.dealer = 'Fairway Ford'
console.log(`${beater._dealer} = ${beater.dealer}`)

beater.miles = 45666
console.log(`${beater.mileage} = ${beater.miles}`)

output:

6262: ./index.js
billy = billy
place held. = place held.
test = test
Martha = Martha
Fairway Ford = Fairway Ford
45666 = 45666
6262:

No I understand that, but that's not the use case or problem I was trying
to solve.

My problem is having to create multiple simple getter/setter properties on
a single schema, which is quite verbose currently.

Hence the idea of a simple default setter / getter for every new virtual
property.

On Sat, Mar 24, 2018, 00:55 Kev notifications@github.com wrote:

Hi @adamreisnz https://github.com/adamreisnz You can achieve this now
with a schema plugin. Here's an example of a schema plugin applied globally
to all schemas, but you can also add them to individual schemas. The docs
are here http://mongoosejs.com/docs/plugins.html.
totalValue.js

'use strict'

module.exports = function (schema) {
schema.virtual('totalValue')
.set(function (totalValue) {
this._totalValue = totalValue
})
.get(function () {
return this._totalValue
})
}

index.js

!/usr/bin/env node

'use strict'

const totalValue = require('./totalValue')
const mongoose = require('mongoose')
mongoose.connect('mongodb://localhost/test')
mongoose.plugin(totalValue)
const Schema = mongoose.Schema
const sOpts = { getters: true, virtuals: true }

const trinketSchema = new Schema({ name: String }, { toObject: sOpts })
const gadgetSchema = new Schema({ name: String }, { toObject: sOpts })

const Trinket = mongoose.model('trinket', trinketSchema)
const Gadget = mongoose.model('gadget', gadgetSchema)

const bobble = new Trinket({
name: 'necklace'
})

const doodad = new Gadget({
name: 'Bluetooth Toilet Flusher'
})

bobble.totalValue = 8
doodad.totalValue = 'priceless'

console.log(bobble)
console.log(doodad)
process.exit(0)

output:

6262: ./index.js
{ _id: 5ab4ea215621af4b61284544,
name: 'necklace',
totalValue: 8,
id: '5ab4ea215621af4b61284544' }
{ _id: 5ab4ea215621af4b61284545,
name: 'Bluetooth Toilet Flusher',
totalValue: 'priceless',
id: '5ab4ea215621af4b61284545' }
6262:


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/Automattic/mongoose/issues/6262#issuecomment-375635922,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAd8Ql-qEwucMwWVQhSfT-DWwrNQXWy2ks5thOKbgaJpZM4S15eh
.

You're right. My example sucked. I got so excited about the possibilities of what someone could do to automate adding virtuals to a schema... Then in order to not get carried away, I re-focused instead on the fact that it was possible at all and lost sight of the point of your feature request in the process. Apologies if I came off as shooting your idea down, I wasn't.

No worries, no offence taken :)

i don't think it's a good solution to default to using underscore-prefixed private properties like that. That seems a bit too opinionated on mongoose's part. Some people abhor that convention and others like it, i think it's a slight inconvenience but overall better for the library to not inject magic like that

I didn't mean to recommend that we use underscores, if there's a better way of implementing it I'm all for it!

This is my default approach though to creating simple getter/setter virtual properties, as it's easiest and simplest. Yet, if you have to create several properties like that it just feels quite verbose.

Perhaps under the hood Mongoose could actually store all private data and properties on a single "private" property, something like __mongoose or __m, which would also avoid other potential naming conflicts (like unable to use properties like isNew, which I've really struggled with to come up with an alternative name for, when you want to indicate new products in a store 😉).

But I don't know how viable it would be to update the whole codebase for a change like that.

@adamreisnz I edited my example above to be more relevant ( I hope! ). On the off chance that anyone finds it useful, and because I don't think there are enough packages on npm yet, I added it here

Interesting idea, but I don't like having to apply the plugin afterwards. I generally define all general plugins globally for all schema's, in advance of defining the schema's. So it kind of moves the problem to having to remember to apply this plugin to each schema after setting up virtuals.

But good effort, I might look at hacking it into Mongoose in some way if I find the time.

I was actually thinking about this earlier, so I decided to test it when I had time tonight. I just assumed that because the plugin was relying on the virtuals being declared already, that it would have to be added afterwards. I was wrong.

It turns out that this works just fine as a global plugin.

example updated above.

I understand @varunjayaraman 's concerns about having an opinionated default, but to work around that we could also support CustomerSchema.virtual('totalValue', '$totalValue') or some similar syntax. Thoughts?

Yes, that could work, to specify the underlying field. But I still think a default could be used, as long as the likelihood of it clashing with another property is very small, e.g. something verbose as $__private_totalValue. As long as it's dynamically generated under the hood, who cares really how the properties are called? And it would save further keystrokes and verbosity when setting up simple virtuals.

Yeah that's fair. I would probably err on the side of just using $totalValue by default, but you're right, it isn't really a big deal unless the property clashes, in which case we would be better off nesting these virtual properties underneath a separate object.

5.2.0 will add getters/setters for you if there aren't any getters or setters defined. By default, the default virtual will be equivalent to:

CustomerSchema.virtual('totalValue');

CustomerSchema.virtual('totalValue2') // Equivalent getters/setters
  .set(function(totalValue) {
    this.$totalValue = totalValue;
  })
  .get(function() {
    return this.$totalValue;
  });

That's great, thanks for that 👍

Was this page helpful?
0 / 5 - 0 ratings