Skip to content

Commit

Permalink
Merge branch 'master' into gh-pages
Browse files Browse the repository at this point in the history
  • Loading branch information
vkarpov15 committed Oct 31, 2024
2 parents 81be005 + 10a3aed commit 64833ad
Show file tree
Hide file tree
Showing 25 changed files with 603 additions and 29 deletions.
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
8.8.0 / 2024-10-31
==================
* feat: upgrade mongodb -> ~6.10 #14991 #14877
* feat(query): add schemaLevelProjections option to query to disable schema-level select: false #14986 #11474
* feat: allow defining virtuals on arrays, not just array elements #14955 #2326
* feat(model): add applyTimestamps() function to apply all schema timestamps, including subdocuments, to a given POJO #14943 #14698
* feat(model): add hideIndexes option to syncIndexes() and cleanIndexes() #14987 #14868
* fix(query): make sanitizeFilter disable implicit $in #14985 #14657
* fix(model): avoid unhandled error if createIndex() throws a sync error #14995
* fix(model): avoid throwing TypeError if bulkSave()'s bulkWrite() fails with a non-BulkWriteError #14993
* types: added toJSON:flattenObjectIds effect #14989
* types: add `__v` to lean() result type and ModifyResult #14990 #12959
* types: use globalThis instead of global for NativeDate #14992 #14988
* docs(change-streams): fix markdown syntax highlighting for script output example #14994


8.7.3 / 2024-10-25
==================
* fix(cursor): close underlying query cursor when calling destroy() #14982 #14966
Expand Down
2 changes: 1 addition & 1 deletion docs/change-streams.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ await Person.create({ name: 'Axl Rose' });

The above script will print output that looks like:

