mongoose 8.7.2 → 8.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser.umd.js +1 -1
- package/lib/cast.js +2 -1
- package/lib/cursor/aggregationCursor.js +31 -0
- package/lib/cursor/queryCursor.js +33 -0
- package/lib/helpers/document/applyTimestamps.js +105 -0
- package/lib/model.js +80 -9
- package/lib/query.js +45 -1
- package/lib/schema/array.js +35 -0
- package/lib/schema/documentArray.js +1 -1
- package/lib/schema.js +10 -0
- package/lib/types/array/index.js +5 -0
- package/lib/types/documentArray/index.js +6 -1
- package/package.json +2 -2
- package/types/connection.d.ts +6 -0
- package/types/cursor.d.ts +6 -0
- package/types/document.d.ts +9 -2
- package/types/index.d.ts +78 -2
- package/types/indexes.d.ts +2 -1
- package/types/inferschematype.d.ts +8 -6
- package/types/models.d.ts +8 -6
- package/types/query.d.ts +13 -1
package/lib/cast.js
CHANGED
|
@@ -28,6 +28,7 @@ const ALLOWED_GEOWITHIN_GEOJSON_TYPES = ['Polygon', 'MultiPolygon'];
|
|
|
28
28
|
* @param {Object} [options] the query options
|
|
29
29
|
* @param {Boolean|"throw"} [options.strict] Wheter to enable all strict options
|
|
30
30
|
* @param {Boolean|"throw"} [options.strictQuery] Enable strict Queries
|
|
31
|
+
* @param {Boolean} [options.sanitizeFilter] avoid adding implict query selectors ($in)
|
|
31
32
|
* @param {Boolean} [options.upsert]
|
|
32
33
|
* @param {Query} [context] passed to setters
|
|
33
34
|
* @api private
|
|
@@ -372,7 +373,7 @@ module.exports = function cast(schema, obj, options, context) {
|
|
|
372
373
|
|
|
373
374
|
}
|
|
374
375
|
}
|
|
375
|
-
} else if (Array.isArray(val) && ['Buffer', 'Array'].indexOf(schematype.instance) === -1) {
|
|
376
|
+
} else if (Array.isArray(val) && ['Buffer', 'Array'].indexOf(schematype.instance) === -1 && !options.sanitizeFilter) {
|
|
376
377
|
const casted = [];
|
|
377
378
|
const valuesArray = val;
|
|
378
379
|
|
|
@@ -196,6 +196,37 @@ AggregationCursor.prototype.close = async function close() {
|
|
|
196
196
|
this.emit('close');
|
|
197
197
|
};
|
|
198
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Marks this cursor as destroyed. Will stop streaming and subsequent calls to
|
|
201
|
+
* `next()` will error.
|
|
202
|
+
*
|
|
203
|
+
* @return {this}
|
|
204
|
+
* @api private
|
|
205
|
+
* @method _destroy
|
|
206
|
+
*/
|
|
207
|
+
|
|
208
|
+
AggregationCursor.prototype._destroy = function _destroy(_err, callback) {
|
|
209
|
+
let waitForCursor = null;
|
|
210
|
+
if (!this.cursor) {
|
|
211
|
+
waitForCursor = new Promise((resolve) => {
|
|
212
|
+
this.once('cursor', resolve);
|
|
213
|
+
});
|
|
214
|
+
} else {
|
|
215
|
+
waitForCursor = Promise.resolve();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
waitForCursor
|
|
219
|
+
.then(() => this.cursor.close())
|
|
220
|
+
.then(() => {
|
|
221
|
+
this._closed = true;
|
|
222
|
+
callback();
|
|
223
|
+
})
|
|
224
|
+
.catch(error => {
|
|
225
|
+
callback(error);
|
|
226
|
+
});
|
|
227
|
+
return this;
|
|
228
|
+
};
|
|
229
|
+
|
|
199
230
|
/**
|
|
200
231
|
* Get the next document from this cursor. Will return `null` when there are
|
|
201
232
|
* no documents left.
|
|
@@ -238,6 +238,39 @@ QueryCursor.prototype.close = async function close() {
|
|
|
238
238
|
}
|
|
239
239
|
};
|
|
240
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Marks this cursor as destroyed. Will stop streaming and subsequent calls to
|
|
243
|
+
* `next()` will error.
|
|
244
|
+
*
|
|
245
|
+
* @return {this}
|
|
246
|
+
* @api private
|
|
247
|
+
* @method _destroy
|
|
248
|
+
*/
|
|
249
|
+
|
|
250
|
+
QueryCursor.prototype._destroy = function _destroy(_err, callback) {
|
|
251
|
+
let waitForCursor = null;
|
|
252
|
+
if (!this.cursor) {
|
|
253
|
+
waitForCursor = new Promise((resolve) => {
|
|
254
|
+
this.once('cursor', resolve);
|
|
255
|
+
});
|
|
256
|
+
} else {
|
|
257
|
+
waitForCursor = Promise.resolve();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
waitForCursor
|
|
261
|
+
.then(() => {
|
|
262
|
+
this.cursor.close();
|
|
263
|
+
})
|
|
264
|
+
.then(() => {
|
|
265
|
+
this._closed = true;
|
|
266
|
+
callback();
|
|
267
|
+
})
|
|
268
|
+
.catch(error => {
|
|
269
|
+
callback(error);
|
|
270
|
+
});
|
|
271
|
+
return this;
|
|
272
|
+
};
|
|
273
|
+
|
|
241
274
|
/**
|
|
242
275
|
* Rewind this cursor to its uninitialized state. Any options that are present on the cursor will
|
|
243
276
|
* remain in effect. Iterating this cursor will cause new queries to be sent to the server, even
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const handleTimestampOption = require('../schema/handleTimestampOption');
|
|
4
|
+
const mpath = require('mpath');
|
|
5
|
+
|
|
6
|
+
module.exports = applyTimestamps;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Apply a given schema's timestamps to the given POJO
|
|
10
|
+
*
|
|
11
|
+
* @param {Schema} schema
|
|
12
|
+
* @param {Object} obj
|
|
13
|
+
* @param {Object} [options]
|
|
14
|
+
* @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
|
|
15
|
+
* @param {Function} [options.currentTime] if set, Mongoose will call this function to get the current time.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
function applyTimestamps(schema, obj, options) {
|
|
19
|
+
if (obj == null) {
|
|
20
|
+
return obj;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
applyTimestampsToChildren(schema, obj, options);
|
|
24
|
+
return applyTimestampsToDoc(schema, obj, options);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Apply timestamps to any subdocuments
|
|
29
|
+
*
|
|
30
|
+
* @param {Schema} schema subdocument schema
|
|
31
|
+
* @param {Object} res subdocument
|
|
32
|
+
* @param {Object} [options]
|
|
33
|
+
* @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
|
|
34
|
+
* @param {Function} [options.currentTime] if set, Mongoose will call this function to get the current time.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
function applyTimestampsToChildren(schema, res, options) {
|
|
38
|
+
for (const childSchema of schema.childSchemas) {
|
|
39
|
+
const _path = childSchema.model.path;
|
|
40
|
+
const _schema = childSchema.schema;
|
|
41
|
+
if (!_path) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const _obj = mpath.get(_path, res);
|
|
45
|
+
if (_obj == null || (Array.isArray(_obj) && _obj.flat(Infinity).length === 0)) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
applyTimestamps(_schema, _obj, options);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Apply timestamps to a given document. Does not apply timestamps to subdocuments: use `applyTimestampsToChildren` instead
|
|
55
|
+
*
|
|
56
|
+
* @param {Schema} schema
|
|
57
|
+
* @param {Object} obj
|
|
58
|
+
* @param {Object} [options]
|
|
59
|
+
* @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
|
|
60
|
+
* @param {Function} [options.currentTime] if set, Mongoose will call this function to get the current time.
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
function applyTimestampsToDoc(schema, obj, options) {
|
|
64
|
+
if (obj == null || typeof obj !== 'object') {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (Array.isArray(obj)) {
|
|
68
|
+
for (const el of obj) {
|
|
69
|
+
applyTimestampsToDoc(schema, el, options);
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (schema.discriminators && Object.keys(schema.discriminators).length > 0) {
|
|
75
|
+
for (const discriminatorKey of Object.keys(schema.discriminators)) {
|
|
76
|
+
const discriminator = schema.discriminators[discriminatorKey];
|
|
77
|
+
const key = discriminator.discriminatorMapping.key;
|
|
78
|
+
const value = discriminator.discriminatorMapping.value;
|
|
79
|
+
if (obj[key] == value) {
|
|
80
|
+
schema = discriminator;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const createdAt = handleTimestampOption(schema.options.timestamps, 'createdAt');
|
|
87
|
+
const updatedAt = handleTimestampOption(schema.options.timestamps, 'updatedAt');
|
|
88
|
+
const currentTime = options?.currentTime;
|
|
89
|
+
|
|
90
|
+
let ts = null;
|
|
91
|
+
if (currentTime != null) {
|
|
92
|
+
ts = currentTime();
|
|
93
|
+
} else if (schema.base?.now) {
|
|
94
|
+
ts = schema.base.now();
|
|
95
|
+
} else {
|
|
96
|
+
ts = new Date();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (createdAt && obj[createdAt] == null && !options?.isUpdate) {
|
|
100
|
+
obj[createdAt] = ts;
|
|
101
|
+
}
|
|
102
|
+
if (updatedAt) {
|
|
103
|
+
obj[updatedAt] = ts;
|
|
104
|
+
}
|
|
105
|
+
}
|
package/lib/model.js
CHANGED
|
@@ -10,6 +10,7 @@ const Document = require('./document');
|
|
|
10
10
|
const DocumentNotFoundError = require('./error/notFound');
|
|
11
11
|
const EventEmitter = require('events').EventEmitter;
|
|
12
12
|
const Kareem = require('kareem');
|
|
13
|
+
const { MongoBulkWriteError } = require('mongodb');
|
|
13
14
|
const MongooseBulkWriteError = require('./error/bulkWriteError');
|
|
14
15
|
const MongooseError = require('./error/index');
|
|
15
16
|
const ObjectParameterError = require('./error/objectParameter');
|
|
@@ -30,6 +31,7 @@ const applyReadConcern = require('./helpers/schema/applyReadConcern');
|
|
|
30
31
|
const applySchemaCollation = require('./helpers/indexes/applySchemaCollation');
|
|
31
32
|
const applyStaticHooks = require('./helpers/model/applyStaticHooks');
|
|
32
33
|
const applyStatics = require('./helpers/model/applyStatics');
|
|
34
|
+
const applyTimestampsHelper = require('./helpers/document/applyTimestamps');
|
|
33
35
|
const applyWriteConcern = require('./helpers/schema/applyWriteConcern');
|
|
34
36
|
const applyVirtualsHelper = require('./helpers/document/applyVirtuals');
|
|
35
37
|
const assignVals = require('./helpers/populate/assignVals');
|
|
@@ -1219,6 +1221,7 @@ Model.createCollection = async function createCollection(options) {
|
|
|
1219
1221
|
*
|
|
1220
1222
|
* @param {Object} [options] options to pass to `ensureIndexes()`
|
|
1221
1223
|
* @param {Boolean} [options.background=null] if specified, overrides each index's `background` property
|
|
1224
|
+
* @param {Boolean} [options.hideIndexes=false] set to `true` to hide indexes instead of dropping. Requires MongoDB server 4.4 or higher
|
|
1222
1225
|
* @return {Promise}
|
|
1223
1226
|
* @api public
|
|
1224
1227
|
*/
|
|
@@ -1439,8 +1442,10 @@ function getIndexesToDrop(schema, schemaIndexes, dbIndexes) {
|
|
|
1439
1442
|
*
|
|
1440
1443
|
* The returned promise resolves to a list of the dropped indexes' names as an array
|
|
1441
1444
|
*
|
|
1442
|
-
* @param {
|
|
1443
|
-
* @
|
|
1445
|
+
* @param {Object} [options]
|
|
1446
|
+
* @param {Array<String>} [options.toDrop] if specified, contains a list of index names to drop
|
|
1447
|
+
* @param {Boolean} [options.hideIndexes=false] set to `true` to hide indexes instead of dropping. Requires MongoDB server 4.4 or higher
|
|
1448
|
+
* @return {Promise<String>} list of dropped or hidden index names
|
|
1444
1449
|
* @api public
|
|
1445
1450
|
*/
|
|
1446
1451
|
|
|
@@ -1451,23 +1456,32 @@ Model.cleanIndexes = async function cleanIndexes(options) {
|
|
|
1451
1456
|
}
|
|
1452
1457
|
const model = this;
|
|
1453
1458
|
|
|
1454
|
-
const collection = model.$__collection;
|
|
1455
|
-
|
|
1456
1459
|
if (Array.isArray(options && options.toDrop)) {
|
|
1457
|
-
const res = await _dropIndexes(options.toDrop,
|
|
1460
|
+
const res = await _dropIndexes(options.toDrop, model, options);
|
|
1458
1461
|
return res;
|
|
1459
1462
|
}
|
|
1460
1463
|
|
|
1461
1464
|
const res = await model.diffIndexes();
|
|
1462
|
-
return await _dropIndexes(res.toDrop,
|
|
1465
|
+
return await _dropIndexes(res.toDrop, model, options);
|
|
1463
1466
|
};
|
|
1464
1467
|
|
|
1465
|
-
async function _dropIndexes(toDrop,
|
|
1468
|
+
async function _dropIndexes(toDrop, model, options) {
|
|
1466
1469
|
if (toDrop.length === 0) {
|
|
1467
1470
|
return [];
|
|
1468
1471
|
}
|
|
1469
1472
|
|
|
1470
|
-
|
|
1473
|
+
const collection = model.$__collection;
|
|
1474
|
+
if (options && options.hideIndexes) {
|
|
1475
|
+
await Promise.all(toDrop.map(indexName => {
|
|
1476
|
+
return model.db.db.command({
|
|
1477
|
+
collMod: collection.collectionName,
|
|
1478
|
+
index: { name: indexName, hidden: true }
|
|
1479
|
+
});
|
|
1480
|
+
}));
|
|
1481
|
+
} else {
|
|
1482
|
+
await Promise.all(toDrop.map(indexName => collection.dropIndex(indexName)));
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1471
1485
|
return toDrop;
|
|
1472
1486
|
}
|
|
1473
1487
|
|
|
@@ -1653,7 +1667,24 @@ function _ensureIndexes(model, options, callback) {
|
|
|
1653
1667
|
}
|
|
1654
1668
|
}
|
|
1655
1669
|
|
|
1656
|
-
|
|
1670
|
+
// Just in case `createIndex()` throws a sync error
|
|
1671
|
+
let promise = null;
|
|
1672
|
+
try {
|
|
1673
|
+
promise = model.collection.createIndex(indexFields, indexOptions);
|
|
1674
|
+
} catch (err) {
|
|
1675
|
+
if (!indexError) {
|
|
1676
|
+
indexError = err;
|
|
1677
|
+
}
|
|
1678
|
+
if (!model.$caught) {
|
|
1679
|
+
model.emit('error', err);
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
indexSingleDone(err, indexFields, indexOptions);
|
|
1683
|
+
create();
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
promise.then(
|
|
1657
1688
|
name => {
|
|
1658
1689
|
indexSingleDone(null, indexFields, indexOptions, name);
|
|
1659
1690
|
create();
|
|
@@ -3417,6 +3448,11 @@ Model.bulkSave = async function bulkSave(documents, options) {
|
|
|
3417
3448
|
(err) => ({ bulkWriteResult: null, bulkWriteError: err })
|
|
3418
3449
|
);
|
|
3419
3450
|
|
|
3451
|
+
// If not a MongoBulkWriteError, treat this as all documents failed to save.
|
|
3452
|
+
if (bulkWriteError != null && !(bulkWriteError instanceof MongoBulkWriteError)) {
|
|
3453
|
+
throw bulkWriteError;
|
|
3454
|
+
}
|
|
3455
|
+
|
|
3420
3456
|
const matchedCount = bulkWriteResult?.matchedCount ?? 0;
|
|
3421
3457
|
const insertedCount = bulkWriteResult?.insertedCount ?? 0;
|
|
3422
3458
|
if (writeOperations.length > 0 && matchedCount + insertedCount < writeOperations.length && !bulkWriteError) {
|
|
@@ -3540,6 +3576,41 @@ Model.applyVirtuals = function applyVirtuals(obj, virtualsToApply) {
|
|
|
3540
3576
|
return obj;
|
|
3541
3577
|
};
|
|
3542
3578
|
|
|
3579
|
+
/**
|
|
3580
|
+
* Apply this model's timestamps to a given POJO, including subdocument timestamps
|
|
3581
|
+
*
|
|
3582
|
+
* #### Example:
|
|
3583
|
+
*
|
|
3584
|
+
* const userSchema = new Schema({ name: String }, { timestamps: true });
|
|
3585
|
+
* const User = mongoose.model('User', userSchema);
|
|
3586
|
+
*
|
|
3587
|
+
* const obj = { name: 'John' };
|
|
3588
|
+
* User.applyTimestamps(obj);
|
|
3589
|
+
* obj.createdAt; // 2024-06-01T18:00:00.000Z
|
|
3590
|
+
* obj.updatedAt; // 2024-06-01T18:00:00.000Z
|
|
3591
|
+
*
|
|
3592
|
+
* @param {Object} obj object or document to apply virtuals on
|
|
3593
|
+
* @param {Object} [options]
|
|
3594
|
+
* @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
|
|
3595
|
+
* @param {Function} [options.currentTime] if set, Mongoose will call this function to get the current time.
|
|
3596
|
+
* @returns {Object} obj
|
|
3597
|
+
* @api public
|
|
3598
|
+
*/
|
|
3599
|
+
|
|
3600
|
+
Model.applyTimestamps = function applyTimestamps(obj, options) {
|
|
3601
|
+
if (obj == null) {
|
|
3602
|
+
return obj;
|
|
3603
|
+
}
|
|
3604
|
+
// Nothing to do if this is already a hydrated document - it should already have timestamps
|
|
3605
|
+
if (obj.$__ != null) {
|
|
3606
|
+
return obj;
|
|
3607
|
+
}
|
|
3608
|
+
|
|
3609
|
+
applyTimestampsHelper(this.schema, obj, options);
|
|
3610
|
+
|
|
3611
|
+
return obj;
|
|
3612
|
+
};
|
|
3613
|
+
|
|
3543
3614
|
/**
|
|
3544
3615
|
* Cast the given POJO to the model's schema
|
|
3545
3616
|
*
|
package/lib/query.js
CHANGED
|
@@ -1142,6 +1142,38 @@ Query.prototype.select = function select() {
|
|
|
1142
1142
|
throw new TypeError('Invalid select() argument. Must be string or object.');
|
|
1143
1143
|
};
|
|
1144
1144
|
|
|
1145
|
+
/**
|
|
1146
|
+
* Enable or disable schema level projections for this query. Enabled by default.
|
|
1147
|
+
* Set to `false` to include fields with `select: false` in the query result by default.
|
|
1148
|
+
*
|
|
1149
|
+
* #### Example:
|
|
1150
|
+
*
|
|
1151
|
+
* const userSchema = new Schema({
|
|
1152
|
+
* email: { type: String, required: true },
|
|
1153
|
+
* passwordHash: { type: String, select: false, required: true }
|
|
1154
|
+
* });
|
|
1155
|
+
* const UserModel = mongoose.model('User', userSchema);
|
|
1156
|
+
*
|
|
1157
|
+
* const doc = await UserModel.findOne().orFail().schemaLevelProjections(false);
|
|
1158
|
+
*
|
|
1159
|
+
* // Contains password hash, because `schemaLevelProjections()` overrides `select: false`
|
|
1160
|
+
* doc.passwordHash;
|
|
1161
|
+
*
|
|
1162
|
+
* @method schemaLevelProjections
|
|
1163
|
+
* @memberOf Query
|
|
1164
|
+
* @instance
|
|
1165
|
+
* @param {Boolean} value
|
|
1166
|
+
* @return {Query} this
|
|
1167
|
+
* @see SchemaTypeOptions https://mongoosejs.com/docs/schematypes.html#all-schema-types
|
|
1168
|
+
* @api public
|
|
1169
|
+
*/
|
|
1170
|
+
|
|
1171
|
+
Query.prototype.schemaLevelProjections = function schemaLevelProjections(value) {
|
|
1172
|
+
this._mongooseOptions.schemaLevelProjections = value;
|
|
1173
|
+
|
|
1174
|
+
return this;
|
|
1175
|
+
};
|
|
1176
|
+
|
|
1145
1177
|
/**
|
|
1146
1178
|
* Sets this query's `sanitizeProjection` option. If set, `sanitizeProjection` does
|
|
1147
1179
|
* two things:
|
|
@@ -1689,6 +1721,10 @@ Query.prototype.setOptions = function(options, overwrite) {
|
|
|
1689
1721
|
this._mongooseOptions.translateAliases = options.translateAliases;
|
|
1690
1722
|
delete options.translateAliases;
|
|
1691
1723
|
}
|
|
1724
|
+
if ('schemaLevelProjections' in options) {
|
|
1725
|
+
this._mongooseOptions.schemaLevelProjections = options.schemaLevelProjections;
|
|
1726
|
+
delete options.schemaLevelProjections;
|
|
1727
|
+
}
|
|
1692
1728
|
|
|
1693
1729
|
if (options.lean == null && this.schema && 'lean' in this.schema.options) {
|
|
1694
1730
|
this._mongooseOptions.lean = this.schema.options.lean;
|
|
@@ -2222,6 +2258,7 @@ Query.prototype._unsetCastError = function _unsetCastError() {
|
|
|
2222
2258
|
* - `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.
|
|
2223
2259
|
* - `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.
|
|
2224
2260
|
* - `nearSphere`: use `$nearSphere` instead of `near()`. See the [`Query.prototype.nearSphere()` docs](https://mongoosejs.com/docs/api/query.html#Query.prototype.nearSphere())
|
|
2261
|
+
* - `schemaLevelProjections`: if `false`, Mongoose will not apply schema-level `select: false` or `select: true` for this query
|
|
2225
2262
|
*
|
|
2226
2263
|
* Mongoose maintains a separate object for internal options because
|
|
2227
2264
|
* Mongoose sends `Query.prototype.options` to the MongoDB server, and the
|
|
@@ -4863,6 +4900,9 @@ Query.prototype.cast = function(model, obj) {
|
|
|
4863
4900
|
opts.strictQuery = this.options.strictQuery;
|
|
4864
4901
|
}
|
|
4865
4902
|
}
|
|
4903
|
+
if ('sanitizeFilter' in this._mongooseOptions) {
|
|
4904
|
+
opts.sanitizeFilter = this._mongooseOptions.sanitizeFilter;
|
|
4905
|
+
}
|
|
4866
4906
|
|
|
4867
4907
|
try {
|
|
4868
4908
|
return cast(model.schema, obj, opts, this);
|
|
@@ -4946,7 +4986,11 @@ Query.prototype._applyPaths = function applyPaths() {
|
|
|
4946
4986
|
sanitizeProjection = this._mongooseOptions.sanitizeProjection;
|
|
4947
4987
|
}
|
|
4948
4988
|
|
|
4949
|
-
|
|
4989
|
+
const schemaLevelProjections = this._mongooseOptions.schemaLevelProjections ?? true;
|
|
4990
|
+
|
|
4991
|
+
if (schemaLevelProjections) {
|
|
4992
|
+
helpers.applyPaths(this._fields, this.model.schema, sanitizeProjection);
|
|
4993
|
+
}
|
|
4950
4994
|
|
|
4951
4995
|
let _selectPopulatedPaths = true;
|
|
4952
4996
|
|
package/lib/schema/array.js
CHANGED
|
@@ -11,9 +11,12 @@ const SchemaArrayOptions = require('../options/schemaArrayOptions');
|
|
|
11
11
|
const SchemaType = require('../schemaType');
|
|
12
12
|
const CastError = SchemaType.CastError;
|
|
13
13
|
const Mixed = require('./mixed');
|
|
14
|
+
const VirtualOptions = require('../options/virtualOptions');
|
|
15
|
+
const VirtualType = require('../virtualType');
|
|
14
16
|
const arrayDepth = require('../helpers/arrayDepth');
|
|
15
17
|
const cast = require('../cast');
|
|
16
18
|
const clone = require('../helpers/clone');
|
|
19
|
+
const getConstructorName = require('../helpers/getConstructorName');
|
|
17
20
|
const isOperator = require('../helpers/query/isOperator');
|
|
18
21
|
const util = require('util');
|
|
19
22
|
const utils = require('../utils');
|
|
@@ -217,6 +220,12 @@ SchemaArray._checkRequired = SchemaType.prototype.checkRequired;
|
|
|
217
220
|
|
|
218
221
|
SchemaArray.checkRequired = SchemaType.checkRequired;
|
|
219
222
|
|
|
223
|
+
/*!
|
|
224
|
+
* Virtuals defined on this array itself.
|
|
225
|
+
*/
|
|
226
|
+
|
|
227
|
+
SchemaArray.prototype.virtuals = null;
|
|
228
|
+
|
|
220
229
|
/**
|
|
221
230
|
* Check if the given value satisfies the `required` validator.
|
|
222
231
|
*
|
|
@@ -575,6 +584,32 @@ SchemaArray.prototype.castForQuery = function($conditional, val, context) {
|
|
|
575
584
|
}
|
|
576
585
|
};
|
|
577
586
|
|
|
587
|
+
/**
|
|
588
|
+
* Add a virtual to this array. Specifically to this array, not the individual elements.
|
|
589
|
+
*
|
|
590
|
+
* @param {String} name
|
|
591
|
+
* @param {Object} [options]
|
|
592
|
+
* @api private
|
|
593
|
+
*/
|
|
594
|
+
|
|
595
|
+
SchemaArray.prototype.virtual = function virtual(name, options) {
|
|
596
|
+
if (name instanceof VirtualType || getConstructorName(name) === 'VirtualType') {
|
|
597
|
+
return this.virtual(name.path, name.options);
|
|
598
|
+
}
|
|
599
|
+
options = new VirtualOptions(options);
|
|
600
|
+
|
|
601
|
+
if (utils.hasUserDefinedProperty(options, ['ref', 'refPath'])) {
|
|
602
|
+
throw new MongooseError('Cannot set populate virtual as a property of an array');
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const virtual = new VirtualType(options, name);
|
|
606
|
+
if (this.virtuals === null) {
|
|
607
|
+
this.virtuals = {};
|
|
608
|
+
}
|
|
609
|
+
this.virtuals[name] = virtual;
|
|
610
|
+
return virtual;
|
|
611
|
+
};
|
|
612
|
+
|
|
578
613
|
function cast$all(val, context) {
|
|
579
614
|
if (!Array.isArray(val)) {
|
|
580
615
|
val = [val];
|
|
@@ -429,7 +429,7 @@ SchemaDocumentArray.prototype.cast = function(value, doc, init, prev, options) {
|
|
|
429
429
|
// We need to create a new array, otherwise change tracking will
|
|
430
430
|
// update the old doc (gh-4449)
|
|
431
431
|
if (!options.skipDocumentArrayCast || utils.isMongooseDocumentArray(value)) {
|
|
432
|
-
value = new MongooseDocumentArray(value, path, doc);
|
|
432
|
+
value = new MongooseDocumentArray(value, path, doc, this);
|
|
433
433
|
}
|
|
434
434
|
|
|
435
435
|
if (prev != null) {
|
package/lib/schema.js
CHANGED
|
@@ -2304,6 +2304,7 @@ Schema.prototype.indexes = function() {
|
|
|
2304
2304
|
* @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()`.
|
|
2305
2305
|
* @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.
|
|
2306
2306
|
* @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.
|
|
2307
|
+
* @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.
|
|
2307
2308
|
* @return {VirtualType}
|
|
2308
2309
|
*/
|
|
2309
2310
|
|
|
@@ -2416,6 +2417,15 @@ Schema.prototype.virtual = function(name, options) {
|
|
|
2416
2417
|
return mem[part];
|
|
2417
2418
|
}, this.tree);
|
|
2418
2419
|
|
|
2420
|
+
if (options && options.applyToArray && parts.length > 1) {
|
|
2421
|
+
const path = this.path(parts.slice(0, -1).join('.'));
|
|
2422
|
+
if (path && path.$isMongooseArray) {
|
|
2423
|
+
return path.virtual(parts[parts.length - 1], options);
|
|
2424
|
+
} else {
|
|
2425
|
+
throw new MongooseError(`Path "${path}" is not an array`);
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2419
2429
|
return virtuals[name];
|
|
2420
2430
|
};
|
|
2421
2431
|
|
package/lib/types/array/index.js
CHANGED
|
@@ -90,6 +90,9 @@ function MongooseArray(values, path, doc, schematype) {
|
|
|
90
90
|
if (mongooseArrayMethods.hasOwnProperty(prop)) {
|
|
91
91
|
return mongooseArrayMethods[prop];
|
|
92
92
|
}
|
|
93
|
+
if (schematype && schematype.virtuals && schematype.virtuals.hasOwnProperty(prop)) {
|
|
94
|
+
return schematype.virtuals[prop].applyGetters(undefined, target);
|
|
95
|
+
}
|
|
93
96
|
if (typeof prop === 'string' && numberRE.test(prop) && schematype?.$embeddedSchemaType != null) {
|
|
94
97
|
return schematype.$embeddedSchemaType.applyGetters(__array[prop], doc);
|
|
95
98
|
}
|
|
@@ -101,6 +104,8 @@ function MongooseArray(values, path, doc, schematype) {
|
|
|
101
104
|
mongooseArrayMethods.set.call(proxy, prop, value, false);
|
|
102
105
|
} else if (internals.hasOwnProperty(prop)) {
|
|
103
106
|
internals[prop] = value;
|
|
107
|
+
} else if (schematype && schematype.virtuals && schematype.virtuals.hasOwnProperty(prop)) {
|
|
108
|
+
schematype.virtuals[prop].applySetters(value, target);
|
|
104
109
|
} else {
|
|
105
110
|
__array[prop] = value;
|
|
106
111
|
}
|
|
@@ -28,7 +28,7 @@ const numberRE = /^\d+$/;
|
|
|
28
28
|
* @see https://bit.ly/f6CnZU
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
-
function MongooseDocumentArray(values, path, doc) {
|
|
31
|
+
function MongooseDocumentArray(values, path, doc, schematype) {
|
|
32
32
|
const __array = [];
|
|
33
33
|
|
|
34
34
|
const internals = {
|
|
@@ -84,6 +84,9 @@ function MongooseDocumentArray(values, path, doc) {
|
|
|
84
84
|
if (DocumentArrayMethods.hasOwnProperty(prop)) {
|
|
85
85
|
return DocumentArrayMethods[prop];
|
|
86
86
|
}
|
|
87
|
+
if (schematype && schematype.virtuals && schematype.virtuals.hasOwnProperty(prop)) {
|
|
88
|
+
return schematype.virtuals[prop].applyGetters(undefined, target);
|
|
89
|
+
}
|
|
87
90
|
if (ArrayMethods.hasOwnProperty(prop)) {
|
|
88
91
|
return ArrayMethods[prop];
|
|
89
92
|
}
|
|
@@ -95,6 +98,8 @@ function MongooseDocumentArray(values, path, doc) {
|
|
|
95
98
|
DocumentArrayMethods.set.call(proxy, prop, value, false);
|
|
96
99
|
} else if (internals.hasOwnProperty(prop)) {
|
|
97
100
|
internals[prop] = value;
|
|
101
|
+
} else if (schematype && schematype.virtuals && schematype.virtuals.hasOwnProperty(prop)) {
|
|
102
|
+
schematype.virtuals[prop].applySetters(value, target);
|
|
98
103
|
} else {
|
|
99
104
|
__array[prop] = value;
|
|
100
105
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mongoose",
|
|
3
3
|
"description": "Mongoose MongoDB ODM",
|
|
4
|
-
"version": "8.
|
|
4
|
+
"version": "8.8.0",
|
|
5
5
|
"author": "Guillermo Rauch <guillermo@learnboost.com>",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"mongodb",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"bson": "^6.7.0",
|
|
23
23
|
"kareem": "2.6.3",
|
|
24
|
-
"mongodb": "6.
|
|
24
|
+
"mongodb": "~6.10.0",
|
|
25
25
|
"mpath": "0.9.0",
|
|
26
26
|
"mquery": "5.0.0",
|
|
27
27
|
"ms": "2.1.3",
|
package/types/connection.d.ts
CHANGED
|
@@ -50,6 +50,12 @@ declare module 'mongoose' {
|
|
|
50
50
|
autoIndex?: boolean;
|
|
51
51
|
/** Set to `false` to disable Mongoose automatically calling `createCollection()` on every model created on this connection. */
|
|
52
52
|
autoCreate?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Sanitizes query filters against [query selector injection attacks](
|
|
55
|
+
* https://thecodebarbarian.com/2014/09/04/defending-against-query-selector-injection-attacks.html
|
|
56
|
+
* ) by wrapping any nested objects that have a property whose name starts with $ in a $eq.
|
|
57
|
+
*/
|
|
58
|
+
sanitizeFilter?: boolean;
|
|
53
59
|
}
|
|
54
60
|
|
|
55
61
|
class Connection extends events.EventEmitter implements SessionStarter {
|
package/types/cursor.d.ts
CHANGED
|
@@ -26,6 +26,12 @@ declare module 'mongoose' {
|
|
|
26
26
|
*/
|
|
27
27
|
close(): Promise<void>;
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Destroy this cursor, closing the underlying cursor. Will stop streaming
|
|
31
|
+
* and subsequent calls to `next()` will error.
|
|
32
|
+
*/
|
|
33
|
+
destroy(): this;
|
|
34
|
+
|
|
29
35
|
/**
|
|
30
36
|
* Rewind this cursor to its uninitialized state. Any options that are present on the cursor will
|
|
31
37
|
* remain in effect. Iterating this cursor will cause new queries to be sent to the server, even
|
package/types/document.d.ts
CHANGED
|
@@ -256,10 +256,17 @@ declare module 'mongoose' {
|
|
|
256
256
|
set(value: string | Record<string, any>): this;
|
|
257
257
|
|
|
258
258
|
/** The return value of this method is used in calls to JSON.stringify(doc). */
|
|
259
|
-
toJSON(options?: ToObjectOptions & { flattenMaps?: true }): FlattenMaps<Require_id<DocType>>;
|
|
259
|
+
toJSON(options?: ToObjectOptions & { flattenMaps?: true, flattenObjectIds?: false }): FlattenMaps<Require_id<DocType>>;
|
|
260
|
+
toJSON(options: ToObjectOptions & { flattenObjectIds: false }): FlattenMaps<Require_id<DocType>>;
|
|
261
|
+
toJSON(options: ToObjectOptions & { flattenObjectIds: true }): ObjectIdToString<FlattenMaps<Require_id<DocType>>>;
|
|
260
262
|
toJSON(options: ToObjectOptions & { flattenMaps: false }): Require_id<DocType>;
|
|
261
|
-
toJSON
|
|
263
|
+
toJSON(options: ToObjectOptions & { flattenMaps: false; flattenObjectIds: true }): ObjectIdToString<Require_id<DocType>>;
|
|
264
|
+
|
|
265
|
+
toJSON<T = Require_id<DocType>>(options?: ToObjectOptions & { flattenMaps?: true, flattenObjectIds?: false }): FlattenMaps<T>;
|
|
266
|
+
toJSON<T = Require_id<DocType>>(options: ToObjectOptions & { flattenObjectIds: false }): FlattenMaps<T>;
|
|
267
|
+
toJSON<T = Require_id<DocType>>(options: ToObjectOptions & { flattenObjectIds: true }): ObjectIdToString<FlattenMaps<T>>;
|
|
262
268
|
toJSON<T = Require_id<DocType>>(options: ToObjectOptions & { flattenMaps: false }): T;
|
|
269
|
+
toJSON<T = Require_id<DocType>>(options: ToObjectOptions & { flattenMaps: false; flattenObjectIds: true }): ObjectIdToString<T>;
|
|
263
270
|
|
|
264
271
|
/** Converts this document into a plain-old JavaScript object ([POJO](https://masteringjs.io/tutorials/fundamentals/pojo)). */
|
|
265
272
|
toObject(options?: ToObjectOptions): Require_id<DocType>;
|