Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions lib/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -766,13 +766,17 @@ function init(self, obj, doc, opts, prefix) {
doc[i] = {};
if (!strict && !(i in docSchema.tree) && !(i in docSchema.methods) && !(i in docSchema.virtuals)) {
self[i] = doc[i];
} else if (opts?.virtuals && (i in docSchema.virtuals)) {
self[i] = doc[i];
}
}
init(self, value, doc[i], opts, path + '.');
} else if (!schemaType) {
doc[i] = value;
if (!strict && !prefix) {
self[i] = value;
} else if (opts?.virtuals && (i in docSchema.virtuals)) {
self[i] = value;
}
} else {
// Retain order when overwriting defaults
Expand All @@ -789,9 +793,9 @@ function init(self, obj, doc, opts, prefix) {
if (opts && opts.setters) {
// Call applySetters with `init = false` because otherwise setters are a noop
const overrideInit = false;
doc[i] = schemaType.applySetters(value, self, overrideInit);
doc[i] = schemaType.applySetters(value, self, overrideInit, null, opts);
} else {
doc[i] = schemaType.cast(value, self, true);
doc[i] = schemaType.cast(value, self, true, undefined, opts);
}
} catch (e) {
self.invalidate(e.path, new ValidatorError({
Expand Down
5 changes: 5 additions & 0 deletions lib/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -3966,13 +3966,18 @@ Model.buildBulkWriteOperations = function buildBulkWriteOperations(documents, op
* @param {Object} [options] optional options
* @param {Boolean} [options.setters=false] if true, apply schema setters when hydrating
* @param {Boolean} [options.hydratedPopulatedDocs=false] if true, populates the docs if passing pre-populated data
* @param {Boolean} [options.virtuals=false] if true, sets any virtuals present on `obj`
* @return {Document} document instance
* @api public
*/

Model.hydrate = function(obj, projection, options) {
_checkContext(this, 'hydrate');

if (options?.virtuals && options?.hydratedPopulatedDocs === false) {
throw new MongooseError('Cannot set `hydratedPopulatedDocs` option to false if `virtuals` option is truthy because `virtuals: true` also sets populated virtuals');
}

if (projection != null) {
if (obj != null && obj.$__ != null) {
obj = obj.toObject(internalToObjectOptions);
Expand Down
2 changes: 1 addition & 1 deletion lib/schema/documentArray.js
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,7 @@ SchemaDocumentArray.prototype.cast = function(value, doc, init, prev, options) {
}

subdoc = new Constructor(null, value, initDocumentOptions, selected, i);
rawArray[i] = subdoc.$init(rawArray[i]);
rawArray[i] = subdoc.$init(rawArray[i], options);
} else {
if (prev && typeof prev.id === 'function') {
subdoc = prev.id(rawArray[i]._id);
Expand Down
4 changes: 2 additions & 2 deletions lib/schema/map.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class SchemaMap extends SchemaType {
if (_val == null) {
_val = map.$__schemaType._castNullish(_val);
} else {
_val = map.$__schemaType.cast(_val, doc, true, null, { path: path + '.' + key });
_val = map.$__schemaType.cast(_val, doc, true, null, { ...options, path: path + '.' + key });
}
map.$init(key, _val);
}
Expand All @@ -49,7 +49,7 @@ class SchemaMap extends SchemaType {
if (_val == null) {
_val = map.$__schemaType._castNullish(_val);
} else {
_val = map.$__schemaType.cast(_val, doc, true, null, { path: path + '.' + key });
_val = map.$__schemaType.cast(_val, doc, true, null, { ...options, path: path + '.' + key });
}
map.$init(key, _val);
}
Expand Down
2 changes: 1 addition & 1 deletion lib/schema/subdocument.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ SchemaSubdocument.prototype.cast = function(val, doc, init, priorVal, options) {
if (init) {
subdoc = new Constructor(void 0, selected, doc, false, { defaults: false });
delete subdoc.$__.defaults;
subdoc.$init(val);
subdoc.$init(val, options);
const exclude = isExclusive(selected);
applyDefaults(subdoc, selected, exclude);
} else {
Expand Down
110 changes: 110 additions & 0 deletions test/model.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7700,6 +7700,116 @@ describe('Model', function() {
assert.equal(doc.text, 'foobar');
});

it('supports virtuals option for `hydrate()` (gh-15627)', function() {
// 2) virtual in a document array
const arrSchema = new Schema({
value: String
});
arrSchema.virtual('valueLower');

// 3) virtual in a subdocument
const nestedSchema = new Schema({
foo: String
});
nestedSchema.virtual('fooRev');

// 4) virtual in a map of subdocuments
const mapSubSchema = new Schema({
v: String
}, { _id: false });
mapSubSchema.virtual('vDouble');

const schema = Schema({
name: String,
nested: nestedSchema,
arr: [arrSchema],
map: {
type: Map,
of: mapSubSchema
},
friendId: {
type: mongoose.Schema.Types.ObjectId
}
}, { toObject: { virtuals: true }, toJSON: { virtuals: true } });

// 1) top-level virtual
schema.virtual('topLevel');

// 5) top-level populated virtual
schema.virtual('friend', {
ref: 'TestUser',
localField: 'friendId',
foreignField: '_id',
justOne: true
});

db.model('User', new Schema({
_id: mongoose.Schema.Types.ObjectId,
username: String
}));

const TestModel = db.model('Test', schema);

const userId = new mongoose.Types.ObjectId();

// Hydrate with various virtuals in the raw object
const doc = TestModel.hydrate({
name: 'Bill',
topLevel: 'test top level virtual',
arr: [
{ value: 'FOO', valueLower: 'foo' },
{ value: 'BAR', valueLower: 'bar' }
],
nested: { foo: 'baz', fooRev: 'zab' },
map: {
first: { v: 'ab', vDouble: 'abab' },
second: { v: 'xy', vDouble: 'xyxy' }
},
friendId: userId,
friend: { _id: userId, username: 'Populated Friend' }
}, null, { virtuals: true }); // ensure virtuals:true here

// 1) Top-level virtual
assert.equal(doc.name, 'Bill');
assert.equal(doc.topLevel, 'test top level virtual');
assert.strictEqual(doc.toObject().topLevel, 'test top level virtual');

// 2) Document array virtuals
assert.equal(doc.arr.length, 2);
assert.equal(doc.arr[0].value, 'FOO');
assert.equal(doc.arr[0].valueLower, 'foo');
assert.strictEqual(doc.arr[0].toObject().valueLower, 'foo');
assert.equal(doc.arr[1].value, 'BAR');
assert.equal(doc.arr[1].valueLower, 'bar');
assert.strictEqual(doc.arr[1].toObject().valueLower, 'bar');

// 3) Virtual in subdocument
assert.ok(doc.nested);
assert.equal(doc.nested.foo, 'baz');
assert.equal(doc.nested.fooRev, 'zab');
assert.strictEqual(doc.nested.toObject().fooRev, 'zab');

// 4) Virtual in map of subdocuments
assert.ok(doc.map instanceof Map);
assert.equal(doc.map.get('first').v, 'ab');
assert.equal(doc.map.get('first').vDouble, 'abab');
assert.strictEqual(doc.map.get('first').toObject().vDouble, 'abab');
assert.equal(doc.map.get('second').v, 'xy');
assert.equal(doc.map.get('second').vDouble, 'xyxy');
assert.strictEqual(doc.map.get('second').toObject().vDouble, 'xyxy');

// 5) Top-level populated virtual
assert.equal(doc.friendId.toString(), userId.toString());
assert.ok(doc.friend);
assert.equal(doc.friend._id.toString(), userId.toString());
assert.equal(doc.friend.username, 'Populated Friend');

assert.throws(
() => TestModel.hydrate({}, null, { virtuals: true, hydratedPopulatedDocs: false }),
/Cannot set `hydratedPopulatedDocs` option to false if `virtuals` option is truthy/
);
});

it('sets index collation based on schema collation (gh-7621)', async function() {
let testSchema = new Schema(
{ name: { type: String, index: true } }
Expand Down
1 change: 1 addition & 0 deletions types/models.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ declare module 'mongoose' {
interface HydrateOptions {
setters?: boolean;
hydratedPopulatedDocs?: boolean;
virtuals?: boolean;
}

interface InsertManyOptions extends
Expand Down