```no-highlight
```javascript
{
_id: {
_data: '8262408DAC000000012B022C0100296E5A10042890851837DB4792BE6B235E8B85489F46645F6964006462408DAC6F5C42FF5EE087A20004'
Expand Down
3 changes: 2 additions & 1 deletion lib/cast.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const ALLOWED_GEOWITHIN_GEOJSON_TYPES = ['Polygon', 'MultiPolygon'];
* @param {Object} [options] the query options
* @param {Boolean|"throw"} [options.strict] Wheter to enable all strict options
* @param {Boolean|"throw"} [options.strictQuery] Enable strict Queries
* @param {Boolean} [options.sanitizeFilter] avoid adding implict query selectors ($in)
* @param {Boolean} [options.upsert]
* @param {Query} [context] passed to setters
* @api private
Expand Down Expand Up @@ -372,7 +373,7 @@ module.exports = function cast(schema, obj, options, context) {

}
}
} else if (Array.isArray(val) && ['Buffer', 'Array'].indexOf(schematype.instance) === -1) {
} else if (Array.isArray(val) && ['Buffer', 'Array'].indexOf(schematype.instance) === -1 && !options.sanitizeFilter) {
const casted = [];
const valuesArray = val;

Expand Down
105 changes: 105 additions & 0 deletions lib/helpers/document/applyTimestamps.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
'use strict';

const handleTimestampOption = require('../schema/handleTimestampOption');
const mpath = require('mpath');

module.exports = applyTimestamps;

/**
* Apply a given schema's timestamps to the given POJO
*
* @param {Schema} schema
* @param {Object} obj
* @param {Object} [options]
* @param {Boolean} [options.isUpdate=false] if true, treat this as an update: just set updatedAt, skip setting createdAt. If false, set both createdAt and updatedAt
* @param {Function} [options.currentTime] if set, Mongoose will call this function to get the current time.
*/

function applyTimestamps(schema, obj, options) {
if (obj == null) {
return obj;
}

applyTimestampsToChildren(schema, obj, options);
return applyTimestampsToDoc(schema, obj, options);
}

/**
* Apply timestamps to any subdocuments
*
* @param {Schema} schema subdocument schema
* @param {Object} res subdocument
* @param {Object} [options]
* @param {Boolean} [options.isUpdate=false] if true, treat this as an update: just set updatedAt, skip setting createdAt. If false, set both createdAt and updatedAt
* @param {Function} [options.currentTime] if set, Mongoose will call this function to get the current time.
*/

function applyTimestampsToChildren(schema, res, options) {
for (const childSchema of schema.childSchemas) {
const _path = childSchema.model.path;
const _schema = childSchema.schema;
if (!_path) {
continue;
}
const _obj = mpath.get(_path, res);
if (_obj == null || (Array.isArray(_obj) && _obj.flat(Infinity).length === 0)) {
continue;
}

applyTimestamps(_schema, _obj, options);
}
}

/**
* Apply timestamps to a given document. Does not apply timestamps to subdocuments: use `applyTimestampsToChildren` instead
*
* @param {Schema} schema
* @param {Object} obj
* @param {Object} [options]
* @param {Boolean} [options.isUpdate=false] if true, treat this as an update: just set updatedAt, skip setting createdAt. If false, set both createdAt and updatedAt
* @param {Function} [options.currentTime] if set, Mongoose will call this function to get the current time.
*/

function applyTimestampsToDoc(schema, obj, options) {
if (obj == null || typeof obj !== 'object') {
return;
}
if (Array.isArray(obj)) {
for (const el of obj) {
applyTimestampsToDoc(schema, el, options);
}
return;
}

if (schema.discriminators && Object.keys(schema.discriminators).length > 0) {
for (const discriminatorKey of Object.keys(schema.discriminators)) {
const discriminator = schema.discriminators[discriminatorKey];
const key = discriminator.discriminatorMapping.key;
const value = discriminator.discriminatorMapping.value;
if (obj[key] == value) {
schema = discriminator;
break;
}
}
}

const createdAt = handleTimestampOption(schema.options.timestamps, 'createdAt');
const updatedAt = handleTimestampOption(schema.options.timestamps, 'updatedAt');
const currentTime = options?.currentTime;

let ts = null;
if (currentTime != null) {
ts = currentTime();
} else if (schema.base?.now) {
ts = schema.base.now();
} else {
ts = new Date();
}

if (createdAt && obj[createdAt] == null && !options?.isUpdate) {
obj[createdAt] = ts;
}
if (updatedAt) {
obj[updatedAt] = ts;
}
}
89 changes: 80 additions & 9 deletions lib/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const Document = require('./document');
const DocumentNotFoundError = require('./error/notFound');
const EventEmitter = require('events').EventEmitter;
const Kareem = require('kareem');
const { MongoBulkWriteError } = require('mongodb');
const MongooseBulkWriteError = require('./error/bulkWriteError');
const MongooseError = require('./error/index');
const ObjectParameterError = require('./error/objectParameter');
Expand All @@ -30,6 +31,7 @@ const applyReadConcern = require('./helpers/schema/applyReadConcern');
const applySchemaCollation = require('./helpers/indexes/applySchemaCollation');
const applyStaticHooks = require('./helpers/model/applyStaticHooks');
const applyStatics = require('./helpers/model/applyStatics');
const applyTimestampsHelper = require('./helpers/document/applyTimestamps');
const applyWriteConcern = require('./helpers/schema/applyWriteConcern');
const applyVirtualsHelper = require('./helpers/document/applyVirtuals');
const assignVals = require('./helpers/populate/assignVals');
Expand Down Expand Up @@ -1219,6 +1221,7 @@ Model.createCollection = async function createCollection(options) {
*
* @param {Object} [options] options to pass to `ensureIndexes()`
* @param {Boolean} [options.background=null] if specified, overrides each index's `background` property
* @param {Boolean} [options.hideIndexes=false] set to `true` to hide indexes instead of dropping. Requires MongoDB server 4.4 or higher
* @return {Promise}
* @api public
*/
Expand Down Expand Up @@ -1439,8 +1442,10 @@ function getIndexesToDrop(schema, schemaIndexes, dbIndexes) {
*
* The returned promise resolves to a list of the dropped indexes' names as an array
*
* @param {Function} [callback] optional callback
* @return {Promise|undefined} Returns `undefined` if callback is specified, returns a promise if no callback.
* @param {Object} [options]
* @param {Array<String>} [options.toDrop] if specified, contains a list of index names to drop
* @param {Boolean} [options.hideIndexes=false] set to `true` to hide indexes instead of dropping. Requires MongoDB server 4.4 or higher
* @return {Promise<String>} list of dropped or hidden index names
* @api public
*/

Expand All @@ -1451,23 +1456,32 @@ Model.cleanIndexes = async function cleanIndexes(options) {
}
const model = this;

const collection = model.$__collection;

if (Array.isArray(options && options.toDrop)) {
const res = await _dropIndexes(options.toDrop, collection);
const res = await _dropIndexes(options.toDrop, model, options);
return res;
}

const res = await model.diffIndexes();
return await _dropIndexes(res.toDrop, collection);
return await _dropIndexes(res.toDrop, model, options);
};

async function _dropIndexes(toDrop, collection) {
async function _dropIndexes(toDrop, model, options) {
if (toDrop.length === 0) {
return [];
}

await Promise.all(toDrop.map(indexName => collection.dropIndex(indexName)));
const collection = model.$__collection;
if (options && options.hideIndexes) {
await Promise.all(toDrop.map(indexName => {
return model.db.db.command({
collMod: collection.collectionName,
index: { name: indexName, hidden: true }
});
}));
} else {
await Promise.all(toDrop.map(indexName => collection.dropIndex(indexName)));
}

return toDrop;
}

Expand Down Expand Up @@ -1653,7 +1667,24 @@ function _ensureIndexes(model, options, callback) {
}
}

model.collection.createIndex(indexFields, indexOptions).then(
// Just in case `createIndex()` throws a sync error
let promise = null;
try {
promise = model.collection.createIndex(indexFields, indexOptions);
} catch (err) {
if (!indexError) {
indexError = err;
}
if (!model.$caught) {
model.emit('error', err);
}

indexSingleDone(err, indexFields, indexOptions);
create();
return;
}

promise.then(
name => {
indexSingleDone(null, indexFields, indexOptions, name);
create();
Expand Down Expand Up @@ -3417,6 +3448,11 @@ Model.bulkSave = async function bulkSave(documents, options) {
(err) => ({ bulkWriteResult: null, bulkWriteError: err })
);

// If not a MongoBulkWriteError, treat this as all documents failed to save.
if (bulkWriteError != null && !(bulkWriteError instanceof MongoBulkWriteError)) {
throw bulkWriteError;
}

const matchedCount = bulkWriteResult?.matchedCount ?? 0;
const insertedCount = bulkWriteResult?.insertedCount ?? 0;
if (writeOperations.length > 0 && matchedCount + insertedCount < writeOperations.length && !bulkWriteError) {
Expand Down Expand Up @@ -3540,6 +3576,41 @@ Model.applyVirtuals = function applyVirtuals(obj, virtualsToApply) {
return obj;
};

/**
* Apply this model's timestamps to a given POJO, including subdocument timestamps
*
* #### Example:
*
* const userSchema = new Schema({ name: String }, { timestamps: true });
* const User = mongoose.model('User', userSchema);
*
* const obj = { name: 'John' };
* User.applyTimestamps(obj);
* obj.createdAt; // 2024-06-01T18:00:00.000Z
* obj.updatedAt; // 2024-06-01T18:00:00.000Z
*
* @param {Object} obj object or document to apply virtuals on
* @param {Object} [options]
* @param {Boolean} [options.isUpdate=false] if true, treat this as an update: just set updatedAt, skip setting createdAt. If false, set both createdAt and updatedAt
* @param {Function} [options.currentTime] if set, Mongoose will call this function to get the current time.
* @returns {Object} obj
* @api public
*/

Model.applyTimestamps = function applyTimestamps(obj, options) {
if (obj == null) {
return obj;
}
// Nothing to do if this is already a hydrated document - it should already have timestamps
if (obj.$__ != null) {
return obj;
}

applyTimestampsHelper(this.schema, obj, options);

return obj;
};

/**
* Cast the given POJO to the model's schema
*
Expand Down
46 changes: 45 additions & 1 deletion lib/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -1142,6 +1142,38 @@ Query.prototype.select = function select() {
throw new TypeError('Invalid select() argument. Must be string or object.');
};

/**
* Enable or disable schema level projections for this query. Enabled by default.
* Set to `false` to include fields with `select: false` in the query result by default.
*
* #### Example:
*
* const userSchema = new Schema({
* email: { type: String, required: true },
* passwordHash: { type: String, select: false, required: true }
* });
* const UserModel = mongoose.model('User', userSchema);
*
* const doc = await UserModel.findOne().orFail().schemaLevelProjections(false);
*
* // Contains password hash, because `schemaLevelProjections()` overrides `select: false`
* doc.passwordHash;
*
* @method schemaLevelProjections
* @memberOf Query
* @instance
* @param {Boolean} value
* @return {Query} this
* @see SchemaTypeOptions https://mongoosejs.com/docs/schematypes.html#all-schema-types
* @api public
*/

Query.prototype.schemaLevelProjections = function schemaLevelProjections(value) {
this._mongooseOptions.schemaLevelProjections = value;

return this;
};

/**
* Sets this query's `sanitizeProjection` option. If set, `sanitizeProjection` does
* two things:
Expand Down Expand Up @@ -1689,6 +1721,10 @@ Query.prototype.setOptions = function(options, overwrite) {
this._mongooseOptions.translateAliases = options.translateAliases;
delete options.translateAliases;
}
if ('schemaLevelProjections' in options) {
this._mongooseOptions.schemaLevelProjections = options.schemaLevelProjections;
delete options.schemaLevelProjections;
}

if (options.lean == null && this.schema && 'lean' in this.schema.options) {
this._mongooseOptions.lean = this.schema.options.lean;
Expand Down Expand Up @@ -2222,6 +2258,7 @@ Query.prototype._unsetCastError = function _unsetCastError() {
* - `strict`: controls how Mongoose handles keys that aren't in the schema for updates. This option is `true` by default, which means Mongoose will silently strip any paths in the update that aren't in the schema. See the [`strict` mode docs](https://mongoosejs.com/docs/guide.html#strict) for more information.
* - `strictQuery`: controls how Mongoose handles keys that aren't in the schema for the query `filter`. This option is `false` by default, which means Mongoose will allow `Model.find({ foo: 'bar' })` even if `foo` is not in the schema. See the [`strictQuery` docs](https://mongoosejs.com/docs/guide.html#strictQuery) for more information.
* - `nearSphere`: use `$nearSphere` instead of `near()`. See the [`Query.prototype.nearSphere()` docs](https://mongoosejs.com/docs/api/query.html#Query.prototype.nearSphere())
* - `schemaLevelProjections`: if `false`, Mongoose will not apply schema-level `select: false` or `select: true` for this query
*
* Mongoose maintains a separate object for internal options because
* Mongoose sends `Query.prototype.options` to the MongoDB server, and the
Expand Down Expand Up @@ -4863,6 +4900,9 @@ Query.prototype.cast = function(model, obj) {
opts.strictQuery = this.options.strictQuery;
}
}
if ('sanitizeFilter' in this._mongooseOptions) {
opts.sanitizeFilter = this._mongooseOptions.sanitizeFilter;
}

try {
return cast(model.schema, obj, opts, this);
Expand Down Expand Up @@ -4946,7 +4986,11 @@ Query.prototype._applyPaths = function applyPaths() {
sanitizeProjection = this._mongooseOptions.sanitizeProjection;
}

helpers.applyPaths(this._fields, this.model.schema, sanitizeProjection);
const schemaLevelProjections = this._mongooseOptions.schemaLevelProjections ?? true;

if (schemaLevelProjections) {
helpers.applyPaths(this._fields, this.model.schema, sanitizeProjection);
}

let _selectPopulatedPaths = true;

Expand Down
Loading

0 comments on commit 64833ad

Please sign in to comment.