Help on virtual "on the fly" #4888

Open
jbdemonte opened this Issue Jan 11, 2017 · 7 comments

Projects

None yet

4 participants

@jbdemonte

Hi,

We have a pretty large application which has tons of kinda "REST" API which returns data mostly based on mongoose models. In lots of them, we are adding custom aggregate, flags...
Obviously, it would be insane and difficult to maintain to have them it the schema description file.

So, we add these data by leaning the mongoose result (or playing with .toObject())

I'm trying to have a cleaner way to handle that process, to keep the mongoose model up to the real http response send.

Here is a plugin I'm trying to write to handle that:

var mongoose = require('mongoose');
mongoose.plugin(AddVirtual);

/**
 * Add a virtual on the fly
 * @param schema
 * @constructor
 */
function AddVirtual(schema) {
  var added = {};

  schema.methods.addVirtual = function (name, value) {
    if (!added[name]) {
      added[name] = true;
      schema.virtual('name');
    }
    this[name] = value;
  };
}

var CatSchema = new mongoose.Schema(
  {name: String},
  {toObject: {virtuals: true}, toJSON: {virtuals: true}}
);
CatSchema.virtual('classic');
var Cat = mongoose.model('Cat', CatSchema);

var kitty = new Cat({ name: 'Zildjian' });
kitty.addVirtual('onTheFly', 123);

console.log(kitty.onTheFly); // => 123
console.log(kitty.toJSON()); // => classic is in the result, but not the onTheFly

It seems to not be possible to add virtual after the model instanciation, do you see any way I could use to bypass this (I'm going to dig into the Model / Document class).

Thanks in advance for your time on this non-issue

@varunjayaraman
Collaborator
varunjayaraman commented Jan 11, 2017 edited

You have a typo: instead of schema.virtual('name'); you want to do schema.virtual(name);

To be honest, I don't think it's "insane and difficult" to maintain all your virtuals in the mongoose schema, that's kind of the point of the schema. It separates your model logic from your "controllers" or whatever you want to call the function that handles your API endpoints. And I'm a bit confused why you don't want to put your virtuals in the schema, since dynamically modifying your model seems dangerous.

Maybe I just don't understand what you're looking for. Are you trying to mutate the original Schema on the fly in one of your endpoints, or are you just trying to mutate the document itself to give it access to a virtual inside your endpoint. If it's the former, you can't mutate the original Schema object from a document because mongoose uses a constructor function to create a new object that gets stored on the model object. If it's the latter, why can't you just use a function? Or if you find yourself using that function often, just make it a virtual in your schema.

@jbdemonte

argg 'name' .... shame on me

@jbdemonte

Indeed, It works much better

var mongoose = require('mongoose');
mongoose.plugin(AddVirtual);

/**
 * Add a virtual on the fly
 * @param schema
 * @constructor
 */
function AddVirtual(schema) {
  var added = {};

  schema.methods.addVirtual = function (name, value) {
    if (!added[name]) {
      added[name] = true;
      schema
        .virtual(name)
        .get(function () {
          return value;
        })
        .set(function (_value) {
          value = _value;
        });
    }
    this[name] = value;
  };
}

var CatSchema = new mongoose.Schema(
  {name: String},
  {toObject: {virtuals: true}, toJSON: {virtuals: true}}
);
CatSchema.virtual('classic');

var Cat = mongoose.model('Cat', CatSchema);

var kitty = new Cat({ name: 'Zildjian' });
kitty.addVirtual('onTheFly', 123);

console.log(kitty.onTheFly);
console.log(kitty.toJSON());

kitty.set('onTheFly', 456);
kitty.onTheFly = 789;

console.log(kitty.onTheFly);
console.log(kitty.toJSON());

Any reason why ‘kitty.onTheFly=456‘ and ‘kitty.set('onTheFly', 456)‘ are not the same?

To clarify are use, we are ... pretty flexible with the REST API, we add various types of data in the results, so, the schema would have tons of unecessary virtual property

@TrejGun
Contributor
TrejGun commented Jan 12, 2017
.get(function (_value) {
          return _value;
        })
        .set(function (_value) {
          value = _value;
        });

try this

@jbdemonte

Nop,
and in fact, adding logs:

function AddVirtual(schema) {
  var added = {};

  schema.methods.addVirtual = function (name, value) {
    console.log('init', value);
    if (!added[name]) {
      added[name] = true;
      schema
        .virtual(name)
        .get(function () {
          console.log('get', value);
          return value;
        })
        .set(function (_value) {
          value = _value;
          console.log('set', value);
        });
    }
    this[name] = value;
  };
}

The "set" accessor is not called when having the "kitty.onTheFly = 789"
I'm going to dig to check how internally the virtual are handle, I guess they do not use a Object.defineProperty

@TrejGun
Contributor
TrejGun commented Jan 17, 2017

1 you have to have real field to store virtual
2 the real field can't have same name as virtual (or you will have recursion and error Maximum call stack size exceeded)
3 for example your virtual field name is onTheFly and corresponding real field is _onTheFly
4 you are setting virtual kitty.set('onTheFly', 456); and it sets real _onTheFly
5 you are getting kitty.toJSON({virtuals:true}); you are getting _onTheFly
6 you are setting REAL kitty.onTheFly = 456 and it sets real onTheFly
7 you are getting kitty.toJSON({virtuals:true}); you are getting onTheFly

var mongoose = require('mongoose');

function AddVirtual(schema) {
	var added = {};

	schema.methods.addVirtual = function (name, value) {
		if (!added[name]) {
			added[name] = true;
			schema
				.virtual(name)
				.get(function () {
					return this["_" + name];
				})
				.set(function (_value) {
					this["_" + name] = _value;
				});
		}
		this["_" + name] = value;
	};
}

mongoose.plugin(AddVirtual);

var CatSchema = new mongoose.Schema(
	{name: String},
	{toObject: {virtuals: true}, toJSON: {virtuals: true}}
);

var Cat = mongoose.model('Cat', CatSchema);

var kitty = new Cat({name: 'Zildjian'});
kitty.addVirtual('onTheFly', 123);


console.log("kitty.onTheFly", kitty.onTheFly, kitty._onTheFly);
kitty.set('onTheFly', 456);
console.log("kitty.onTheFly", kitty.onTheFly, kitty._onTheFly);
kitty.onTheFly = 789;
console.log("kitty.onTheFly", kitty.onTheFly, kitty._onTheFly);
kitty._onTheFly = 369;
console.log("kitty.onTheFly", kitty.onTheFly, kitty._onTheFly);
@vkarpov15
Collaborator

Just fair warning, adding a virtual to a single doc dynamically is going to be pretty slow. That's most of the reason why mongoose recommends you don't modify your schema after it's been compiled into a model. Not really sure how adding dynamic virtuals helps your code architecture organization issues though?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment