ChrisRich
ChrisRich

Reputation: 8736

Keystone.js / mongoose virtual fields lean record

I'm trying to produce a lean record for a REST API that include virtual fields.

The official documentation for how to implement virtual fields for Mongoose:
http://mongoosejs.com/docs/guide.html

My model:

var keystone = require('keystone')
    , Types = keystone.Field.Types
    , list = new keystone.List('Vendors');

list.add({
    name : {
          first: {type : Types.Text}
        , last: {type : Types.Text}
    }
});

list.schema.virtual('name.full').get(function() {
    return this.name.first + ' ' + this.name.last;
});

list.register();

Now, let's query the model:

var keystone = require('keystone'),
    vendors = keystone.list('Vendors');

vendors.model.find()
    .exec(function(err, doc){
        console.log(doc)
    });

Virtual field name.full is not here:

[ { _id: 563acf280f2b2dfd4f59bcf3,
    __v: 0,
    name: { first: 'Walter', last: 'White' } }]

But if we do this:

vendors.model.find()
    .exec(function(err, doc){
        console.log(doc.name.full); // "Walter White"
    });

Then the virtual shows.

I guess the reason is that when I do a console.log(doc) the Mongoose document.toString() method is invoked which does not include virtuals by default. Fair enough. That's understandable.

To include the virtuals in any of the conversion methods you have to go:

doc.toString({virtuals: true}) 
doc.toObject({virtuals: true}) 
doc.toJSON({virtuals: true}) 

However, this includes keys I don't want for my REST API to pump out to my users:

{ _id: 563acf280f2b2dfd4f59bcf3,
  __v: 0,
  name: { first: 'Walter', last: 'White', full: 'Walter White' },
  _: { name: { last: [Object], first: [Object] } },
  list: 
   List {
     options: 
      { schema: [Object],
        noedit: false,
        nocreate: false,
        nodelete: false,
        autocreate: false,
        sortable: false,
        hidden: false,
        track: false,
        inherits: false,
        searchFields: '__name__',
        defaultSort: '__default__',
        defaultColumns: '__name__',
        label: 'Vendors' },
     key: 'Vendors',
     path: 'vendors',
     schema: 
      Schema {
        paths: [Object],
        subpaths: {},
        virtuals: [Object],
        nested: [Object],
        inherits: {},
        callQueue: [],
        _indexes: [],
        methods: [Object],
        statics: {},
        tree: [Object],
        _requiredpaths: [],
        discriminatorMapping: undefined,
        _indexedpaths: undefined,
        options: [Object] },
     schemaFields: [ [Object] ],
     uiElements: [ [Object], [Object] ],
     underscoreMethods: { name: [Object] },
     fields: { 'name.first': [Object], 'name.last': [Object] },
     fieldTypes: { text: true },
     relationships: {},
     mappings: 
      { name: null,
        createdBy: null,
        createdOn: null,
        modifiedBy: null,
        modifiedOn: null },
     model: 
      { [Function: model]
        base: [Object],
        modelName: 'Vendors',
        model: [Function: model],
        db: [Object],
        discriminators: undefined,
        schema: [Object],
        options: undefined,
        collection: [Object] } },
  id: '563acf280f2b2dfd4f59bcf3' }

I can always of course just delete the unwanted keys, but this doesn't seem quite right:

vendors.model.findOne()
    .exec(function(err, doc){
        var c = doc.toObject({virtuals: true});
        delete c.list;
        delete c._;
        console.log(c)
    });

This produces what I need:

{ _id: 563acf280f2b2dfd4f59bcf3,
  __v: 0,
  name: { first: 'Walter', last: 'White', full: 'Walter White' },
  id: '563acf280f2b2dfd4f59bcf3' }

Is there not a better way of getting a lean record?

Upvotes: 0

Views: 1304

Answers (2)

Mr5o1
Mr5o1

Reputation: 1818

I think you want the select method.. something like this:

vendors.model.findOne()
.select('_id __v name').
.exec(function(err, doc){
    console.log(c)
});

Also personally I prefer setting virtuals: true on the schema rather than the document, but depends on use case I guess.

Upvotes: 1

user1146717
user1146717

Reputation:

One solution would be to use a module like Lodash (or Underscore) which allows you pick a whitelist of property names:

vendors.model.findOne()
    .exec(function(err, doc){
        var c = _.pick(doc, ['id', 'name.first', 'name.last', 'name.full']);
        console.log(c)
    });

Given your use-case of serving this data via REST API, I think explicitly defining a whitelist of property names is safer. You could even define a virtual property on your schema which returns the predefined whitelist:

list.schema.virtual('whitelist').get(function() {
    return ['id', 'name.first', 'name.last', 'name.full'];
});

and use it in multiple places, or have different versions of your whitelist, all managed at the model layer.

Upvotes: 0

Related Questions