Skip to content

Commit

Permalink
Merge branch '6.4' into gh-11580
Browse files Browse the repository at this point in the history
  • Loading branch information
vkarpov15 committed May 16, 2022
2 parents 5b7348c + c2bd9a9 commit 3d17528
Show file tree
Hide file tree
Showing 9 changed files with 159 additions and 75 deletions.
40 changes: 40 additions & 0 deletions docs/typescript/populate.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,43 @@ ParentModel.findOne({}).populate<Pick<PopulatedParent, 'child'>>('child').orFail
const t: string = doc.child.name;
});
```

## Using `PopulatedDoc`

Mongoose also exports a `PopulatedDoc` type that helps you define populated documents in your document interface:

```ts
import { Schema, model, Document, PopulatedDoc } from 'mongoose';

// `child` is either an ObjectId or a populated document
interface Parent {
child?: PopulatedDoc<Document<ObjectId> & Child>,
name?: string
}
const ParentModel = model<Parent>('Parent', new Schema({
child: { type: 'ObjectId', ref: 'Child' },
name: String
}));

interface Child {
name?: string;
}
const childSchema: Schema = new Schema({ name: String });
const ChildModel = model<Child>('Child', childSchema);

ParentModel.findOne({}).populate('child').orFail().then((doc: Parent) => {
const child = doc.child;
if (child == null || child instanceof ObjectId) {
throw new Error('should be populated');
} else {
// Works
doc.child.name.trim();
}
});
```

However, we recommend using the `.populate<{ child: Child }>` syntax from the first section instead of `PopulatedDoc`.
Here's two reasons why:

1. You still need to add an extra check to check if `child instanceof ObjectId`. Otherwise, the TypeScript compiler will fail with `Property name does not exist on type ObjectId`. So using `PopulatedDoc<>` means you need an extra check everywhere you use `doc.child`.
2. In the `Parent` interface, `child` is a hydrated document, which makes it slow difficult for Mongoose to infer the type of `child` when you use `lean()` or `toObject()`.
4 changes: 4 additions & 0 deletions lib/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -1323,6 +1323,10 @@ Document.prototype.$set = function $set(path, val, type, options) {

if (!schema) {
this.$__set(pathToMark, path, options, constructing, parts, schema, val, priorVal);

if (pathType === 'nested' && val == null) {
cleanModifiedSubpaths(this, path);
}
return this;
}

Expand Down
3 changes: 2 additions & 1 deletion lib/helpers/clone.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ module.exports = clone;

function cloneObject(obj, options, isArrayChild) {
const minimize = options && options.minimize;
const omitUndefined = options && options.omitUndefined;
const ret = {};
let hasKeys;

Expand All @@ -138,7 +139,7 @@ function cloneObject(obj, options, isArrayChild) {
// Don't pass `isArrayChild` down
const val = clone(obj[key], options, false);

if (minimize === false && typeof val === 'undefined') {
if ((minimize === false || omitUndefined) && typeof val === 'undefined') {
delete ret[key];
} else if (minimize !== true || (typeof val !== 'undefined')) {
hasKeys || (hasKeys = true);
Expand Down
92 changes: 38 additions & 54 deletions lib/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,7 @@ Model.prototype.$__delta = function() {
transform: false,
virtuals: false,
getters: false,
omitUndefined: true,
_isNested: true
});
operand(this, where, delta, data, value);
Expand Down Expand Up @@ -1488,61 +1489,68 @@ Model.syncIndexes = function syncIndexes(options, callback) {
* the result of this function would be the result of
* Model.syncIndexes().
*
* @param {Object} options not used at all.
* @param {Object} [options]
* @param {Function} callback optional callback
* @returns {Promise} which containts an object, {toDrop, toCreate}, which
* are indexes that would be dropped in mongodb and indexes that would be created in mongodb.
* @returns {Promise} which contains an object, {toDrop, toCreate}, which
* are indexes that would be dropped in MongoDB and indexes that would be created in MongoDB.
*/

Model.diffIndexes = function diffIndexes(options, callback) {
if (typeof options === 'function') {
callback = options;
options = null;
}
const toDrop = [];
const toCreate = [];
callback = this.$handleCallbackError(callback);
return this.db.base._promiseOrCallback(callback, cb => {
cb = this.$wrapCallback(cb);
this.listIndexes((err, indexes) => {
if (indexes === undefined) {
indexes = [];
this.listIndexes((err, dbIndexes) => {
if (dbIndexes === undefined) {
dbIndexes = [];
}
const schemaIndexes = this.schema.indexes();
// Iterate through the indexes created in mongodb and
// compare against the indexes in the schema.
for (const index of indexes) {
dbIndexes = getRelatedDBIndexes(this, dbIndexes);
const schemaIndexes = getRelatedSchemaIndexes(this, this.schema.indexes());

for (const dbIndex of dbIndexes) {
let found = false;
// Never try to drop `_id` index, MongoDB server doesn't allow it
if (isDefaultIdIndex(index)) {
if (isDefaultIdIndex(dbIndex)) {
continue;
}

for (const schemaIndex of schemaIndexes) {
const key = schemaIndex[0];
const options = decorateDiscriminatorIndexOptions(this.schema, utils.clone(schemaIndex[1]));
if (isIndexEqual(key, options, index)) {
for (const [schemaIndexKeysObject, schemaIndexOptions] of schemaIndexes) {
const options = decorateDiscriminatorIndexOptions(this.schema, utils.clone(schemaIndexOptions));
applySchemaCollation(schemaIndexKeysObject, options, this.schema.options);

if (isIndexEqual(schemaIndexKeysObject, options, dbIndex)) {
found = true;
}
}

if (!found) {
toDrop.push(index.name);
toDrop.push(dbIndex.name);
}
}
// Iterate through the indexes created on the schema and
// compare against the indexes in mongodb.
for (const schemaIndex of schemaIndexes) {
let found = false;
const key = schemaIndex[0];
const options = decorateDiscriminatorIndexOptions(this.schema, utils.clone(schemaIndex[1]));
for (const index of indexes) {
if (isDefaultIdIndex(index)) {
continue;
if (!options || options.toCreate !== false) {
for (const schemaIndex of schemaIndexes) {
let found = false;
const key = schemaIndex[0];
const options = decorateDiscriminatorIndexOptions(this.schema, utils.clone(schemaIndex[1]));
for (const index of dbIndexes) {
if (isDefaultIdIndex(index)) {
continue;
}
if (isIndexEqual(key, options, index)) {
found = true;
}
}
if (isIndexEqual(key, options, index)) {
found = true;
if (!found) {
toCreate.push(key);
}
}
if (!found) {
toCreate.push(key);
}
}
cb(null, { toDrop, toCreate });
});
Expand All @@ -1568,36 +1576,12 @@ Model.cleanIndexes = function cleanIndexes(callback) {
return this.db.base._promiseOrCallback(callback, cb => {
const collection = this.$__collection;

this.listIndexes((err, dbIndexes) => {
this.diffIndexes({ toCreate: false }, (err, res) => {
if (err != null) {
return cb(err);
}

dbIndexes = getRelatedDBIndexes(this, dbIndexes);
const schemaIndexes = getRelatedSchemaIndexes(this, this.schema.indexes());

const toDrop = [];

for (const dbIndex of dbIndexes) {
let found = false;
// Never try to drop `_id` index, MongoDB server doesn't allow it
if (isDefaultIdIndex(dbIndex)) {
continue;
}

for (const [schemaIndexKeysObject, schemaIndexOptions] of schemaIndexes) {
const options = decorateDiscriminatorIndexOptions(this.schema, utils.clone(schemaIndexOptions));
applySchemaCollation(schemaIndexKeysObject, options, this.schema.options);

if (isIndexEqual(schemaIndexKeysObject, options, dbIndex)) {
found = true;
}
}

if (!found) {
toDrop.push(dbIndex.name);
}
}
const toDrop = res.toDrop;

if (toDrop.length === 0) {
return cb(null, []);
Expand Down
9 changes: 9 additions & 0 deletions lib/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -1234,6 +1234,15 @@ function createMapNestedSchemaType(schema, schemaType, path, obj, options) {
_mapType = { [schema.options.typeKey]: obj.of };
}

if (_mapType[schema.options.typeKey] && _mapType[schema.options.typeKey].instanceOfSchema) {
const subdocumentSchema = _mapType[schema.options.typeKey];
subdocumentSchema.eachPath((subpath, type) => {
if (type.options.select === true || type.options.select === false) {
throw new MongooseError('Cannot use schema-level projections (`select: true` or `select: false`) within maps at path "' + path + '.' + subpath + '"');
}
});
}

if (utils.hasUserDefinedProperty(obj, 'ref')) {
_mapType.ref = obj.ref;
}
Expand Down
48 changes: 48 additions & 0 deletions test/document.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11325,4 +11325,52 @@ describe('document', function() {
assert.ok(err.message.includes('failed for path'), err.message);
assert.ok(err.message.includes('value `-1`'), err.message);
});

it('avoids setting nested paths to null when they are set to `undefined` (gh-11723)', async function() {
const nestedSchema = new mongoose.Schema({
count: Number
}, { _id: false });

const mySchema = new mongoose.Schema({
name: String,
nested: { count: Number },
nestedSchema: nestedSchema
}, { minimize: false });

const Test = db.model('Test', mySchema);

const instance1 = new Test({ name: 'test1', nested: { count: 1 }, nestedSchema: { count: 1 } });
await instance1.save();

const update = { nested: { count: undefined }, nestedSchema: { count: undefined } };
instance1.set(update);
await instance1.save();

const doc = await Test.findById(instance1);
assert.strictEqual(doc.nested.count, undefined);
assert.strictEqual(doc.nestedSchema.count, undefined);
});

it('cleans modified subpaths when setting nested path under array to null when subpaths are modified (gh-11764)', async function() {
const Test = db.model('Test', new Schema({
list: [{
quantity: {
value: Number,
unit: String
}
}]
}));

let doc = await Test.create({ list: [{ quantity: { value: 1, unit: 'case' } }] });

doc = await Test.findById(doc);
doc.list[0].quantity.value = null;
doc.list[0].quantity.unit = null;
doc.list[0].quantity = null;

await doc.save();

doc = await Test.findById(doc);
assert.strictEqual(doc.list[0].toObject().quantity, null);
});
});
18 changes: 0 additions & 18 deletions test/model.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8503,24 +8503,6 @@ describe('Model', function() {
assert.deepEqual(indexes[1].key, { name: 1 });
assert.strictEqual(indexes[1].collation.locale, 'en');
});
it('prevents .$* from being put in projections by avoiding drilling down into maps (gh-11698)', async function() {
const subSchema = new Schema({
selected: { type: Number },
not_selected: { type: Number, select: false }
});

const testSchema = new Schema({
subdocument_mapping: {
type: Map,
of: subSchema
}
});


const Test = db.model('Test', testSchema);

assert.ok(await Test.find());
});
});


Expand Down
16 changes: 16 additions & 0 deletions test/schema.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2763,6 +2763,7 @@ describe('schema', function() {
});
assert(batch.message);
});

it('can use on as a schema property (gh-11580)', async() => {
const testSchema = new mongoose.Schema({
on: String
Expand All @@ -2774,6 +2775,21 @@ describe('schema', function() {
const result = await Test.findOne();
assert.ok(result);
assert.ok(result.on);
});

it('disallows using schemas with schema-level projections with map subdocuments (gh-11698)', async function() {
const subSchema = new Schema({
selected: { type: Number },
not_selected: { type: Number, select: false }
});

assert.throws(() => {
new Schema({
subdocument_mapping: {
type: Map,
of: subSchema
}
});
}, /Cannot use schema-level projections.*subdocument_mapping.not_selected/);
});
});
4 changes: 2 additions & 2 deletions test/types/populate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const childSchema: Schema = new Schema({ name: String });
const ChildModel = model<Child>('Child', childSchema);

interface Parent {
child?: PopulatedDoc<Child & Document<ObjectId>>,
child: PopulatedDoc<Document<ObjectId> & Child>,
name?: string
}

Expand All @@ -20,7 +20,7 @@ const ParentModel = model<Parent>('Parent', new Schema({
name: String
}));

ParentModel.findOne({}).populate('child').orFail().then((doc: Parent & Document<ObjectId, {}, Parent>) => {
ParentModel.findOne({}).populate('child').orFail().then((doc: Document<ObjectId, {}, Parent> & Parent) => {
const child = doc.child;
if (child == null || child instanceof ObjectId) {
throw new Error('should be populated');
Expand Down

0 comments on commit 3d17528

Please sign in to comment.