Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

8.8 #14996

Merged
merged 26 commits into from
Oct 31, 2024
Merged

8.8 #14996

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
8d66fa9
feat(model): add applyTimestamps() function to apply all schema times…
vkarpov15 Oct 8, 2024
da75251
Update lib/helpers/document/applyTimestamps.js
vkarpov15 Oct 9, 2024
c52041b
fix code review comments
vkarpov15 Oct 9, 2024
cce41a5
Merge branch 'vkarpov15/gh-14698-2' of github.com:Automattic/mongoose…
vkarpov15 Oct 9, 2024
cb544ed
feat: allow defining virtuals on arrays, not just array elements
vkarpov15 Oct 11, 2024
84fe02c
Merge pull request #14943 from Automattic/vkarpov15/gh-14698-2
vkarpov15 Oct 13, 2024
3068872
Merge branch 'master' into 8.8
vkarpov15 Oct 15, 2024
c888efd
Merge pull request #14955 from Automattic/vkarpov15/gh-2326
vkarpov15 Oct 15, 2024
e12301b
Merge branch 'master' into 8.8
vkarpov15 Oct 26, 2024
6076d1f
fix(query): make sanitizeFilter disable implicit $in
vkarpov15 Oct 26, 2024
a5fedd3
feat(query): add schemaLevelProjections option to query to disable sc…
vkarpov15 Oct 27, 2024
7798bad
types: add schemaLevelProjections to types re: #11474
vkarpov15 Oct 27, 2024
a11cbbc
feat(model): add hideIndexes option to syncIndexes() and cleanIndexes()
vkarpov15 Oct 28, 2024
100e896
docs: make quick note about version compatibility
vkarpov15 Oct 28, 2024
ca8b52d
Update lib/model.js
vkarpov15 Oct 28, 2024
a60e72d
Merge pull request #14987 from Automattic/vkarpov15/gh-14868
vkarpov15 Oct 28, 2024
6a7d98e
docs: improve @see link for SchemaLevelProjections re: #11474
vkarpov15 Oct 28, 2024
429f855
Merge pull request #14986 from Automattic/vkarpov15/gh-11474
vkarpov15 Oct 28, 2024
663f21e
Merge pull request #14985 from Automattic/vkarpov15/gh-14657
vkarpov15 Oct 28, 2024
f1607b0
types: added toJSON:flattenObjectIds effect
OguzBey Oct 28, 2024
bfc226c
types: add __v to lean() result type and ModifyResult
vkarpov15 Oct 28, 2024
de3da48
feat: upgrade mongodb -> ~6.10
vkarpov15 Oct 28, 2024
e3a6bdd
Merge pull request #14991 from Automattic/vkarpov15/mongodb-6.10
vkarpov15 Oct 29, 2024
95e182a
Merge pull request #14990 from Automattic/vkarpov15/gh-12959-versionkey
vkarpov15 Oct 29, 2024
38ee9bf
Merge branch 'master' into 8.8
vkarpov15 Oct 29, 2024
6d1eda7
Merge pull request #14989 from OguzBey/tojson-type-improve
vkarpov15 Oct 29, 2024
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
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;
}
}
64 changes: 56 additions & 8 deletions lib/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,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 +1220,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 +1441,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 +1455,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 @@ -3540,6 +3553,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
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
Loading