Skip to content

Commit

Permalink
feat: allow defining virtuals on arrays, not just array elements
Browse files Browse the repository at this point in the history
  • Loading branch information
vkarpov15 committed Oct 11, 2024
1 parent 02c5efd commit cb544ed
Show file tree
Hide file tree
Showing 7 changed files with 103 additions and 2 deletions.
10 changes: 10 additions & 0 deletions lib/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -2304,6 +2304,7 @@ Schema.prototype.indexes = function() {
* @param {Boolean} [options.count=false] Only works with populate virtuals. If [truthy](https://masteringjs.io/tutorials/fundamentals/truthy), this populate virtual will contain the number of documents rather than the documents themselves when you `populate()`.
* @param {Function|null} [options.get=null] Adds a [getter](https://mongoosejs.com/docs/tutorials/getters-setters.html) to this virtual to transform the populated doc.
* @param {Object|Function} [options.match=null] Apply a default [`match` option to populate](https://mongoosejs.com/docs/populate.html#match), adding an additional filter to the populate query.
* @param {Boolean} [options.applyToArray=false] If true and the given `name` is a direct child of an array, apply the virtual to the array rather than the elements.
* @return {VirtualType}
*/

Expand Down Expand Up @@ -2416,6 +2417,15 @@ Schema.prototype.virtual = function(name, options) {
return mem[part];
}, this.tree);

if (options && options.applyToArray && parts.length > 1) {
const path = this.path(parts.slice(0, -1).join('.'));
if (path && path.$isMongooseArray) {
return path.virtual(parts[parts.length - 1], options);
} else {
throw new MongooseError(`Path "${path}" is not an array`);
}
}

return virtuals[name];
};

Expand Down
35 changes: 35 additions & 0 deletions lib/schema/array.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@ const SchemaArrayOptions = require('../options/schemaArrayOptions');
const SchemaType = require('../schemaType');
const CastError = SchemaType.CastError;
const Mixed = require('./mixed');
const VirtualOptions = require('../options/virtualOptions');
const VirtualType = require('../virtualType');
const arrayDepth = require('../helpers/arrayDepth');
const cast = require('../cast');
const clone = require('../helpers/clone');
const getConstructorName = require('../helpers/getConstructorName');
const isOperator = require('../helpers/query/isOperator');
const util = require('util');
const utils = require('../utils');
Expand Down Expand Up @@ -217,6 +220,12 @@ SchemaArray._checkRequired = SchemaType.prototype.checkRequired;

SchemaArray.checkRequired = SchemaType.checkRequired;

/*!
* Virtuals defined on this array itself.
*/

SchemaArray.prototype.virtuals = null;

/**
* Check if the given value satisfies the `required` validator.
*
Expand Down Expand Up @@ -575,6 +584,32 @@ SchemaArray.prototype.castForQuery = function($conditional, val, context) {
}
};

/**
* Add a virtual to this array. Specifically to this array, not the individual elements.
*
* @param {String} name
* @param {Object} [options]
* @api private
*/

SchemaArray.prototype.virtual = function virtual(name, options) {
if (name instanceof VirtualType || getConstructorName(name) === 'VirtualType') {
return this.virtual(name.path, name.options);
}
options = new VirtualOptions(options);

if (utils.hasUserDefinedProperty(options, ['ref', 'refPath'])) {
throw new MongooseError('Cannot set populate virtual as a property of an array');
}

const virtual = new VirtualType(options, name);
if (this.virtuals === null) {
this.virtuals = {};
}
this.virtuals[name] = virtual;
return virtual;
};

function cast$all(val, context) {
if (!Array.isArray(val)) {
val = [val];
Expand Down
2 changes: 1 addition & 1 deletion lib/schema/documentArray.js
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ SchemaDocumentArray.prototype.cast = function(value, doc, init, prev, options) {
// We need to create a new array, otherwise change tracking will
// update the old doc (gh-4449)
if (!options.skipDocumentArrayCast || utils.isMongooseDocumentArray(value)) {
value = new MongooseDocumentArray(value, path, doc);
value = new MongooseDocumentArray(value, path, doc, this);
}

if (prev != null) {
Expand Down
5 changes: 5 additions & 0 deletions lib/types/array/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ function MongooseArray(values, path, doc, schematype) {
if (mongooseArrayMethods.hasOwnProperty(prop)) {
return mongooseArrayMethods[prop];
}
if (schematype && schematype.virtuals && schematype.virtuals.hasOwnProperty(prop)) {
return schematype.virtuals[prop].applyGetters(undefined, target);
}
if (typeof prop === 'string' && numberRE.test(prop) && schematype?.$embeddedSchemaType != null) {
return schematype.$embeddedSchemaType.applyGetters(__array[prop], doc);
}
Expand All @@ -101,6 +104,8 @@ function MongooseArray(values, path, doc, schematype) {
mongooseArrayMethods.set.call(proxy, prop, value, false);
} else if (internals.hasOwnProperty(prop)) {
internals[prop] = value;
} else if (schematype && schematype.virtuals && schematype.virtuals.hasOwnProperty(prop)) {
schematype.virtuals[prop].applySetters(value, target);
} else {
__array[prop] = value;
}
Expand Down
7 changes: 6 additions & 1 deletion lib/types/documentArray/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const numberRE = /^\d+$/;
* @see https://bit.ly/f6CnZU
*/

function MongooseDocumentArray(values, path, doc) {
function MongooseDocumentArray(values, path, doc, schematype) {
const __array = [];

const internals = {
Expand Down Expand Up @@ -84,6 +84,9 @@ function MongooseDocumentArray(values, path, doc) {
if (DocumentArrayMethods.hasOwnProperty(prop)) {
return DocumentArrayMethods[prop];
}
if (schematype && schematype.virtuals && schematype.virtuals.hasOwnProperty(prop)) {
return schematype.virtuals[prop].applyGetters(undefined, target);
}
if (ArrayMethods.hasOwnProperty(prop)) {
return ArrayMethods[prop];
}
Expand All @@ -95,6 +98,8 @@ function MongooseDocumentArray(values, path, doc) {
DocumentArrayMethods.set.call(proxy, prop, value, false);
} else if (internals.hasOwnProperty(prop)) {
internals[prop] = value;
} else if (schematype && schematype.virtuals && schematype.virtuals.hasOwnProperty(prop)) {
schematype.virtuals[prop].applySetters(value, target);
} else {
__array[prop] = value;
}
Expand Down
43 changes: 43 additions & 0 deletions test/document.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13978,6 +13978,49 @@ describe('document', function() {
assert.ok(Buffer.isBuffer(reloaded.pdfSettings.fileContent));
assert.strictEqual(reloaded.pdfSettings.fileContent.toString('utf8'), 'hello');
});

describe('gh-2306', function() {
it('allow define virtual on non-object path', function() {
const schema = new mongoose.Schema({ num: Number, str: String, nums: [Number] });
schema.path('nums').virtual('last').get(function() {
return this[this.length - 1];
});
schema.virtual('nums.first', { applyToArray: true }).get(function() {
return this[0];
});
schema.virtual('nums.selectedIndex', { applyToArray: true })
.get(function() {
return this.__selectedIndex;
})
.set(function(v) {
this.__selectedIndex = v;
});
const M = db.model('gh2306', schema);
const m = new M({ num: 2, str: 'a', nums: [1, 2, 3] });

assert.strictEqual(m.nums.last, 3);
assert.strictEqual(m.nums.first, 1);

assert.strictEqual(m.nums.selectedIndex, undefined);
m.nums.selectedIndex = 42;
assert.strictEqual(m.nums.__selectedIndex, 42);
});

it('works on document arrays', function() {
const schema = new mongoose.Schema({ books: [{ title: String, author: String }] });
schema.path('books').virtual('last').get(function() {
return this[this.length - 1];
});
schema.virtual('books.first', { applyToArray: true }).get(function() {
return this[0];
});
const M = db.model('Test', schema);
const m = new M({ books: [{ title: 'Casino Royale', author: 'Ian Fleming' }, { title: 'The Man With The Golden Gun', author: 'Ian Fleming' }] });

assert.strictEqual(m.books.first.title, 'Casino Royale');
assert.strictEqual(m.books.last.title, 'The Man With The Golden Gun');
});
});
});

describe('Check if instance function that is supplied in schema option is available', function() {
Expand Down
3 changes: 3 additions & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,9 @@ declare module 'mongoose' {
/** Additional options like `limit` and `lean`. */
options?: QueryOptions<DocType> & { match?: AnyObject };

/** If true and the given `name` is a direct child of an array, apply the virtual to the array rather than the elements. */
applyToArray?: boolean;

/** Additional options for plugins */
[extra: string]: any;
}
Expand Down

0 comments on commit cb544ed

Please sign in to comment.