Skip to content

Commit

Permalink
Merge pull request #14260 from Automattic/8.1
Browse files Browse the repository at this point in the history
8.1
  • Loading branch information
vkarpov15 authored Jan 16, 2024
2 parents 1eaa2d6 + fd80ad3 commit 3165a97
Show file tree
Hide file tree
Showing 18 changed files with 376 additions and 129 deletions.
2 changes: 0 additions & 2 deletions .github/workflows/tsd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ on:
- 'types/**'
- 'test/types/**'
push:
branches:
- master
paths:
- '.github/workflows/tsd.yml'
- 'package.json'
Expand Down
20 changes: 20 additions & 0 deletions docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,7 @@ Valid options:
* [collectionOptions](#collectionOptions)
* [methods](#methods)
* [query](#query-helpers)
* [autoSearchIndex](#autoSearchIndex)

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

Expand Down Expand Up @@ -1453,6 +1454,25 @@ const Test = mongoose.model('Test', schema);
await Test.createCollection();
```

<h2 id="autoSearchIndex">
<a href="#autoSearchIndex">
option: autoSearchIndex
</a>
</h2>

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.
Unlike `autoIndex`, this option defaults to false.

```javascript
const schema = new Schema({ name: String }, { autoSearchIndex: true });
schema.searchIndex({
name: 'my-index',
definition: { mappings: { dynamic: true } }
});
// Will automatically attempt to create the `my-index` search index.
const Test = mongoose.model('Test', schema);
``

<h2 id="es6-classes"><a href="#es6-classes">With ES6 Classes</a></h2>

Schemas have a [`loadClass()` method](api/schema.html#schema_Schema-loadClass)
Expand Down
23 changes: 22 additions & 1 deletion lib/connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,26 @@ Connection.prototype.dropCollection = async function dropCollection(collection)
return this.db.dropCollection(collection);
};

/**
* Helper for MongoDB Node driver's `listCollections()`.
* Returns an array of collection objects.
*
* @method listCollections
* @return {Promise<Collection[]>}
* @api public
*/

Connection.prototype.listCollections = async function listCollections() {
if ((this.readyState === STATES.connecting || this.readyState === STATES.disconnected) && this._shouldBufferCommands()) {
await new Promise(resolve => {
this._queue.push({ fn: resolve });
});
}

const cursor = this.db.listCollections();
return await cursor.toArray();
};

/**
* Helper for `dropDatabase()`. Deletes the given database, including all
* collections, documents, and indexes.
Expand Down Expand Up @@ -983,7 +1003,8 @@ Connection.prototype.onClose = function(force) {
Connection.prototype.collection = function(name, options) {
const defaultOptions = {
autoIndex: this.config.autoIndex != null ? this.config.autoIndex : this.base.options.autoIndex,
autoCreate: this.config.autoCreate != null ? this.config.autoCreate : this.base.options.autoCreate
autoCreate: this.config.autoCreate != null ? this.config.autoCreate : this.base.options.autoCreate,
autoSearchIndex: this.config.autoSearchIndex != null ? this.config.autoSearchIndex : this.base.options.autoSearchIndex
};
options = Object.assign({}, defaultOptions, options ? clone(options) : {});
options.$wasForceClosed = this.$wasForceClosed;
Expand Down
5 changes: 5 additions & 0 deletions lib/drivers/node-mongodb-native/connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,11 @@ NativeConnection.prototype.createClient = async function createClient(uri, optio
delete options.sanitizeFilter;
}

if ('autoSearchIndex' in options) {
this.config.autoSearchIndex = options.autoSearchIndex;
delete options.autoSearchIndex;
}

// Backwards compat
if (options.user || options.pass) {
options.auth = options.auth || {};
Expand Down
97 changes: 93 additions & 4 deletions lib/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -1273,10 +1273,14 @@ for (const i in EventEmitter.prototype) {
}

/**
* This function is responsible for building [indexes](https://www.mongodb.com/docs/manual/indexes/),
* unless [`autoIndex`](https://mongoosejs.com/docs/guide.html#autoIndex) is turned off.
* This function is responsible for initializing the underlying connection in MongoDB based on schema options.
* This function performs the following operations:
*
* Mongoose calls this function automatically when a model is created using
* - `createCollection()` unless [`autoCreate`](https://mongoosejs.com/docs/guide.html#autoCreate) option is turned off
* - `ensureIndexes()` unless [`autoIndex`](https://mongoosejs.com/docs/guide.html#autoIndex) option is turned off
* - `createSearchIndex()` on all schema search indexes if `autoSearchIndex` is enabled.
*
* Mongoose calls this function automatically when a model is a created using
* [`mongoose.model()`](https://mongoosejs.com/docs/api/mongoose.html#Mongoose.prototype.model()) or
* [`connection.model()`](https://mongoosejs.com/docs/api/connection.html#Connection.prototype.model()), so you
* don't need to call `init()` to trigger index builds.
Expand Down Expand Up @@ -1324,6 +1328,23 @@ Model.init = function init() {
}
return await this.ensureIndexes({ _automatic: true });
};
const _createSearchIndexes = async() => {
const autoSearchIndex = utils.getOption(
'autoSearchIndex',
this.schema.options,
conn.config,
conn.base.options
);
if (!autoSearchIndex) {
return;
}

const results = [];
for (const searchIndex of this.schema._searchIndexes) {
results.push(await this.createSearchIndex(searchIndex));
}
return results;
};
const _createCollection = async() => {
if ((conn.readyState === STATES.connecting || conn.readyState === STATES.disconnected) && conn._shouldBufferCommands()) {
await new Promise(resolve => {
Expand All @@ -1342,7 +1363,9 @@ Model.init = function init() {
return await this.createCollection();
};

this.$init = _createCollection().then(() => _ensureIndexes());
this.$init = _createCollection().
then(() => _ensureIndexes()).
then(() => _createSearchIndexes());

const _catch = this.$init.catch;
const _this = this;
Expand Down Expand Up @@ -1506,6 +1529,72 @@ Model.syncIndexes = async function syncIndexes(options) {
return dropped;
};

/**
* Create an [Atlas search index](https://www.mongodb.com/docs/atlas/atlas-search/create-index/).
* This function only works when connected to MongoDB Atlas.
*
* #### Example:
*
* const schema = new Schema({ name: { type: String, unique: true } });
* const Customer = mongoose.model('Customer', schema);
* await Customer.createSearchIndex({ name: 'test', definition: { mappings: { dynamic: true } } });
*
* @param {Object} description index options, including `name` and `definition`
* @param {String} description.name
* @param {Object} description.definition
* @return {Promise}
* @api public
*/

Model.createSearchIndex = async function createSearchIndex(description) {
_checkContext(this, 'createSearchIndex');

return await this.$__collection.createSearchIndex(description);
};

/**
* Update an existing [Atlas search index](https://www.mongodb.com/docs/atlas/atlas-search/create-index/).
* This function only works when connected to MongoDB Atlas.
*
* #### Example:
*
* const schema = new Schema({ name: { type: String, unique: true } });
* const Customer = mongoose.model('Customer', schema);
* await Customer.updateSearchIndex('test', { mappings: { dynamic: true } });
*
* @param {String} name
* @param {Object} definition
* @return {Promise}
* @api public
*/

Model.updateSearchIndex = async function updateSearchIndex(name, definition) {
_checkContext(this, 'updateSearchIndex');

return await this.$__collection.updateSearchIndex(name, definition);
};

/**
* Delete an existing [Atlas search index](https://www.mongodb.com/docs/atlas/atlas-search/create-index/) by name.
* This function only works when connected to MongoDB Atlas.
*
* #### Example:
*
* const schema = new Schema({ name: { type: String, unique: true } });
* const Customer = mongoose.model('Customer', schema);
* await Customer.dropSearchIndex('test');
*
* @param {String} name
* @return {Promise}
* @api public
*/

Model.dropSearchIndex = async function dropSearchIndex(name) {
_checkContext(this, 'dropSearchIndex');

return await this.$__collection.dropSearchIndex(name);
};

/**
* Does a dry-run of `Model.syncIndexes()`, returning the indexes that `syncIndexes()` would drop and create if you were to run `syncIndexes()`.
*
Expand Down
3 changes: 2 additions & 1 deletion lib/mongoose.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ function Mongoose(options) {
this.options = Object.assign({
pluralization: true,
autoIndex: true,
autoCreate: true
autoCreate: true,
autoSearchIndex: false
}, options);
const createInitialConnection = utils.getOption('createInitialConnection', this.options);
if (createInitialConnection == null || createInitialConnection) {
Expand Down
24 changes: 24 additions & 0 deletions lib/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ function Schema(obj, options) {
this.inherits = {};
this.callQueue = [];
this._indexes = [];
this._searchIndexes = [];
this.methods = (options && options.methods) || {};
this.methodOptions = {};
this.statics = (options && options.statics) || {};
Expand Down Expand Up @@ -411,6 +412,7 @@ Schema.prototype._clone = function _clone(Constructor) {
s.query = clone(this.query);
s.plugins = Array.prototype.slice.call(this.plugins);
s._indexes = clone(this._indexes);
s._searchIndexes = clone(this._searchIndexes);
s.s.hooks = this.s.hooks.clone();

s.tree = clone(this.tree);
Expand Down Expand Up @@ -908,6 +910,28 @@ Schema.prototype.clearIndexes = function clearIndexes() {
return this;
};

/**
* Add an [Atlas search index](https://www.mongodb.com/docs/atlas/atlas-search/create-index/) that Mongoose will create using `Model.createSearchIndex()`.
* This function only works when connected to MongoDB Atlas.
*
* #### Example:
*
* const ToySchema = new Schema({ name: String, color: String, price: Number });
* ToySchema.searchIndex({ name: 'test', definition: { mappings: { dynamic: true } } });
*
* @param {Object} description index options, including `name` and `definition`
* @param {String} description.name
* @param {Object} description.definition
* @return {Schema} the Schema instance
* @api public
*/

Schema.prototype.searchIndex = function searchIndex(description) {
this._searchIndexes.push(description);

return this;
};

/**
* Reserved document keys.
*
Expand Down
4 changes: 3 additions & 1 deletion lib/schemaType.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ function SchemaType(path, options, instance) {
const defaultOptionsKeys = Object.keys(defaultOptions);

for (const option of defaultOptionsKeys) {
if (defaultOptions.hasOwnProperty(option) && !Object.prototype.hasOwnProperty.call(options, option)) {
if (option === 'validate') {
this.validate(defaultOptions.validate);
} else if (defaultOptions.hasOwnProperty(option) && !Object.prototype.hasOwnProperty.call(options, option)) {
options[option] = defaultOptions[option];
}
}
Expand Down
1 change: 1 addition & 0 deletions lib/validOptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const VALID_OPTIONS = Object.freeze([
'applyPluginsToDiscriminators',
'autoCreate',
'autoIndex',
'autoSearchIndex',
'bufferCommands',
'bufferTimeoutMS',
'cloneSchemas',
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"dependencies": {
"bson": "^6.2.0",
"kareem": "2.5.1",
"mongodb": "6.2.0",
"mongodb": "6.3.0",
"mpath": "0.9.0",
"mquery": "5.0.0",
"ms": "2.1.3",
Expand Down
15 changes: 12 additions & 3 deletions test/connection.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ describe('connections:', function() {
}));
await Model.init();

const res = await conn.db.listCollections().toArray();
const res = await conn.listCollections();
assert.ok(!res.map(c => c.name).includes('gh8814_Conn'));
await conn.close();
});
Expand Down Expand Up @@ -185,16 +185,25 @@ describe('connections:', function() {
size: 1024
});

const collections = await conn.db.listCollections().toArray();
const collections = await conn.listCollections();

const names = collections.map(function(c) { return c.name; });
assert.ok(names.indexOf('gh5712') !== -1);
assert.ok(collections[names.indexOf('gh5712')].options.capped);
await conn.createCollection('gh5712_0');
const collectionsAfterCreation = await conn.db.listCollections().toArray();
const collectionsAfterCreation = await conn.listCollections();
const newCollectionsNames = collectionsAfterCreation.map(function(c) { return c.name; });
assert.ok(newCollectionsNames.indexOf('gh5712') !== -1);
});

it('listCollections()', async function() {
await conn.dropDatabase();
await conn.createCollection('test1176');
await conn.createCollection('test94112');

const collections = await conn.listCollections();
assert.deepStrictEqual(collections.map(coll => coll.name).sort(), ['test1176', 'test94112']);
});
});

it('should allow closing a closed connection', async function() {
Expand Down
31 changes: 31 additions & 0 deletions test/schematype.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,37 @@ describe('schematype', function() {
});
});

it('merges default validators (gh-14070)', function() {
class TestSchemaType extends mongoose.SchemaType {}
TestSchemaType.set('validate', checkIfString);

const schemaType = new TestSchemaType('test-path', {
validate: checkIfLength2
});

assert.equal(schemaType.validators.length, 2);
assert.equal(schemaType.validators[0].validator, checkIfString);
assert.equal(schemaType.validators[1].validator, checkIfLength2);

let err = schemaType.doValidateSync([1, 2]);
assert.ok(err);
assert.equal(err.name, 'ValidatorError');

err = schemaType.doValidateSync('foo');
assert.ok(err);
assert.equal(err.name, 'ValidatorError');

err = schemaType.doValidateSync('ab');
assert.ifError(err);

function checkIfString(v) {
return typeof v === 'string';
}
function checkIfLength2(v) {
return v.length === 2;
}
});

describe('set()', function() {
describe('SchemaType.set()', function() {
it('SchemaType.set, is a function', () => {
Expand Down
4 changes: 4 additions & 0 deletions test/types/connection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ expectType<Connection>(conn.useDb('test', {}));
expectType<Connection>(conn.useDb('test', { noListener: true }));
expectType<Connection>(conn.useDb('test', { useCache: true }));

expectType<Promise<string[]>>(
conn.listCollections().then(collections => collections.map(coll => coll.name))
);

export function autoTypedModelConnection() {
const AutoTypedSchema = autoTypedSchema();
const AutoTypedModel = connection.model('AutoTypeModelConnection', AutoTypedSchema);
Expand Down
Loading

0 comments on commit 3165a97

Please sign in to comment.