Skip to content

Commit 3165a97

Browse files
authored
Merge pull request #14260 from Automattic/8.1
8.1
2 parents 1eaa2d6 + fd80ad3 commit 3165a97

18 files changed

+376
-129
lines changed

.github/workflows/tsd.yml

-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ on:
77
- 'types/**'
88
- 'test/types/**'
99
push:
10-
branches:
11-
- master
1210
paths:
1311
- '.github/workflows/tsd.yml'
1412
- 'package.json'

docs/guide.md

+20
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,7 @@ Valid options:
558558
* [collectionOptions](#collectionOptions)
559559
* [methods](#methods)
560560
* [query](#query-helpers)
561+
* [autoSearchIndex](#autoSearchIndex)
561562

562563
<h2 id="autoIndex"><a href="#autoIndex">option: autoIndex</a></h2>
563564

@@ -1453,6 +1454,25 @@ const Test = mongoose.model('Test', schema);
14531454
await Test.createCollection();
14541455
```
14551456

1457+
<h2 id="autoSearchIndex">
1458+
<a href="#autoSearchIndex">
1459+
option: autoSearchIndex
1460+
</a>
1461+
</h2>
1462+
1463+
Similar to [`autoIndex`](#autoIndex), except for automatically creates any [Atlas search indexes](https://www.mongodb.com/docs/atlas/atlas-search/create-index/) defined in your schema.
1464+
Unlike `autoIndex`, this option defaults to false.
1465+
1466+
```javascript
1467+
const schema = new Schema({ name: String }, { autoSearchIndex: true });
1468+
schema.searchIndex({
1469+
name: 'my-index',
1470+
definition: { mappings: { dynamic: true } }
1471+
});
1472+
// Will automatically attempt to create the `my-index` search index.
1473+
const Test = mongoose.model('Test', schema);
1474+
``
1475+
14561476
<h2 id="es6-classes"><a href="#es6-classes">With ES6 Classes</a></h2>
14571477

14581478
Schemas have a [`loadClass()` method](api/schema.html#schema_Schema-loadClass)

lib/connection.js

+22-1
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,26 @@ Connection.prototype.dropCollection = async function dropCollection(collection)
605605
return this.db.dropCollection(collection);
606606
};
607607

608+
/**
609+
* Helper for MongoDB Node driver's `listCollections()`.
610+
* Returns an array of collection objects.
611+
*
612+
* @method listCollections
613+
* @return {Promise<Collection[]>}
614+
* @api public
615+
*/
616+
617+
Connection.prototype.listCollections = async function listCollections() {
618+
if ((this.readyState === STATES.connecting || this.readyState === STATES.disconnected) && this._shouldBufferCommands()) {
619+
await new Promise(resolve => {
620+
this._queue.push({ fn: resolve });
621+
});
622+
}
623+
624+
const cursor = this.db.listCollections();
625+
return await cursor.toArray();
626+
};
627+
608628
/**
609629
* Helper for `dropDatabase()`. Deletes the given database, including all
610630
* collections, documents, and indexes.
@@ -983,7 +1003,8 @@ Connection.prototype.onClose = function(force) {
9831003
Connection.prototype.collection = function(name, options) {
9841004
const defaultOptions = {
9851005
autoIndex: this.config.autoIndex != null ? this.config.autoIndex : this.base.options.autoIndex,
986-
autoCreate: this.config.autoCreate != null ? this.config.autoCreate : this.base.options.autoCreate
1006+
autoCreate: this.config.autoCreate != null ? this.config.autoCreate : this.base.options.autoCreate,
1007+
autoSearchIndex: this.config.autoSearchIndex != null ? this.config.autoSearchIndex : this.base.options.autoSearchIndex
9871008
};
9881009
options = Object.assign({}, defaultOptions, options ? clone(options) : {});
9891010
options.$wasForceClosed = this.$wasForceClosed;

lib/drivers/node-mongodb-native/connection.js

+5
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,11 @@ NativeConnection.prototype.createClient = async function createClient(uri, optio
246246
delete options.sanitizeFilter;
247247
}
248248

249+
if ('autoSearchIndex' in options) {
250+
this.config.autoSearchIndex = options.autoSearchIndex;
251+
delete options.autoSearchIndex;
252+
}
253+
249254
// Backwards compat
250255
if (options.user || options.pass) {
251256
options.auth = options.auth || {};

lib/model.js

+93-4
Original file line numberDiff line numberDiff line change
@@ -1273,10 +1273,14 @@ for (const i in EventEmitter.prototype) {
12731273
}
12741274

12751275
/**
1276-
* This function is responsible for building [indexes](https://www.mongodb.com/docs/manual/indexes/),
1277-
* unless [`autoIndex`](https://mongoosejs.com/docs/guide.html#autoIndex) is turned off.
1276+
* This function is responsible for initializing the underlying connection in MongoDB based on schema options.
1277+
* This function performs the following operations:
12781278
*
1279-
* Mongoose calls this function automatically when a model is created using
1279+
* - `createCollection()` unless [`autoCreate`](https://mongoosejs.com/docs/guide.html#autoCreate) option is turned off
1280+
* - `ensureIndexes()` unless [`autoIndex`](https://mongoosejs.com/docs/guide.html#autoIndex) option is turned off
1281+
* - `createSearchIndex()` on all schema search indexes if `autoSearchIndex` is enabled.
1282+
*
1283+
* Mongoose calls this function automatically when a model is a created using
12801284
* [`mongoose.model()`](https://mongoosejs.com/docs/api/mongoose.html#Mongoose.prototype.model()) or
12811285
* [`connection.model()`](https://mongoosejs.com/docs/api/connection.html#Connection.prototype.model()), so you
12821286
* don't need to call `init()` to trigger index builds.
@@ -1324,6 +1328,23 @@ Model.init = function init() {
13241328
}
13251329
return await this.ensureIndexes({ _automatic: true });
13261330
};
1331+
const _createSearchIndexes = async() => {
1332+
const autoSearchIndex = utils.getOption(
1333+
'autoSearchIndex',
1334+
this.schema.options,
1335+
conn.config,
1336+
conn.base.options
1337+
);
1338+
if (!autoSearchIndex) {
1339+
return;
1340+
}
1341+
1342+
const results = [];
1343+
for (const searchIndex of this.schema._searchIndexes) {
1344+
results.push(await this.createSearchIndex(searchIndex));
1345+
}
1346+
return results;
1347+
};
13271348
const _createCollection = async() => {
13281349
if ((conn.readyState === STATES.connecting || conn.readyState === STATES.disconnected) && conn._shouldBufferCommands()) {
13291350
await new Promise(resolve => {
@@ -1342,7 +1363,9 @@ Model.init = function init() {
13421363
return await this.createCollection();
13431364
};
13441365

1345-
this.$init = _createCollection().then(() => _ensureIndexes());
1366+
this.$init = _createCollection().
1367+
then(() => _ensureIndexes()).
1368+
then(() => _createSearchIndexes());
13461369

13471370
const _catch = this.$init.catch;
13481371
const _this = this;
@@ -1506,6 +1529,72 @@ Model.syncIndexes = async function syncIndexes(options) {
15061529
return dropped;
15071530
};
15081531

1532+
/**
1533+
* Create an [Atlas search index](https://www.mongodb.com/docs/atlas/atlas-search/create-index/).
1534+
* This function only works when connected to MongoDB Atlas.
1535+
*
1536+
* #### Example:
1537+
*
1538+
* const schema = new Schema({ name: { type: String, unique: true } });
1539+
* const Customer = mongoose.model('Customer', schema);
1540+
* await Customer.createSearchIndex({ name: 'test', definition: { mappings: { dynamic: true } } });
1541+
*
1542+
* @param {Object} description index options, including `name` and `definition`
1543+
* @param {String} description.name
1544+
* @param {Object} description.definition
1545+
* @return {Promise}
1546+
* @api public
1547+
*/
1548+
1549+
Model.createSearchIndex = async function createSearchIndex(description) {
1550+
_checkContext(this, 'createSearchIndex');
1551+
1552+
return await this.$__collection.createSearchIndex(description);
1553+
};
1554+
1555+
/**
1556+
* Update an existing [Atlas search index](https://www.mongodb.com/docs/atlas/atlas-search/create-index/).
1557+
* This function only works when connected to MongoDB Atlas.
1558+
*
1559+
* #### Example:
1560+
*
1561+
* const schema = new Schema({ name: { type: String, unique: true } });
1562+
* const Customer = mongoose.model('Customer', schema);
1563+
* await Customer.updateSearchIndex('test', { mappings: { dynamic: true } });
1564+
*
1565+
* @param {String} name
1566+
* @param {Object} definition
1567+
* @return {Promise}
1568+
* @api public
1569+
*/
1570+
1571+
Model.updateSearchIndex = async function updateSearchIndex(name, definition) {
1572+
_checkContext(this, 'updateSearchIndex');
1573+
1574+
return await this.$__collection.updateSearchIndex(name, definition);
1575+
};
1576+
1577+
/**
1578+
* Delete an existing [Atlas search index](https://www.mongodb.com/docs/atlas/atlas-search/create-index/) by name.
1579+
* This function only works when connected to MongoDB Atlas.
1580+
*
1581+
* #### Example:
1582+
*
1583+
* const schema = new Schema({ name: { type: String, unique: true } });
1584+
* const Customer = mongoose.model('Customer', schema);
1585+
* await Customer.dropSearchIndex('test');
1586+
*
1587+
* @param {String} name
1588+
* @return {Promise}
1589+
* @api public
1590+
*/
1591+
1592+
Model.dropSearchIndex = async function dropSearchIndex(name) {
1593+
_checkContext(this, 'dropSearchIndex');
1594+
1595+
return await this.$__collection.dropSearchIndex(name);
1596+
};
1597+
15091598
/**
15101599
* Does a dry-run of `Model.syncIndexes()`, returning the indexes that `syncIndexes()` would drop and create if you were to run `syncIndexes()`.
15111600
*

lib/mongoose.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ function Mongoose(options) {
6565
this.options = Object.assign({
6666
pluralization: true,
6767
autoIndex: true,
68-
autoCreate: true
68+
autoCreate: true,
69+
autoSearchIndex: false
6970
}, options);
7071
const createInitialConnection = utils.getOption('createInitialConnection', this.options);
7172
if (createInitialConnection == null || createInitialConnection) {

lib/schema.js

+24
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ function Schema(obj, options) {
116116
this.inherits = {};
117117
this.callQueue = [];
118118
this._indexes = [];
119+
this._searchIndexes = [];
119120
this.methods = (options && options.methods) || {};
120121
this.methodOptions = {};
121122
this.statics = (options && options.statics) || {};
@@ -411,6 +412,7 @@ Schema.prototype._clone = function _clone(Constructor) {
411412
s.query = clone(this.query);
412413
s.plugins = Array.prototype.slice.call(this.plugins);
413414
s._indexes = clone(this._indexes);
415+
s._searchIndexes = clone(this._searchIndexes);
414416
s.s.hooks = this.s.hooks.clone();
415417

416418
s.tree = clone(this.tree);
@@ -908,6 +910,28 @@ Schema.prototype.clearIndexes = function clearIndexes() {
908910
return this;
909911
};
910912

913+
/**
914+
* Add an [Atlas search index](https://www.mongodb.com/docs/atlas/atlas-search/create-index/) that Mongoose will create using `Model.createSearchIndex()`.
915+
* This function only works when connected to MongoDB Atlas.
916+
*
917+
* #### Example:
918+
*
919+
* const ToySchema = new Schema({ name: String, color: String, price: Number });
920+
* ToySchema.searchIndex({ name: 'test', definition: { mappings: { dynamic: true } } });
921+
*
922+
* @param {Object} description index options, including `name` and `definition`
923+
* @param {String} description.name
924+
* @param {Object} description.definition
925+
* @return {Schema} the Schema instance
926+
* @api public
927+
*/
928+
929+
Schema.prototype.searchIndex = function searchIndex(description) {
930+
this._searchIndexes.push(description);
931+
932+
return this;
933+
};
934+
911935
/**
912936
* Reserved document keys.
913937
*

lib/schemaType.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ function SchemaType(path, options, instance) {
5959
const defaultOptionsKeys = Object.keys(defaultOptions);
6060

6161
for (const option of defaultOptionsKeys) {
62-
if (defaultOptions.hasOwnProperty(option) && !Object.prototype.hasOwnProperty.call(options, option)) {
62+
if (option === 'validate') {
63+
this.validate(defaultOptions.validate);
64+
} else if (defaultOptions.hasOwnProperty(option) && !Object.prototype.hasOwnProperty.call(options, option)) {
6365
options[option] = defaultOptions[option];
6466
}
6567
}

lib/validOptions.js

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const VALID_OPTIONS = Object.freeze([
1111
'applyPluginsToDiscriminators',
1212
'autoCreate',
1313
'autoIndex',
14+
'autoSearchIndex',
1415
'bufferCommands',
1516
'bufferTimeoutMS',
1617
'cloneSchemas',

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"dependencies": {
2222
"bson": "^6.2.0",
2323
"kareem": "2.5.1",
24-
"mongodb": "6.2.0",
24+
"mongodb": "6.3.0",
2525
"mpath": "0.9.0",
2626
"mquery": "5.0.0",
2727
"ms": "2.1.3",

test/connection.test.js

+12-3
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ describe('connections:', function() {
9494
}));
9595
await Model.init();
9696

97-
const res = await conn.db.listCollections().toArray();
97+
const res = await conn.listCollections();
9898
assert.ok(!res.map(c => c.name).includes('gh8814_Conn'));
9999
await conn.close();
100100
});
@@ -185,16 +185,25 @@ describe('connections:', function() {
185185
size: 1024
186186
});
187187

188-
const collections = await conn.db.listCollections().toArray();
188+
const collections = await conn.listCollections();
189189

190190
const names = collections.map(function(c) { return c.name; });
191191
assert.ok(names.indexOf('gh5712') !== -1);
192192
assert.ok(collections[names.indexOf('gh5712')].options.capped);
193193
await conn.createCollection('gh5712_0');
194-
const collectionsAfterCreation = await conn.db.listCollections().toArray();
194+
const collectionsAfterCreation = await conn.listCollections();
195195
const newCollectionsNames = collectionsAfterCreation.map(function(c) { return c.name; });
196196
assert.ok(newCollectionsNames.indexOf('gh5712') !== -1);
197197
});
198+
199+
it('listCollections()', async function() {
200+
await conn.dropDatabase();
201+
await conn.createCollection('test1176');
202+
await conn.createCollection('test94112');
203+
204+
const collections = await conn.listCollections();
205+
assert.deepStrictEqual(collections.map(coll => coll.name).sort(), ['test1176', 'test94112']);
206+
});
198207
});
199208

200209
it('should allow closing a closed connection', async function() {

test/schematype.test.js

+31
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,37 @@ describe('schematype', function() {
208208
});
209209
});
210210

211+
it('merges default validators (gh-14070)', function() {
212+
class TestSchemaType extends mongoose.SchemaType {}
213+
TestSchemaType.set('validate', checkIfString);
214+
215+
const schemaType = new TestSchemaType('test-path', {
216+
validate: checkIfLength2
217+
});
218+
219+
assert.equal(schemaType.validators.length, 2);
220+
assert.equal(schemaType.validators[0].validator, checkIfString);
221+
assert.equal(schemaType.validators[1].validator, checkIfLength2);
222+
223+
let err = schemaType.doValidateSync([1, 2]);
224+
assert.ok(err);
225+
assert.equal(err.name, 'ValidatorError');
226+
227+
err = schemaType.doValidateSync('foo');
228+
assert.ok(err);
229+
assert.equal(err.name, 'ValidatorError');
230+
231+
err = schemaType.doValidateSync('ab');
232+
assert.ifError(err);
233+
234+
function checkIfString(v) {
235+
return typeof v === 'string';
236+
}
237+
function checkIfLength2(v) {
238+
return v.length === 2;
239+
}
240+
});
241+
211242
describe('set()', function() {
212243
describe('SchemaType.set()', function() {
213244
it('SchemaType.set, is a function', () => {

test/types/connection.test.ts

+4
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ expectType<Connection>(conn.useDb('test', {}));
7070
expectType<Connection>(conn.useDb('test', { noListener: true }));
7171
expectType<Connection>(conn.useDb('test', { useCache: true }));
7272

73+
expectType<Promise<string[]>>(
74+
conn.listCollections().then(collections => collections.map(coll => coll.name))
75+
);
76+
7377
export function autoTypedModelConnection() {
7478
const AutoTypedSchema = autoTypedSchema();
7579
const AutoTypedModel = connection.model('AutoTypeModelConnection', AutoTypedSchema);

0 commit comments

Comments
 (0)