diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 29d4c024659..00000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - -**Do you want to request a *feature* or report a *bug*?** - -**What is the current behavior?** - -**If the current behavior is a bug, please provide the steps to reproduce.** - - - - -**What is the expected behavior?** - -**What are the versions of Node.js, Mongoose and MongoDB you are using? Note that "latest" is not a version.** - - diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 00000000000..3089b69e90e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,62 @@ +name: đŸĒ˛ Bug report +description: Create a report to help us improve + +body: + - type: checkboxes + id: prerequisites + attributes: + label: Prerequisites + options: + - label: I have written a descriptive issue title + required: true + - label: | + I have searched existing issues to ensure the bug has not already been reported + required: true + + - type: input + id: mongoose-version + attributes: + label: Mongoose version + placeholder: 6.x.x + validations: + required: true + + - type: input + id: node-version + attributes: + label: Node.js version + placeholder: 14.x + validations: + required: true + + - type: input + id: mongo-version + attributes: + label: MongoDB server version + placeholder: 5.x + validations: + required: true + + - type: textarea + id: description + attributes: + label: Description + description: A clear and concise description of what the bug is. + validations: + required: true + + - type: textarea + id: steps-to-reproduce + attributes: + label: Steps to Reproduce + description: | + List of steps, sample code, or a link to code or a project that reproduces the behavior. + Make sure you place a stack trace inside a [code (```) block](https://docs.github.com/en/github/writing-on-github/creating-and-highlighting-code-blocks) to avoid linking unrelated issues. + validations: + required: true + + - type: textarea + id: expected-behavior + attributes: + label: Expected Behavior + description: A clear and concise description of what you expected to happen. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000000..d552e213c6f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +documentation: + - name: 📘 Documentation + url: https://mongoosejs.com/docs/guide.html + about: Mongoose Docs diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 00000000000..b2ace34d028 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,37 @@ +name: 🚀 Feature Proposal +description: Submit a proposal for a new feature +labels: ['enhancement', 'new feature'] + +body: + - type: checkboxes + id: prerequisites + attributes: + label: Prerequisites + options: + - label: I have written a descriptive issue title + required: true + - label: | + I have searched existing issues to ensure the feature has not already been requested + required: true + + - type: textarea + id: proposal + attributes: + label: 🚀 Feature Proposal + description: A clear and concise description of what the feature is. + validations: + required: true + + - type: textarea + id: motivation + attributes: + label: Motivation + description: The motivation for the proposal. + + - type: textarea + id: example + attributes: + label: Example + description: | + An example for how this feature would be used. + Make sure you place example code inside a [code (```) block](https://docs.github.com/en/github/writing-on-github/creating-and-highlighting-code-blocks) to avoid linking unrelated issues. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/help.yml b/.github/ISSUE_TEMPLATE/help.yml new file mode 100644 index 00000000000..c9ffe193647 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/help.yml @@ -0,0 +1,67 @@ +name: 🙋 Help +description: Open an issue and request for individual help +labels: ['help', 'help wanted'] + +body: + - type: markdown + attributes: + value: | + Before you submit an issue we recommend to read the [documentation](https://mongoosejs.com/docs/guide.html). + + - type: checkboxes + id: prerequisites + attributes: + label: Prerequisites + options: + - label: I have written a descriptive issue title + required: true + + - type: input + id: mongoose-version + attributes: + label: Mongoose version + placeholder: 6.x.x + validations: + required: true + + - type: input + id: node-version + attributes: + label: Node.js version + placeholder: 14.x + validations: + required: true + + - type: input + id: mongo-version + attributes: + label: MongoDB version + placeholder: 5.x + validations: + required: true + + - type: dropdown + id: os + attributes: + label: Operating system + options: + - Linux + - macOS + - Windows + validations: + required: false + + - type: input + id: os-version + attributes: + label: Operating system version (i.e. 20.04, 11.3, 10) + validations: + required: false + + - type: textarea + id: text + attributes: + label: Issue + description: | + Give as much detail as you can to help us understand. + Make sure you place example code inside a [code (```) block](https://docs.github.com/en/github/writing-on-github/creating-and-highlighting-code-blocks) to avoid linking unrelated issues. diff --git a/.github/ISSUE_TEMPLATE/other.yml b/.github/ISSUE_TEMPLATE/other.yml new file mode 100644 index 00000000000..5579df501c9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/other.yml @@ -0,0 +1,22 @@ +name: ❔ Other +description: Open an issue that is not feature or bug related + +body: + - type: checkboxes + id: prerequisites + attributes: + label: Prerequisites + options: + - label: I have written a descriptive issue title + required: true + - label: | + I have searched existing issues to ensure the issue has not already been raised + required: true + + - type: textarea + id: text + attributes: + label: Issue + description: | + Give as much detail as you can to help us understand. + Make sure you place example code inside a [code (```) block](https://docs.github.com/en/github/writing-on-github/creating-and-highlighting-code-blocks) to avoid linking unrelated issues. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/performance.yml b/.github/ISSUE_TEMPLATE/performance.yml new file mode 100644 index 00000000000..8eef06255b5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/performance.yml @@ -0,0 +1,63 @@ +name: đŸĻĨ Performance issue +description: Report a performance issue +labels: ['performance'] + +body: + - type: checkboxes + id: prerequisites + attributes: + label: Prerequisites + options: + - label: I have written a descriptive issue title + required: true + - label: | + I have searched existing issues to ensure the performance issue has not already been reported + required: true + + - type: input + id: working-version + attributes: + label: Last performant version + placeholder: 1.x.x + validations: + required: true + + - type: input + id: stopped-working-version + attributes: + label: Slowed down in version + placeholder: 2.x.x + validations: + required: true + + - type: input + id: node-version + attributes: + label: Node.js version + placeholder: 14.x + validations: + required: true + + - type: textarea + id: description + attributes: + label: đŸĻĨ Performance issue + description: A clear and concise description of what the performance issue is. + validations: + required: true + + - type: textarea + id: steps-to-reproduce + attributes: + label: Steps to Reproduce + description: | + List of steps, sample code, or a link to code or a project that reproduces the behavior. + Make sure you place a stack trace inside a [code (```) block](https://docs.github.com/en/github/writing-on-github/creating-and-highlighting-code-blocks) to avoid linking unrelated issues. + validations: + required: true + + - type: textarea + id: expected-behavior + attributes: + label: Expected Behavior + description: A clear and concise description of what you expected to happen. diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml new file mode 100644 index 00000000000..70eb3fc046c --- /dev/null +++ b/.github/codeql/codeql-config.yml @@ -0,0 +1,6 @@ +name: "codeql config" + +languages: + - 'javascript' +paths: + - lib diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000000..9c78ef14c90 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,47 @@ +name: "Code Scanning - Action" + +on: + pull_request: + paths: + - 'lib/**' + push: + branches: + - master + paths: + - 'lib/**' + +jobs: + CodeQL-Build: + # CodeQL runs on ubuntu-latest, windows-latest, and macos-latest + runs-on: ubuntu-latest + + permissions: + # required for all workflows + security-events: write + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + config-file: ./.github/codeql/codeql-config.yml + # Override language selection by uncommenting this and choosing your languages + # with: + # languages: go, javascript, csharp, python, cpp, java + + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following + # three lines and modify them (or add more) to build your code if your + # project uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ba37e65a9ca..6c597e4a31f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -121,4 +121,15 @@ jobs: uses: actions/upload-artifact@v3 with: name: coverage - path: coverage \ No newline at end of file + path: coverage + dependency-review: + name: Dependency Review + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Check out repo + uses: actions/checkout@v3 + - name: Dependency review + uses: actions/dependency-review-action@v1 diff --git a/CHANGELOG.md b/CHANGELOG.md index d791a8e0632..8434a4c105b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +6.3.4 / 2022-05-19 +================== + * fix(schema): disallow using schemas with schema-level projection with map subdocuments #11698 + * fix(document): avoid setting nested paths to null when they're undefined #11723 + * fix: allow using comment with findOneAndUpdate(), count(), `distinct()` and `hint` with `findOneAndUpdate()` #11793 + * fix(document): clean modified subpaths when setting nested path to null after modifying subpaths #11764 + * fix(types): allow calling `deleteModel()` with RegExp in TypeScript #11812 + * docs(typescript): add section on PopulatedDoc to TypeScript populate docs #11685 + 6.3.3 / 2022-05-09 ================== * perf: avoid leaking memory when using populate() with QueryCursor because of reusing populate options with `_docs` #11641 diff --git a/docs/populate.md b/docs/populate.md index a9663c3ebfb..501870aa69e 100644 --- a/docs/populate.md +++ b/docs/populate.md @@ -291,7 +291,7 @@ If you were to `populate()` using the `limit` option, you would find that the 2nd story has 0 fans: ```javascript -const stories = Story.find().populate({ +const stories = await Story.find().populate({ path: 'fans', options: { limit: 2 } }); diff --git a/docs/typescript.md b/docs/typescript.md index 4e3cbac8833..f6f2fc47d87 100644 --- a/docs/typescript.md +++ b/docs/typescript.md @@ -129,4 +129,4 @@ However, before you do, please [open an issue on Mongoose's GitHub page](https:/ ### Next Up -Now that you've seen the basics of how to use Mongoose in TypeScript, let's take a look at [statics in TypeScript](/docs/typescript/statics.html). +Now that you've seen the basics of how to use Mongoose in TypeScript, let's take a look at [statics in TypeScript](/docs/typescript/statics-and-methods.html). diff --git a/docs/typescript/populate.md b/docs/typescript/populate.md index f0526db0b6b..87e53f08444 100644 --- a/docs/typescript/populate.md +++ b/docs/typescript/populate.md @@ -57,3 +57,43 @@ ParentModel.findOne({}).populate>('child').orFail const t: string = doc.child.name; }); ``` + +## Using `PopulatedDoc` + +Mongoose also exports a `PopulatedDoc` type that helps you define populated documents in your document interface: + +```ts +import { Schema, model, Document, PopulatedDoc } from 'mongoose'; + +// `child` is either an ObjectId or a populated document +interface Parent { + child?: PopulatedDoc & Child>, + name?: string +} +const ParentModel = model('Parent', new Schema({ + child: { type: 'ObjectId', ref: 'Child' }, + name: String +})); + +interface Child { + name?: string; +} +const childSchema: Schema = new Schema({ name: String }); +const ChildModel = model('Child', childSchema); + +ParentModel.findOne({}).populate('child').orFail().then((doc: Parent) => { + const child = doc.child; + if (child == null || child instanceof ObjectId) { + throw new Error('should be populated'); + } else { + // Works + doc.child.name.trim(); + } +}); +``` + +However, we recommend using the `.populate<{ child: Child }>` syntax from the first section instead of `PopulatedDoc`. +Here's two reasons why: + +1. You still need to add an extra check to check if `child instanceof ObjectId`. Otherwise, the TypeScript compiler will fail with `Property name does not exist on type ObjectId`. So using `PopulatedDoc<>` means you need an extra check everywhere you use `doc.child`. +2. In the `Parent` interface, `child` is a hydrated document, which makes it slow difficult for Mongoose to infer the type of `child` when you use `lean()` or `toObject()`. \ No newline at end of file diff --git a/docs/typescript/query-helpers.md b/docs/typescript/query-helpers.md index d182130d5a5..a9f99f6b32f 100644 --- a/docs/typescript/query-helpers.md +++ b/docs/typescript/query-helpers.md @@ -24,28 +24,31 @@ The 2nd generic parameter, `TQueryHelpers`, should be an interface that contains Below is an example of creating a `ProjectModel` with a `byName` query helper. ```typescript -import { Document, Model, Query, Schema, connect, model } from 'mongoose'; +import { HydratedDocument, Model, Query, Schema, model } from 'mongoose'; interface Project { name: string; stars: number; } -const schema = new Schema({ - name: { type: String, required: true }, - stars: { type: Number, required: true } -}); +type ProjectModelType = Model; // Query helpers should return `Query> & ProjectQueryHelpers` // to enable chaining. +type ProjectModelQuery = Query, ProjectQueryHelpers> & ProjectQueryHelpers; interface ProjectQueryHelpers { - byName(name: string): Query> & ProjectQueryHelpers; + byName(this: ProjectModelQuery, name: string): ProjectModelQuery; } -schema.query.byName = function(name): Query> & ProjectQueryHelpers { + +const schema = new Schema({ + name: { type: String, required: true }, + stars: { type: Number, required: true } +}); +schema.query.byName = function(name: string): ProjectModelQuery { return this.find({ name: name }); }; // 2nd param to `model()` is the Model class to return. -const ProjectModel = model>('Project', schema); +const ProjectModel = model('Project', schema); run().catch(err => console.log(err)); diff --git a/lib/connection.js b/lib/connection.js index cd36386bc00..f03f42a67c4 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -548,7 +548,7 @@ Connection.prototype.dropDatabase = _wrapConnHelper(function dropDatabase(cb) { // If `dropDatabase()` is called, this model's collection will not be // init-ed. It is sufficiently common to call `dropDatabase()` after // `mongoose.connect()` but before creating models that we want to - // support this. See gh-6967 + // support this. See gh-6796 for (const name of Object.keys(this.models)) { delete this.models[name].$init; } diff --git a/lib/document.js b/lib/document.js index 20394332a2a..431406aa9d4 100644 --- a/lib/document.js +++ b/lib/document.js @@ -1323,6 +1323,10 @@ Document.prototype.$set = function $set(path, val, type, options) { if (!schema) { this.$__set(pathToMark, path, options, constructing, parts, schema, val, priorVal); + + if (pathType === 'nested' && val == null) { + cleanModifiedSubpaths(this, path); + } return this; } @@ -1374,7 +1378,7 @@ Document.prototype.$set = function $set(path, val, type, options) { })(); let didPopulate = false; - if (refMatches && val instanceof Document) { + if (refMatches && val instanceof Document && (!val.$__.wasPopulated || utils.deepEqual(val.$__.wasPopulated.value, val._id))) { const unpopulatedValue = (schema && schema.$isSingleNested) ? schema.cast(val, this) : val._id; this.$populated(path, unpopulatedValue, { [populateModelSymbol]: val.constructor }); val.$__.wasPopulated = { value: unpopulatedValue }; @@ -3460,9 +3464,9 @@ Document.prototype.$toObject = function(options, json) { const path = json ? 'toJSON' : 'toObject'; const baseOptions = this.constructor && - this.constructor.base && - this.constructor.base.options && - get(this.constructor.base.options, path) || {}; + this.constructor.base && + this.constructor.base.options && + get(this.constructor.base.options, path) || {}; const schemaOptions = this.$__schema && this.$__schema.options || {}; // merge base default options with Schema's set default options if available. // `clone` is necessary here because `utils.options` directly modifies the second input. @@ -3499,7 +3503,8 @@ Document.prototype.$toObject = function(options, json) { _isNested: true, json: json, minimize: _minimize, - flattenMaps: flattenMaps + flattenMaps: flattenMaps, + _seen: (options && options._seen) || new Map() }); if (utils.hasUserDefinedProperty(options, 'getters')) { diff --git a/lib/error/index.js b/lib/error/index.js index 8a40f28176f..4f5b446736f 100644 --- a/lib/error/index.js +++ b/lib/error/index.js @@ -5,11 +5,11 @@ * Mongoose-specific errors. * * #### Example: - * const Model = mongoose.model('Test', new Schema({ answer: Number })); + * const Model = mongoose.model('Test', new mongoose.Schema({ answer: Number })); * const doc = new Model({ answer: 'not a number' }); * const err = doc.validateSync(); * - * err instanceof mongoose.Error; // true + * err instanceof mongoose.Error.ValidationError; // true * * @constructor Error * @param {String} msg Error message diff --git a/lib/helpers/clone.js b/lib/helpers/clone.js index 3ae0ac504a7..379a74abeca 100644 --- a/lib/helpers/clone.js +++ b/lib/helpers/clone.js @@ -77,7 +77,7 @@ function clone(obj, options, isArrayChild) { } } - if (obj instanceof ObjectId) { + if (isBsonType(obj, 'ObjectID')) { return new ObjectId(obj.id); } @@ -118,9 +118,16 @@ module.exports = clone; function cloneObject(obj, options, isArrayChild) { const minimize = options && options.minimize; + const omitUndefined = options && options.omitUndefined; + const seen = options && options._seen; const ret = {}; let hasKeys; + if (seen && seen.has(obj)) { + return seen.get(obj); + } else if (seen) { + seen.set(obj, ret); + } if (trustedSymbol in obj) { ret[trustedSymbol] = obj[trustedSymbol]; } @@ -138,7 +145,7 @@ function cloneObject(obj, options, isArrayChild) { // Don't pass `isArrayChild` down const val = clone(obj[key], options, false); - if (minimize === false && typeof val === 'undefined') { + if ((minimize === false || omitUndefined) && typeof val === 'undefined') { delete ret[key]; } else if (minimize !== true || (typeof val !== 'undefined')) { hasKeys || (hasKeys = true); diff --git a/lib/helpers/common.js b/lib/helpers/common.js index c38c88cf9c0..c1433ce4748 100644 --- a/lib/helpers/common.js +++ b/lib/helpers/common.js @@ -5,8 +5,7 @@ */ const Binary = require('../driver').get().Binary; -const Decimal128 = require('../types/decimal128'); -const ObjectId = require('../types/objectid'); +const isBsonType = require('./isBsonType'); const isMongooseObject = require('./isMongooseObject'); exports.flatten = flatten; @@ -99,9 +98,9 @@ function shouldFlatten(val) { return val && typeof val === 'object' && !(val instanceof Date) && - !(val instanceof ObjectId) && + !isBsonType(val, 'ObjectID') && (!Array.isArray(val) || val.length !== 0) && !(val instanceof Buffer) && - !(val instanceof Decimal128) && + !isBsonType(val, 'Decimal128') && !(val instanceof Binary); } diff --git a/lib/helpers/discriminator/areDiscriminatorValuesEqual.js b/lib/helpers/discriminator/areDiscriminatorValuesEqual.js index 87b2408e6db..52c7ee0eb0a 100644 --- a/lib/helpers/discriminator/areDiscriminatorValuesEqual.js +++ b/lib/helpers/discriminator/areDiscriminatorValuesEqual.js @@ -1,6 +1,6 @@ 'use strict'; -const ObjectId = require('../../types/objectid'); +const isBsonType = require('../isBsonType'); module.exports = function areDiscriminatorValuesEqual(a, b) { if (typeof a === 'string' && typeof b === 'string') { @@ -9,7 +9,7 @@ module.exports = function areDiscriminatorValuesEqual(a, b) { if (typeof a === 'number' && typeof b === 'number') { return a === b; } - if (a instanceof ObjectId && b instanceof ObjectId) { + if (isBsonType(a, 'ObjectID') && isBsonType(b, 'ObjectID')) { return a.toString() === b.toString(); } return false; diff --git a/lib/helpers/isBsonType.js b/lib/helpers/isBsonType.js index 01435d3f285..54791cd02dc 100644 --- a/lib/helpers/isBsonType.js +++ b/lib/helpers/isBsonType.js @@ -1,13 +1,15 @@ 'use strict'; -const get = require('./get'); - /*! * Get the bson type, if it exists */ function isBsonType(obj, typename) { - return get(obj, '_bsontype', void 0) === typename; + return ( + typeof obj === 'object' && + obj !== null && + obj._bsontype === typename + ); } module.exports = isBsonType; diff --git a/lib/helpers/path/setDottedPath.js b/lib/helpers/path/setDottedPath.js index d276e164d3b..c6544c655bf 100644 --- a/lib/helpers/path/setDottedPath.js +++ b/lib/helpers/path/setDottedPath.js @@ -1,14 +1,25 @@ 'use strict'; +const specialProperties = require('../specialProperties'); + + module.exports = function setDottedPath(obj, path, val) { if (path.indexOf('.') === -1) { + if (specialProperties.has(path)) { + return; + } + obj[path] = val; return; } const parts = path.split('.'); + const last = parts.pop(); let cur = obj; for (const part of parts) { + if (specialProperties.has(part)) { + continue; + } if (cur[part] == null) { cur[part] = {}; } @@ -16,5 +27,7 @@ module.exports = function setDottedPath(obj, path, val) { cur = cur[part]; } - cur[last] = val; + if (!specialProperties.has(last)) { + cur[last] = val; + } }; \ No newline at end of file diff --git a/lib/helpers/topology/isAtlas.js b/lib/helpers/topology/isAtlas.js index f0638d55136..8d4ae23c8c4 100644 --- a/lib/helpers/topology/isAtlas.js +++ b/lib/helpers/topology/isAtlas.js @@ -8,6 +8,19 @@ module.exports = function isAtlas(topologyDescription) { } const hostnames = Array.from(topologyDescription.servers.keys()); - return hostnames.length > 0 && - hostnames.every(host => host.endsWith('.mongodb.net:27017')); + + if (hostnames.length === 0) { + return false; + } + + for (let i = 0, il = hostnames.length; i < il; ++i) { + const url = new URL(hostnames[i]); + if ( + url.hostname.endsWith('.mongodb.net') === false || + url.port !== '27017' + ) { + return false; + } + } + return true; }; \ No newline at end of file diff --git a/lib/model.js b/lib/model.js index 65538c738b9..82fe93530bf 100644 --- a/lib/model.js +++ b/lib/model.js @@ -768,6 +768,7 @@ Model.prototype.$__delta = function() { transform: false, virtuals: false, getters: false, + omitUndefined: true, _isNested: true }); operand(this, where, delta, data, value); @@ -1488,61 +1489,68 @@ Model.syncIndexes = function syncIndexes(options, callback) { * the result of this function would be the result of * Model.syncIndexes(). * - * @param {Object} options not used at all. + * @param {Object} [options] * @param {Function} callback optional callback - * @returns {Promise} which containts an object, {toDrop, toCreate}, which - * are indexes that would be dropped in mongodb and indexes that would be created in mongodb. + * @returns {Promise} which contains an object, {toDrop, toCreate}, which + * are indexes that would be dropped in MongoDB and indexes that would be created in MongoDB. */ Model.diffIndexes = function diffIndexes(options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } const toDrop = []; const toCreate = []; callback = this.$handleCallbackError(callback); return this.db.base._promiseOrCallback(callback, cb => { cb = this.$wrapCallback(cb); - this.listIndexes((err, indexes) => { - if (indexes === undefined) { - indexes = []; + this.listIndexes((err, dbIndexes) => { + if (dbIndexes === undefined) { + dbIndexes = []; } - const schemaIndexes = this.schema.indexes(); - // Iterate through the indexes created in mongodb and - // compare against the indexes in the schema. - for (const index of indexes) { + dbIndexes = getRelatedDBIndexes(this, dbIndexes); + const schemaIndexes = getRelatedSchemaIndexes(this, this.schema.indexes()); + + for (const dbIndex of dbIndexes) { let found = false; // Never try to drop `_id` index, MongoDB server doesn't allow it - if (isDefaultIdIndex(index)) { + if (isDefaultIdIndex(dbIndex)) { continue; } - for (const schemaIndex of schemaIndexes) { - const key = schemaIndex[0]; - const options = decorateDiscriminatorIndexOptions(this.schema, utils.clone(schemaIndex[1])); - if (isIndexEqual(key, options, index)) { + for (const [schemaIndexKeysObject, schemaIndexOptions] of schemaIndexes) { + const options = decorateDiscriminatorIndexOptions(this.schema, utils.clone(schemaIndexOptions)); + applySchemaCollation(schemaIndexKeysObject, options, this.schema.options); + + if (isIndexEqual(schemaIndexKeysObject, options, dbIndex)) { found = true; } } if (!found) { - toDrop.push(index.name); + toDrop.push(dbIndex.name); } } // Iterate through the indexes created on the schema and // compare against the indexes in mongodb. - for (const schemaIndex of schemaIndexes) { - let found = false; - const key = schemaIndex[0]; - const options = decorateDiscriminatorIndexOptions(this.schema, utils.clone(schemaIndex[1])); - for (const index of indexes) { - if (isDefaultIdIndex(index)) { - continue; + if (!options || options.toCreate !== false) { + for (const schemaIndex of schemaIndexes) { + let found = false; + const key = schemaIndex[0]; + const options = decorateDiscriminatorIndexOptions(this.schema, utils.clone(schemaIndex[1])); + for (const index of dbIndexes) { + if (isDefaultIdIndex(index)) { + continue; + } + if (isIndexEqual(key, options, index)) { + found = true; + } } - if (isIndexEqual(key, options, index)) { - found = true; + if (!found) { + toCreate.push(key); } } - if (!found) { - toCreate.push(key); - } } cb(null, { toDrop, toCreate }); }); @@ -1568,36 +1576,12 @@ Model.cleanIndexes = function cleanIndexes(callback) { return this.db.base._promiseOrCallback(callback, cb => { const collection = this.$__collection; - this.listIndexes((err, dbIndexes) => { + this.diffIndexes({ toCreate: false }, (err, res) => { if (err != null) { return cb(err); } - dbIndexes = getRelatedDBIndexes(this, dbIndexes); - const schemaIndexes = getRelatedSchemaIndexes(this, this.schema.indexes()); - - const toDrop = []; - - for (const dbIndex of dbIndexes) { - let found = false; - // Never try to drop `_id` index, MongoDB server doesn't allow it - if (isDefaultIdIndex(dbIndex)) { - continue; - } - - for (const [schemaIndexKeysObject, schemaIndexOptions] of schemaIndexes) { - const options = decorateDiscriminatorIndexOptions(this.schema, utils.clone(schemaIndexOptions)); - applySchemaCollation(schemaIndexKeysObject, options, this.schema.options); - - if (isIndexEqual(schemaIndexKeysObject, options, dbIndex)) { - found = true; - } - } - - if (!found) { - toDrop.push(dbIndex.name); - } - } + const toDrop = res.toDrop; if (toDrop.length === 0) { return cb(null, []); diff --git a/lib/schema.js b/lib/schema.js index 78b2e386753..4e7729409ea 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -1235,6 +1235,15 @@ function createMapNestedSchemaType(schema, schemaType, path, obj, options) { _mapType = { [schema.options.typeKey]: obj.of }; } + if (_mapType[schema.options.typeKey] && _mapType[schema.options.typeKey].instanceOfSchema) { + const subdocumentSchema = _mapType[schema.options.typeKey]; + subdocumentSchema.eachPath((subpath, type) => { + if (type.options.select === true || type.options.select === false) { + throw new MongooseError('Cannot use schema-level projections (`select: true` or `select: false`) within maps at path "' + path + '.' + subpath + '"'); + } + }); + } + if (utils.hasUserDefinedProperty(obj, 'ref')) { _mapType.ref = obj.ref; } diff --git a/lib/schema/decimal128.js b/lib/schema/decimal128.js index f20a452ccaf..3323df4e094 100644 --- a/lib/schema/decimal128.js +++ b/lib/schema/decimal128.js @@ -6,9 +6,9 @@ const SchemaType = require('../schematype'); const CastError = SchemaType.CastError; -const Decimal128Type = require('../types/decimal128'); const castDecimal128 = require('../cast/decimal128'); const utils = require('../utils'); +const isBsonType = require('../helpers/isBsonType'); /** * Decimal128 SchemaType constructor. @@ -105,7 +105,7 @@ Decimal128.cast = function cast(caster) { */ Decimal128._defaultCaster = v => { - if (v != null && !(v instanceof Decimal128Type)) { + if (v != null && !isBsonType(v, 'Decimal128')) { throw new Error(); } return v; @@ -115,7 +115,7 @@ Decimal128._defaultCaster = v => { * ignore */ -Decimal128._checkRequired = v => v instanceof Decimal128Type; +Decimal128._checkRequired = v => isBsonType(v, 'Decimal128'); /** * Override the function the required validator uses to check whether a string @@ -164,7 +164,7 @@ Decimal128.prototype.checkRequired = function checkRequired(value, doc) { Decimal128.prototype.cast = function(value, doc, init) { if (SchemaType._isRef(this, value, doc, init)) { - if (value instanceof Decimal128Type) { + if (isBsonType(value, 'Decimal128')) { return value; } diff --git a/lib/types/DocumentArray/methods/index.js b/lib/types/DocumentArray/methods/index.js index ac43d91bd3b..3a6a7c55d19 100644 --- a/lib/types/DocumentArray/methods/index.js +++ b/lib/types/DocumentArray/methods/index.js @@ -2,11 +2,11 @@ const ArrayMethods = require('../../array/methods'); const Document = require('../../../document'); -const ObjectId = require('../../objectid'); const castObjectId = require('../../../cast/objectid'); const getDiscriminatorByValue = require('../../../helpers/discriminator/getDiscriminatorByValue'); const internalToObjectOptions = require('../../../options').internalToObjectOptions; const utils = require('../../../utils'); +const isBsonType = require('../../../helpers/isBsonType'); const arrayParentSymbol = require('../../../helpers/symbols').arrayParentSymbol; const arrayPathSymbol = require('../../../helpers/symbols').arrayPathSymbol; @@ -66,7 +66,7 @@ const methods = { // only objects are permitted so we can safely assume that // non-objects are to be interpreted as _id if (Buffer.isBuffer(value) || - value instanceof ObjectId || !utils.isObject(value)) { + isBsonType(value, 'ObjectID') || !utils.isObject(value)) { value = { _id: value }; } @@ -134,7 +134,7 @@ const methods = { if (sid == _id._id) { return val; } - } else if (!(id instanceof ObjectId) && !(_id instanceof ObjectId)) { + } else if (!isBsonType(id, 'ObjectID') && !isBsonType(_id, 'ObjectID')) { if (id == _id || utils.deepEqual(id, _id)) { return val; } diff --git a/lib/types/array/methods/index.js b/lib/types/array/methods/index.js index 792d9b59e87..9de796d8aae 100644 --- a/lib/types/array/methods/index.js +++ b/lib/types/array/methods/index.js @@ -3,10 +3,10 @@ const Document = require('../../../document'); const ArraySubdocument = require('../../ArraySubdocument'); const MongooseError = require('../../../error/mongooseError'); -const ObjectId = require('../../objectid'); const cleanModifiedSubpaths = require('../../../helpers/document/cleanModifiedSubpaths'); const internalToObjectOptions = require('../../../options').internalToObjectOptions; const utils = require('../../../utils'); +const isBsonType = require('../../../helpers/isBsonType'); const arrayAtomicsSymbol = require('../../../helpers/symbols').arrayAtomicsSymbol; const arrayParentSymbol = require('../../../helpers/symbols').arrayParentSymbol; @@ -227,7 +227,7 @@ const methods = { // only objects are permitted so we can safely assume that // non-objects are to be interpreted as _id if (Buffer.isBuffer(value) || - value instanceof ObjectId || !utils.isObject(value)) { + isBsonType(value, 'ObjectID') || !utils.isObject(value)) { value = { _id: value }; } @@ -469,7 +469,7 @@ const methods = { */ indexOf(obj, fromIndex) { - if (obj instanceof ObjectId) { + if (isBsonType(obj, 'ObjectID')) { obj = obj.toString(); } diff --git a/lib/types/map.js b/lib/types/map.js index 903110e63ec..486c03f4e7f 100644 --- a/lib/types/map.js +++ b/lib/types/map.js @@ -1,13 +1,13 @@ 'use strict'; const Mixed = require('../schema/mixed'); -const ObjectId = require('./objectid'); const clone = require('../helpers/clone'); const deepEqual = require('../utils').deepEqual; const getConstructorName = require('../helpers/getConstructorName'); const handleSpreadDoc = require('../helpers/document/handleSpreadDoc'); const util = require('util'); const specialProperties = require('../helpers/specialProperties'); +const isBsonType = require('../helpers/isBsonType'); const populateModelSymbol = require('../helpers/symbols').populateModelSymbol; @@ -43,7 +43,7 @@ class MongooseMap extends Map { } get(key, options) { - if (key instanceof ObjectId) { + if (isBsonType(key, 'ObjectID')) { key = key.toString(); } @@ -55,7 +55,7 @@ class MongooseMap extends Map { } set(key, value) { - if (key instanceof ObjectId) { + if (isBsonType(key, 'ObjectID')) { key = key.toString(); } @@ -117,7 +117,7 @@ class MongooseMap extends Map { } delete(key) { - if (key instanceof ObjectId) { + if (isBsonType(key, 'ObjectID')) { key = key.toString(); } diff --git a/lib/utils.js b/lib/utils.js index 27992139393..7a6f129fe41 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -6,7 +6,6 @@ const ms = require('ms'); const mpath = require('mpath'); -const Decimal = require('./types/decimal128'); const ObjectId = require('./types/objectid'); const PopulateOptions = require('./options/PopulateOptions'); const clone = require('./helpers/clone'); @@ -307,7 +306,7 @@ exports.merge = function merge(to, from, options, path) { to[key] = from[key].clone(); } continue; - } else if (from[key] instanceof ObjectId) { + } else if (isBsonType(from[key], 'ObjectID')) { to[key] = new ObjectId(from[key]); continue; } @@ -481,7 +480,7 @@ exports.tick = function tick(callback) { */ exports.isMongooseType = function(v) { - return v instanceof ObjectId || v instanceof Decimal || v instanceof Buffer; + return isBsonType(v, 'ObjectID') || isBsonType(v, 'Decimal128') || v instanceof Buffer; }; exports.isMongooseObject = isMongooseObject; @@ -795,7 +794,7 @@ exports.array.unique = function(arr) { } ret.push(item); primitives.add(item); - } else if (item instanceof ObjectId) { + } else if (isBsonType(item, 'ObjectID')) { if (ids.has(item.toString())) { continue; } diff --git a/package.json b/package.json index 66b77945807..d43863af6a1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "6.3.3", + "version": "6.3.4", "author": "Guillermo Rauch ", "keywords": [ "mongodb", @@ -23,7 +23,7 @@ "kareem": "2.3.5", "mongodb": "4.5.0", "mpath": "0.9.0", - "mquery": "4.0.2", + "mquery": "4.0.3", "ms": "2.1.3", "sift": "16.0.0" }, diff --git a/test/aggregate.test.js b/test/aggregate.test.js index 455fd188f02..8080952970a 100644 --- a/test/aggregate.test.js +++ b/test/aggregate.test.js @@ -17,7 +17,7 @@ const Schema = mongoose.Schema; * Test data */ -function setupData(db, callback) { +async function setupData(db) { const EmployeeSchema = new Schema({ name: String, sal: Number, @@ -26,7 +26,6 @@ function setupData(db, callback) { reportsTo: String }); - let saved = 0; const emps = [ { name: 'Alice', sal: 18000, dept: 'sales', customers: ['Eve', 'Fred'] }, { name: 'Bob', sal: 15000, dept: 'sales', customers: ['Gary', 'Herbert', 'Isaac'], reportsTo: 'Alice' }, @@ -35,17 +34,9 @@ function setupData(db, callback) { ]; const Employee = db.model('Employee', EmployeeSchema); - Employee.deleteMany({}, function() { - emps.forEach(function(data) { - const emp = new Employee(data); + await Employee.deleteMany({}); - emp.save(function() { - if (++saved === emps.length) { - callback(); - } - }); - }); - }); + await Employee.create(emps); } /** @@ -584,8 +575,8 @@ describe('aggregate: ', function() { }); describe('exec', function() { - beforeEach(function(done) { - setupData(db, done); + beforeEach(async function() { + await setupData(db); }); it('project', async function() { @@ -668,29 +659,25 @@ describe('aggregate: ', function() { assert.ok(threw); }); - it('match', function(done) { + it('match', function() { const aggregate = new Aggregate([], db.model('Employee')); - aggregate. + return aggregate. match({ sal: { $gt: 15000 } }). exec(function(err, docs) { assert.ifError(err); assert.equal(docs.length, 1); - - done(); }); }); - it('sort', function(done) { + it('sort', function() { const aggregate = new Aggregate([], db.model('Employee')); - aggregate. + return aggregate. sort('sal'). exec(function(err, docs) { assert.ifError(err); assert.equal(docs[0].sal, 14000); - - done(); }); }); @@ -765,10 +752,10 @@ describe('aggregate: ', function() { ]); }); - it('complex pipeline', function(done) { + it('complex pipeline', function() { const aggregate = new Aggregate([], db.model('Employee')); - aggregate. + return aggregate. match({ sal: { $lt: 16000 } }). unwind('customers'). project({ emp: '$name', cust: '$customers' }). @@ -779,8 +766,6 @@ describe('aggregate: ', function() { assert.equal(docs.length, 1); assert.equal(docs[0].cust, 'Gary'); assert.equal(docs[0].emp, 'Bob'); - - done(); }); }); @@ -1086,18 +1071,13 @@ describe('aggregate: ', function() { assert.equal(err.name, 'MongoServerError'); }); - it('cursor() without options (gh-3855)', function(done) { - const db = start(); - + it('cursor() without options (gh-3855)', function() { const MyModel = db.model('Test', { name: String }); - db.on('open', function() { - const cursor = MyModel. - aggregate([{ $match: { name: 'test' } }]). - cursor(); - assert.ok(cursor instanceof require('stream').Readable); - done(); - }); + const cursor = MyModel. + aggregate([{ $match: { name: 'test' } }]). + cursor(); + assert.ok(cursor instanceof require('stream').Readable); }); it('cursor() with useMongooseAggCursor (gh-5145)', function() { @@ -1109,56 +1089,48 @@ describe('aggregate: ', function() { assert.ok(cursor instanceof require('stream').Readable); }); - it('cursor() with useMongooseAggCursor works (gh-5145) (gh-5394)', function(done) { + it('cursor() with useMongooseAggCursor works (gh-5145) (gh-5394)', async function() { const MyModel = db.model('Test', { name: String }); - MyModel.create({ name: 'test' }, function(error) { - assert.ifError(error); + await MyModel.create({ name: 'test' }); - const docs = []; - MyModel. - aggregate([{ $match: { name: 'test' } }]). - cursor({ useMongooseAggCursor: true }). - eachAsync(function(doc) { - docs.push(doc); - }). - then(function() { - assert.equal(docs.length, 1); - assert.equal(docs[0].name, 'test'); - done(); - }); - }); + const docs = []; + await MyModel. + aggregate([{ $match: { name: 'test' } }]). + cursor({ useMongooseAggCursor: true }). + eachAsync(function(doc) { + docs.push(doc); + }); + + assert.equal(docs.length, 1); + assert.equal(docs[0].name, 'test'); }); - it('cursor() eachAsync (gh-4300)', function(done) { + it('cursor() eachAsync (gh-4300)', async function() { const MyModel = db.model('Test', { name: String }); let cur = 0; const expectedNames = ['Axl', 'Slash']; - MyModel.create([{ name: 'Axl' }, { name: 'Slash' }]). - then(function() { - return MyModel.aggregate([{ $sort: { name: 1 } }]). - cursor(). - eachAsync(function(doc) { - const _cur = cur; - assert.equal(doc.name, expectedNames[cur]); - return { - then: function(resolve) { - setTimeout(function() { - assert.equal(_cur, cur++); - resolve(); - }, 50); - } - }; - }). - then(function() { - done(); - }); - }). - catch(done); + + await MyModel.create([{ name: 'Axl' }, { name: 'Slash' }]); + + await MyModel.aggregate([{ $sort: { name: 1 } }]). + cursor(). + eachAsync(function(doc) { + const _cur = cur; + assert.equal(doc.name, expectedNames[cur]); + return { + then: function(resolve) { + setTimeout(function() { + assert.equal(_cur, cur++); + resolve(); + }, 50); + } + }; + }); }); - it('cursor() eachAsync with options (parallel)', function(done) { + it('cursor() eachAsync with options (parallel)', async function() { const MyModel = db.model('Test', { name: String }); const names = []; @@ -1175,40 +1147,34 @@ describe('aggregate: ', function() { } }; }; - MyModel.create([{ name: 'Axl' }, { name: 'Slash' }]). - then(function() { - return MyModel.aggregate([{ $sort: { name: 1 } }]). - cursor(). - eachAsync(checkDoc, { parallel: 2 }).then(function() { - assert.ok(Date.now() - startedAt[1] >= 100, Date.now() - startedAt[1]); - assert.equal(startedAt.length, 2); - assert.ok(startedAt[1] - startedAt[0] < 50, `${startedAt[1] - startedAt[0]}`); - assert.deepEqual(names.sort(), expectedNames); - done(); - }); - }). - catch(done); + + await MyModel.create([{ name: 'Axl' }, { name: 'Slash' }]); + + await MyModel.aggregate([{ $sort: { name: 1 } }]). + cursor(). + eachAsync(checkDoc, { parallel: 2 }).then(function() { + assert.ok(Date.now() - startedAt[1] >= 100, Date.now() - startedAt[1]); + assert.equal(startedAt.length, 2); + assert.ok(startedAt[1] - startedAt[0] < 50, `${startedAt[1] - startedAt[0]}`); + assert.deepEqual(names.sort(), expectedNames); + }); }); - it('is now a proper aggregate cursor vs what it was before gh-10410', function(done) { + it('is now a proper aggregate cursor vs what it was before gh-10410', function() { const MyModel = db.model('Test', { name: String }); assert.throws(() => { MyModel.aggregate([]).cursor({ batchSize: 1000 }).exec(); }); - done(); }); - it('query by document (gh-4866)', function(done) { + it('query by document (gh-4866)', async function() { const MyModel = db.model('Test', { name: String }); - MyModel.create({ name: 'test' }). - then(function(doc) { return MyModel.aggregate([{ $match: doc }]); }). - then(function() { - done(); - }). - catch(done); + const doc = await MyModel.create({ name: 'test' }); + const res = await MyModel.aggregate([{ $match: doc }]); + assert.equal(res.length, 1); }); it('sort by text score (gh-5258)', async function() { diff --git a/test/colors.js b/test/colors.js deleted file mode 100644 index 617bbabeae6..00000000000 --- a/test/colors.js +++ /dev/null @@ -1,129 +0,0 @@ -/** - * Module dependencies. - */ - -'use strict'; - -const start = require('./common'); - -const DocumentArray = require('../lib/types/DocumentArray'); -const ArraySubdocument = require('../lib/types/ArraySubdocument'); -const assert = require('assert'); - -const mongoose = start.mongoose; -const Schema = mongoose.Schema; -const MongooseDocumentArray = mongoose.Types.DocumentArray; - -/** - * setup - */ - -const test = new Schema({ - string: String, - number: Number, - date: { - type: Date, - default: Date.now - } -}); - -function TestDoc(schema) { - const Subdocument = function() { - ArraySubdocument.call(this, {}, new DocumentArray()); - }; - - /** - * Inherits from ArraySubdocument. - */ - - Subdocument.prototype.__proto__ = ArraySubdocument.prototype; - - /** - * Set schema. - */ - - Subdocument.prototype.$__setSchema(schema || test); - - return Subdocument; -} - -/** - * Test. - */ - -describe('debug: colors', function() { - let db; - let Test; - - before(function() { - db = start(); - Test = db.model('Test', test, 'Test'); - }); - - after(async function() { - await db.close(); - }); - - it('Document', function(done) { - const date = new Date(); - - Test.create([{ - string: 'qwerty', - number: 123, - date: date - }, { - string: 'asdfgh', - number: 456, - date: date - }, { - string: 'zxcvbn', - number: 789, - date: date - }], function(err) { - assert.ifError(err); - Test - .find() - .lean(false) - .exec(function(err, docs) { - assert.ifError(err); - - const colorfull = require('util').inspect(docs, { - depth: null, - colors: true - }); - - const colorless = require('util').inspect(docs, { - depth: null, - colors: false - }); - - assert.notEqual(colorfull, colorless); - - done(); - }); - }); - }); - - it('MongooseDocumentArray', function() { - const Subdocument = TestDoc(); - - const sub1 = new Subdocument(); - sub1.string = 'string'; - sub1.number = 12345; - sub1.date = new Date(); - - const docs = new MongooseDocumentArray([sub1]); - - const colorfull = require('util').inspect(docs, { - depth: null, - colors: true - }); - - const colorless = require('util').inspect(docs, { - depth: null, - colors: false - }); - - assert.notEqual(colorfull, colorless); - }); -}); diff --git a/test/common.js b/test/common.js index 9e6bcc46f80..175a8c391bd 100644 --- a/test/common.js +++ b/test/common.js @@ -159,14 +159,10 @@ module.exports.mongodVersion = async function() { }); }; -function dropDBs(done) { - const db = module.exports({ noErrorListener: true }); - db.once('open', function() { - // drop the default test database - db.db.dropDatabase(function() { - done(); - }); - }); +async function dropDBs() { + const db = await module.exports({ noErrorListener: true }).asPromise(); + await db.dropDatabase(); + await db.close(); } before(async function() { @@ -186,17 +182,9 @@ before(async function() { } }); -before(function(done) { - this.timeout(60 * 1000); - dropDBs(done); -}); +before(dropDBs); -after(function(done) { - dropDBs(() => {}); - - // Give `dropDatabase()` some time to run - setTimeout(() => done(), 250); -}); +after(dropDBs); beforeEach(function() { if (this.currentTest) { diff --git a/test/docs/discriminators.test.js b/test/docs/discriminators.test.js index e3e684157a1..b13abb16fc9 100644 --- a/test/docs/discriminators.test.js +++ b/test/docs/discriminators.test.js @@ -163,7 +163,7 @@ describe('discriminator docs', function() { * If a custom _id field is set on the base schema, that will always * override the discriminator's _id field, as shown below. */ - it('Handling custom _id fields', function(done) { + it('Handling custom _id fields', function() { const options = { discriminatorKey: 'kind' }; // Base schema has a custom String `_id` and a Date `time`... @@ -187,17 +187,13 @@ describe('discriminator docs', function() { // the `_id` path. assert.strictEqual(typeof event1._id, 'string'); assert.strictEqual(typeof event1.time, 'string'); - - // acquit:ignore:start - done(); - // acquit:ignore:end }); /** * When you use `Model.create()`, mongoose will pull the correct type from * the discriminator key for you. */ - it('Using discriminators with `Model.create()`', function(done) { + it('Using discriminators with `Model.create()`', async function() { const Schema = mongoose.Schema; const shapeSchema = new Schema({ name: String @@ -210,22 +206,17 @@ describe('discriminator docs', function() { const Square = Shape.discriminator('Square', new Schema({ side: Number })); - const shapes = [ + const shapes = await Shape.create([ { name: 'Test' }, { kind: 'Circle', radius: 5 }, { kind: 'Square', side: 10 } - ]; - Shape.create(shapes, function(error, shapes) { - assert.ifError(error); - assert.ok(shapes[0] instanceof Shape); - assert.ok(shapes[1] instanceof Circle); - assert.equal(shapes[1].radius, 5); - assert.ok(shapes[2] instanceof Square); - assert.equal(shapes[2].side, 10); - // acquit:ignore:start - done(); - // acquit:ignore:end - }); + ]); + + assert.ok(shapes[0] instanceof Shape); + assert.ok(shapes[1] instanceof Circle); + assert.equal(shapes[1].radius, 5); + assert.ok(shapes[2] instanceof Square); + assert.equal(shapes[2].side, 10); }); /** @@ -239,7 +230,7 @@ describe('discriminator docs', function() { * schemas **before** you use them. You should **not** call `pre()` or * `post()` after calling `discriminator()` */ - it('Embedded discriminators in arrays', function(done) { + it('Embedded discriminators in arrays', async function() { const eventSchema = new Schema({ message: String }, { discriminatorKey: 'kind', _id: false }); @@ -271,37 +262,31 @@ describe('discriminator docs', function() { const Batch = db.model('EventBatch', batchSchema); // Create a new batch of events with different kinds - const batch = { + const doc = await Batch.create({ events: [ { kind: 'Clicked', element: '#hero', message: 'hello' }, { kind: 'Purchased', product: 'action-figure-1', message: 'world' } ] - }; + }); + + assert.equal(doc.events.length, 2); - Batch.create(batch). - then(function(doc) { - assert.equal(doc.events.length, 2); + assert.equal(doc.events[0].element, '#hero'); + assert.equal(doc.events[0].message, 'hello'); + assert.ok(doc.events[0] instanceof Clicked); - assert.equal(doc.events[0].element, '#hero'); - assert.equal(doc.events[0].message, 'hello'); - assert.ok(doc.events[0] instanceof Clicked); + assert.equal(doc.events[1].product, 'action-figure-1'); + assert.equal(doc.events[1].message, 'world'); + assert.ok(doc.events[1] instanceof Purchased); - assert.equal(doc.events[1].product, 'action-figure-1'); - assert.equal(doc.events[1].message, 'world'); - assert.ok(doc.events[1] instanceof Purchased); + doc.events.push({ kind: 'Purchased', product: 'action-figure-2' }); - doc.events.push({ kind: 'Purchased', product: 'action-figure-2' }); - return doc.save(); - }). - then(function(doc) { - assert.equal(doc.events.length, 3); + await doc.save(); - assert.equal(doc.events[2].product, 'action-figure-2'); - assert.ok(doc.events[2] instanceof Purchased); + assert.equal(doc.events.length, 3); - done(); - }). - catch(done); + assert.equal(doc.events[2].product, 'action-figure-2'); + assert.ok(doc.events[2] instanceof Purchased); }); /** @@ -311,7 +296,7 @@ describe('discriminator docs', function() { * embedded discriminator. */ - it('Recursive embedded discriminators in arrays', function(done) { + it('Recursive embedded discriminators in arrays', async function() { const singleEventSchema = new Schema({ message: String }, { discriminatorKey: 'kind', _id: false }); @@ -331,37 +316,31 @@ describe('discriminator docs', function() { const Eventlist = db.model('EventList', eventListSchema); // Create a new batch of events with different kinds - const list = { + const doc = await Eventlist.create({ events: [ { kind: 'SubEvent', sub_events: [{ kind: 'SubEvent', sub_events: [], message: 'test1' }], message: 'hello' }, { kind: 'SubEvent', sub_events: [{ kind: 'SubEvent', sub_events: [{ kind: 'SubEvent', sub_events: [], message: 'test3' }], message: 'test2' }], message: 'world' } ] - }; + }); + + assert.equal(doc.events.length, 2); - Eventlist.create(list). - then(function(doc) { - assert.equal(doc.events.length, 2); + assert.equal(doc.events[0].sub_events[0].message, 'test1'); + assert.equal(doc.events[0].message, 'hello'); + assert.ok(doc.events[0].sub_events[0] instanceof SubEvent); - assert.equal(doc.events[0].sub_events[0].message, 'test1'); - assert.equal(doc.events[0].message, 'hello'); - assert.ok(doc.events[0].sub_events[0] instanceof SubEvent); + assert.equal(doc.events[1].sub_events[0].sub_events[0].message, 'test3'); + assert.equal(doc.events[1].message, 'world'); + assert.ok(doc.events[1].sub_events[0].sub_events[0] instanceof SubEvent); - assert.equal(doc.events[1].sub_events[0].sub_events[0].message, 'test3'); - assert.equal(doc.events[1].message, 'world'); - assert.ok(doc.events[1].sub_events[0].sub_events[0] instanceof SubEvent); + doc.events.push({ kind: 'SubEvent', sub_events: [{ kind: 'SubEvent', sub_events: [], message: 'test4' }], message: 'pushed' }); - doc.events.push({ kind: 'SubEvent', sub_events: [{ kind: 'SubEvent', sub_events: [], message: 'test4' }], message: 'pushed' }); - return doc.save(); - }). - then(function(doc) { - assert.equal(doc.events.length, 3); + await doc.save(); - assert.equal(doc.events[2].message, 'pushed'); - assert.ok(doc.events[2].sub_events[0] instanceof SubEvent); + assert.equal(doc.events.length, 3); - done(); - }). - catch(done); + assert.equal(doc.events[2].message, 'pushed'); + assert.ok(doc.events[2].sub_events[0] instanceof SubEvent); }); /** diff --git a/test/docs/validation.test.js b/test/docs/validation.test.js index b9c9b10b151..83f578a3ef8 100644 --- a/test/docs/validation.test.js +++ b/test/docs/validation.test.js @@ -33,7 +33,7 @@ describe('validation docs', function() { * - Validation is customizable */ - it('Validation', function(done) { + it('Validation', async function() { const schema = new Schema({ name: { type: String, @@ -44,17 +44,20 @@ describe('validation docs', function() { // This cat has no name :( const cat = new Cat(); - cat.save(function(error) { - assert.equal(error.errors['name'].message, - 'Path `name` is required.'); - error = cat.validateSync(); - assert.equal(error.errors['name'].message, - 'Path `name` is required.'); - // acquit:ignore:start - done(); - // acquit:ignore:end - }); + let error; + try { + await cat.save(); + } catch (err) { + error = err; + } + + assert.equal(error.errors['name'].message, + 'Path `name` is required.'); + + error = cat.validateSync(); + assert.equal(error.errors['name'].message, + 'Path `name` is required.'); }); /** @@ -67,7 +70,7 @@ describe('validation docs', function() { * Each of the validator links above provide more information about how to enable them and customize their error messages. */ - it('Built-in Validators', function(done) { + it('Built-in Validators', function() { const breakfastSchema = new Schema({ eggs: { type: Number, @@ -109,9 +112,6 @@ describe('validation docs', function() { badBreakfast.bacon = null; error = badBreakfast.validateSync(); assert.equal(error.errors['bacon'].message, 'Why no bacon?'); - // acquit:ignore:start - done(); - // acquit:ignore:end }); /** @@ -125,7 +125,7 @@ describe('validation docs', function() { * Mongoose replaces `{VALUE}` with the value being validated. */ - it('Custom Error Messages', function(done) { + it('Custom Error Messages', function() { const breakfastSchema = new Schema({ eggs: { type: Number, @@ -153,9 +153,6 @@ describe('validation docs', function() { assert.equal(error.errors['eggs'].message, 'Must be at least 6, got 2'); assert.equal(error.errors['drink'].message, 'Milk is not supported'); - // acquit:ignore:start - done(); - // acquit:ignore:end }); /** @@ -216,7 +213,7 @@ describe('validation docs', function() { * You can find detailed instructions on how to do this in the * [`SchemaType#validate()` API docs](./api.html#schematype_SchemaType-validate). */ - it('Custom Validators', function(done) { + it('Custom Validators', function() { const userSchema = new Schema({ phone: { type: String, @@ -249,9 +246,6 @@ describe('validation docs', function() { // and fits `DDD-DDD-DDDD` error = user.validateSync(); assert.equal(error, null); - // acquit:ignore:start - done(); - // acquit:ignore:end }); /** @@ -260,7 +254,7 @@ describe('validation docs', function() { * promise to settle. If the returned promise rejects, or fulfills with * the value `false`, Mongoose will consider that a validation error. */ - it('Async Custom Validators', function(done) { + it('Async Custom Validators', async function() { const userSchema = new Schema({ name: { type: String, @@ -284,14 +278,16 @@ describe('validation docs', function() { user.email = 'test@test.co'; user.name = 'test'; - user.validate().catch(error => { - assert.ok(error); - assert.equal(error.errors['name'].message, 'Oops!'); - assert.equal(error.errors['email'].message, 'Email validation failed'); - // acquit:ignore:start - done(); - // acquit:ignore:end - }); + + let error; + try { + await user.validate(); + } catch (err) { + error = err; + } + assert.ok(error); + assert.equal(error.errors['name'].message, 'Oops!'); + assert.equal(error.errors['email'].message, 'Email validation failed'); }); /** @@ -304,7 +300,7 @@ describe('validation docs', function() { * thrown. */ - it('Validation Errors', function(done) { + it('Validation Errors', async function() { const toySchema = new Schema({ color: String, name: String @@ -326,30 +322,32 @@ describe('validation docs', function() { const toy = new Toy({ color: 'Green', name: 'Power Ranger' }); - toy.save(function(err) { - // `err` is a ValidationError object - // `err.errors.color` is a ValidatorError object - assert.equal(err.errors.color.message, 'Color `Green` not valid'); - assert.equal(err.errors.color.kind, 'Invalid color'); - assert.equal(err.errors.color.path, 'color'); - assert.equal(err.errors.color.value, 'Green'); - - // This is new in mongoose 5. If your validator throws an exception, - // mongoose will use that message. If your validator returns `false`, - // mongoose will use the 'Name `Power Ranger` is not valid' message. - assert.equal(err.errors.name.message, - 'Need to get a Turbo Man for Christmas'); - assert.equal(err.errors.name.value, 'Power Ranger'); - // If your validator threw an error, the `reason` property will contain - // the original error thrown, including the original stack trace. - assert.equal(err.errors.name.reason.message, - 'Need to get a Turbo Man for Christmas'); - - assert.equal(err.name, 'ValidationError'); - // acquit:ignore:start - done(); - // acquit:ignore:end - }); + let error; + try { + await toy.save(); + } catch(err) { + error = err; + } + + // `error` is a ValidationError object + // `error.errors.color` is a ValidatorError object + assert.equal(error.errors.color.message, 'Color `Green` not valid'); + assert.equal(error.errors.color.kind, 'Invalid color'); + assert.equal(error.errors.color.path, 'color'); + assert.equal(error.errors.color.value, 'Green'); + + // If your validator throws an exception, mongoose will use the error + // message. If your validator returns `false`, + // mongoose will use the 'Name `Power Ranger` is not valid' message. + assert.equal(error.errors.name.message, + 'Need to get a Turbo Man for Christmas'); + assert.equal(error.errors.name.value, 'Power Ranger'); + // If your validator threw an error, the `reason` property will contain + // the original error thrown, including the original stack trace. + assert.equal(error.errors.name.reason.message, + 'Need to get a Turbo Man for Christmas'); + + assert.equal(error.name, 'ValidationError'); }); /** @@ -387,7 +385,7 @@ describe('validation docs', function() { * nested objects are not fully fledged paths. */ - it('Required Validators On Nested Objects', function(done) { + it('Required Validators On Nested Objects', function() { let personSchema = new Schema({ name: { first: String, @@ -418,9 +416,6 @@ describe('validation docs', function() { const person = new Person(); const error = person.validateSync(); assert.ok(error.errors['name']); - // acquit:ignore:start - done(); - // acquit:ignore:end }); /** @@ -437,7 +432,7 @@ describe('validation docs', function() { * Be careful: update validators are off by default because they have several * caveats. */ - it('Update Validators', function(done) { + it('Update Validators', async function() { const toySchema = new Schema({ color: String, name: String @@ -450,13 +445,15 @@ describe('validation docs', function() { }, 'Invalid color'); const opts = { runValidators: true }; - Toy.updateOne({}, { color: 'not a color' }, opts, function(err) { - assert.equal(err.errors.color.message, - 'Invalid color'); - // acquit:ignore:start - done(); - // acquit:ignore:end - }); + + let error; + try { + await Toy.updateOne({}, { color: 'not a color' }, opts); + } catch (err) { + error = err; + } + + assert.equal(error.errors.color.message, 'Invalid color'); }); /** @@ -468,7 +465,7 @@ describe('validation docs', function() { * not defined. */ - it('Update Validators and `this`', function(done) { + it('Update Validators and `this`', async function() { const toySchema = new Schema({ color: String, name: String @@ -487,22 +484,23 @@ describe('validation docs', function() { const Toy = db.model('ActionFigure', toySchema); const toy = new Toy({ color: 'red', name: 'Red Power Ranger' }); - const error = toy.validateSync(); + let error = toy.validateSync(); assert.ok(error.errors['color']); const update = { color: 'red', name: 'Red Power Ranger' }; const opts = { runValidators: true }; - Toy.updateOne({}, update, opts, function(error) { - // The update validator throws an error: - // "TypeError: Cannot read property 'toLowerCase' of undefined", - // because `this` is **not** the document being updated when using - // update validators - assert.ok(error); - // acquit:ignore:start - done(); - // acquit:ignore:end - }); + error = null; + try { + await Toy.updateOne({}, update, opts); + } catch (err) { + error = err; + } + // The update validator throws an error: + // "TypeError: Cannot read property 'toLowerCase' of undefined", + // because `this` is **not** the document being updated when using + // update validators + assert.ok(error); }); /** @@ -510,7 +508,7 @@ describe('validation docs', function() { * to the underlying query. */ - it('The `context` option', function(done) { + it('The `context` option', async function() { // acquit:ignore:start const toySchema = new Schema({ color: String, @@ -531,12 +529,14 @@ describe('validation docs', function() { // Note the context option const opts = { runValidators: true, context: 'query' }; - Toy.updateOne({}, update, opts, function(error) { - assert.ok(error.errors['color']); - // acquit:ignore:start - done(); - // acquit:ignore:end - }); + let error; + try { + await Toy.updateOne({}, update, opts); + } catch (err) { + error = err; + } + + assert.ok(error.errors['color']); }); /** @@ -599,7 +599,7 @@ describe('validation docs', function() { * of the array. */ - it('Update Validators Only Run For Some Operations', function(done) { + it('Update Validators Only Run For Some Operations', async function() { const testSchema = new Schema({ number: { type: Number, max: 0 }, arr: [{ message: { type: String, maxlength: 10 } }] @@ -615,17 +615,13 @@ describe('validation docs', function() { let update = { $inc: { number: 1 } }; const opts = { runValidators: true }; - Test.updateOne({}, update, opts, function() { - // There will never be a validation error here - update = { $push: [{ message: 'hello' }, { message: 'world' }] }; - Test.updateOne({}, update, opts, function(error) { - // This will never error either even though the array will have at - // least 2 elements. - // acquit:ignore:start - assert.ifError(error); - done(); - // acquit:ignore:end - }); - }); + + // There will never be a validation error here + await Test.updateOne({}, update, opts); + + // This will never error either even though the array will have at + // least 2 elements. + update = { $push: [{ message: 'hello' }, { message: 'world' }] }; + await Test.updateOne({}, update, opts); }); }); diff --git a/test/document.populate.test.js b/test/document.populate.test.js index 26a84ab4a41..45a7c308ba8 100644 --- a/test/document.populate.test.js +++ b/test/document.populate.test.js @@ -109,36 +109,32 @@ describe('document.populate', function() { afterEach(() => require('./util').clearTestData(db)); afterEach(() => require('./util').stopRemainingOps(db)); - beforeEach(function(done) { + beforeEach(async function() { B = db.model('BlogPost', BlogPostSchema); User = db.model('User', UserSchema); _id = new mongoose.Types.ObjectId(); - User.create({ - name: 'Phoenix', - email: 'phx@az.com', - blogposts: [_id] - }, { - name: 'Newark', - email: 'ewr@nj.com', - blogposts: [_id] - }, function(err, u1, u2) { - assert.ifError(err); - - user1 = u1; - user2 = u2; - - B.create({ - title: 'the how and why', - _creator: user1, - fans: [user1, user2], - comments: [{ _creator: user2, content: 'user2' }, { _creator: user1, content: 'user1' }] - }, function(err, p) { - assert.ifError(err); - post = p; - done(); - }); + const [u1, u2] = await User.create([ + { + name: 'Phoenix', + email: 'phx@az.com', + blogposts: [_id] + }, + { + name: 'Newark', + email: 'ewr@nj.com', + blogposts: [_id] + } + ]); + user1 = u1; + user2 = u2; + + post = await B.create({ + title: 'the how and why', + _creator: user1, + fans: [user1, user2], + comments: [{ _creator: user2, content: 'user2' }, { _creator: user1, content: 'user1' }] }); }); @@ -147,111 +143,103 @@ describe('document.populate', function() { }); describe('populating two paths', function() { - it('with space delmited string works', function(done) { - B.findById(post, function(err, post) { - const creator_id = post._creator; - const alt_id = post.fans[1]; - post.populate('_creator fans', function(err) { - assert.ifError(err); - assert.ok(post._creator); - assert.equal(String(post._creator._id), String(creator_id)); - assert.equal(String(post.fans[0]._id), String(creator_id)); - assert.equal(String(post.fans[1]._id), String(alt_id)); - done(); - }); - }); + it('with space delmited string works', async function() { + const p = await B.findById(post); + const creator_id = p._creator; + const alt_id = p.fans[1]; + + await p.populate('_creator fans'); + + assert.ok(p._creator); + assert.equal(String(p._creator._id), String(creator_id)); + assert.equal(String(p.fans[0]._id), String(creator_id)); + assert.equal(String(p.fans[1]._id), String(alt_id)); }); }); - it('works with just a callback', function(done) { - B.findById(post, function(err, post) { - const creator_id = post._creator; - const alt_id = post.fans[1]; - post.populate('_creator', function(err) { - assert.ifError(err); - assert.ok(post._creator); - assert.equal(String(post._creator._id), String(creator_id)); - assert.equal(String(post.fans[1]), String(alt_id)); - done(); - }); - }); + it('works with await', async function() { + const p = await B.findById(post); + const creator_id = p._creator; + const alt_id = p.fans[1]; + + await p.populate('_creator'); + + assert.ok(post._creator); + assert.equal(String(p._creator._id), String(creator_id)); + assert.equal(String(p.fans[1]), String(alt_id)); }); - it('populating using space delimited paths with options', function(done) { - B.findById(post, function(err, post) { - const param = {}; - param.select = '-email'; - param.options = { sort: 'name' }; - param.path = '_creator fans'; // 2 paths - - const creator_id = post._creator; - const alt_id = post.fans[1]; - post.populate(param, function(err, post) { - assert.ifError(err); - assert.equal(post.fans.length, 2); - assert.equal(String(post._creator._id), String(creator_id)); - assert.equal(String(post.fans[1]._id), String(creator_id)); - assert.equal(String(post.fans[0]._id), String(alt_id)); - assert.ok(!post.fans[0].email); - assert.ok(!post.fans[1].email); - assert.ok(!post.fans[0].isInit('email')); - assert.ok(!post.fans[0].isInit(['email'])); - assert.ok(!post.fans[1].isInit('email')); - done(); - }); - }); + it('populating using space delimited paths with options', async function() { + const p = await B.findById(post); + + const param = {}; + param.select = '-email'; + param.options = { sort: 'name' }; + param.path = '_creator fans'; // 2 paths + + const creator_id = post._creator._id; + const alt_id = post.fans[1]._id; + + await p.populate(param); + + assert.equal(p.fans.length, 2); + assert.equal(String(p._creator._id), String(creator_id)); + assert.equal(String(p.fans[1]._id), String(creator_id)); + assert.equal(String(p.fans[0]._id), String(alt_id)); + assert.ok(!p.fans[0].email); + assert.ok(!p.fans[1].email); + assert.ok(!p.fans[0].isInit('email')); + assert.ok(!p.fans[0].isInit(['email'])); + assert.ok(!p.fans[1].isInit('email')); }); - it('using multiple populate calls', function(done) { - B.findById(post, function(err, post) { - const creator_id = post._creator; - const alt_id = post.fans[1]; - - const param = {}; - param.select = '-email'; - param.options = { sort: 'name' }; - param.path = '_creator'; - post.populate(param); - param.path = 'fans'; - - post.populate(param, function(err, post) { - assert.ifError(err); - assert.equal(post.fans.length, 2); - assert.equal(String(post._creator._id), String(creator_id)); - assert.equal(String(post.fans[1]._id), String(creator_id)); - assert.equal(String(post.fans[0]._id), String(alt_id)); - assert.ok(!post.fans[0].email); - assert.ok(!post.fans[1].email); - assert.ok(!post.fans[0].isInit('email')); - assert.ok(!post.fans[1].isInit('email')); - done(); - }); - }); + it('using multiple populate calls', async function() { + const p = await B.findById(post); + + const creator_id = post._creator._id; + const alt_id = post.fans[1]._id; + + const param = {}; + param.select = '-email'; + param.options = { sort: 'name' }; + param.path = '_creator'; + post.populate(param); + param.path = 'fans'; + + await p.populate(param); + + assert.equal(p.fans.length, 2); + assert.equal(String(p._creator._id), String(creator_id)); + assert.equal(String(p.fans[1]._id), String(creator_id)); + assert.equal(String(p.fans[0]._id), String(alt_id)); + assert.ok(!p.fans[0].email); + assert.ok(!p.fans[1].email); + assert.ok(!p.fans[0].isInit('email')); + assert.ok(!p.fans[1].isInit('email')); }); - it('with custom model selection', function(done) { - B.findById(post, function(err, post) { - const param = {}; - param.select = '-email'; - param.options = { sort: 'name' }; - param.path = '_creator fans'; - param.model = 'User'; - - const creator_id = post._creator; - const alt_id = post.fans[1]; - post.populate(param, function(err, post) { - assert.ifError(err); - assert.equal(post.fans.length, 2); - assert.equal(String(post._creator._id), String(creator_id)); - assert.equal(String(post.fans[1]._id), String(creator_id)); - assert.equal(String(post.fans[0]._id), String(alt_id)); - assert.ok(!post.fans[0].email); - assert.ok(!post.fans[1].email); - assert.ok(!post.fans[0].isInit('email')); - assert.ok(!post.fans[1].isInit('email')); - done(); - }); - }); + it('with custom model selection', async function() { + const p = await B.findById(post); + + const param = {}; + param.select = '-email'; + param.options = { sort: 'name' }; + param.path = '_creator fans'; + param.model = 'User'; + + const creator_id = post._creator._id; + const alt_id = post.fans[1]._id; + + await p.populate(param); + + assert.equal(p.fans.length, 2); + assert.equal(String(p._creator._id), String(creator_id)); + assert.equal(String(p.fans[1]._id), String(creator_id)); + assert.equal(String(p.fans[0]._id), String(alt_id)); + assert.ok(!p.fans[0].email); + assert.ok(!p.fans[1].email); + assert.ok(!p.fans[0].isInit('email')); + assert.ok(!p.fans[1].isInit('email')); }); it('one path, model selection as second argument', async function() { @@ -313,58 +301,33 @@ describe('document.populate', function() { assert.ok(b.fans[0].email); }); - it('a property not in schema', function(done) { - B.findById(post, function(err, post) { - assert.ifError(err); - post.populate('idontexist', function(err) { - assert.ok(err); - - // stuff an ad-hoc value in - post.$__setValue('idontexist', user1._id); - - // populate the non-schema value by passing an explicit model - post.populate({ path: 'idontexist', model: 'User', strictPopulate: false }, function(err, post) { - assert.ifError(err); - assert.ok(post); - assert.equal(user1._id.toString(), post.get('idontexist')._id); - assert.equal(post.get('idontexist').name, 'Phoenix'); - done(); - }); - }); - }); + it('a property not in schema', async function() { + const p = await B.findById(post); + + const err = await p.populate('idontexist').then(() => null, err => err); + assert.ok(err); }); - it('of empty array', function(done) { - B.findById(post, function(err, post) { - post.fans = []; - post.populate('fans', function(err) { - assert.ifError(err); - done(); - }); - }); + it('of empty array', async function() { + const p = await B.findById(post); + p.fans = []; + await p.populate('fans'); }); - it('of array of null/undefined', function(done) { - B.findById(post, function(err, post) { - post.fans = [null, undefined]; - post.populate('fans', function(err) { - assert.ifError(err); - done(); - }); - }); + it('of array of null/undefined', async function() { + const p = await B.findById(post); + + p.fans = [null, undefined]; + await p.populate('fans'); }); - it('of null property', function(done) { - B.findById(post, function(err, post) { - post._creator = null; - post.populate('_creator', function(err) { - assert.ifError(err); - done(); - }); - }); + it('of null property', async function() { + const p = await B.findById(post); + p._creator = null; + await p.populate('_creator'); }); - it('String _ids', function(done) { + it('String _ids', async function() { const UserSchema = new Schema({ _id: String, name: String @@ -381,21 +344,17 @@ describe('document.populate', function() { const alice = new User({ _id: 'alice', name: 'Alice In Wonderland' }); - alice.save(function(err) { - assert.ifError(err); + await alice.save(); - const note = new Note({ author: 'alice', body: 'Buy Milk' }); - note.populate('author', function(err) { - assert.ifError(err); - assert.ok(note.author); - assert.equal(note.author._id, 'alice'); - assert.equal(note.author.name, 'Alice In Wonderland'); - done(); - }); - }); + const note = new Note({ author: 'alice', body: 'Buy Milk' }); + await note.populate('author'); + + assert.ok(note.author); + assert.equal(note.author._id, 'alice'); + assert.equal(note.author.name, 'Alice In Wonderland'); }); - it('Buffer _ids', function(done) { + it('Buffer _ids', async function() { const UserSchema = new Schema({ _id: Buffer, name: String @@ -411,30 +370,21 @@ describe('document.populate', function() { const Note = db.model('Test', NoteSchema); const alice = new User({ _id: new mongoose.Types.Buffer('YWxpY2U=', 'base64'), name: 'Alice' }); + await alice.save(); - alice.save(function(err) { - assert.ifError(err); - - const note = new Note({ author: 'alice', body: 'Buy Milk' }); - note.save(function(err) { - assert.ifError(err); - - Note.findById(note.id, function(err, note) { - assert.ifError(err); - assert.equal(note.author, 'alice'); - note.populate('author', function(err, note) { - assert.ifError(err); - assert.equal(note.body, 'Buy Milk'); - assert.ok(note.author); - assert.equal(note.author.name, 'Alice'); - done(); - }); - }); - }); - }); + let note = new Note({ author: 'alice', body: 'Buy Milk' }); + await note.save(); + + note = await Note.findById(note); + assert.equal(note.author, 'alice'); + await note.populate('author'); + + assert.equal(note.body, 'Buy Milk'); + assert.ok(note.author); + assert.equal(note.author.name, 'Alice'); }); - it('Number _ids', function(done) { + it('Number _ids', async function() { const UserSchema = new Schema({ _id: Number, name: String @@ -450,35 +400,28 @@ describe('document.populate', function() { const Note = db.model('Test', NoteSchema); const alice = new User({ _id: 2359, name: 'Alice' }); + await alice.save(); - alice.save(function(err) { - assert.ifError(err); + const note = new Note({ author: 2359, body: 'Buy Milk' }); + await note.populate('author'); - const note = new Note({ author: 2359, body: 'Buy Milk' }); - note.populate('author', function(err, note) { - assert.ifError(err); - assert.ok(note.author); - assert.equal(note.author._id, 2359); - assert.equal('Alice', note.author.name); - done(); - }); - }); + assert.ok(note.author); + assert.equal(note.author._id, 2359); + assert.equal('Alice', note.author.name); }); describe('sub-level properties', function() { - it('with string arg', function(done) { - B.findById(post, function(err, post) { - assert.ifError(err); - const id0 = post.comments[0]._creator; - const id1 = post.comments[1]._creator; - post.populate('comments._creator', function(err, post) { - assert.ifError(err); - assert.equal(post.comments.length, 2); - assert.equal(post.comments[0]._creator.id, id0); - assert.equal(post.comments[1]._creator.id, id1); - done(); - }); - }); + it('with string arg', async function() { + const p = await B.findById(post); + + const id0 = p.comments[0]._creator; + const id1 = p.comments[1]._creator; + + await p.populate('comments._creator'); + + assert.equal(p.comments.length, 2); + assert.equal(p.comments[0]._creator.id, id0); + assert.equal(p.comments[1]._creator.id, id1); }); }); @@ -559,7 +502,7 @@ describe('document.populate', function() { }); describe('gh-7889', function() { - it('should save item added to array after populating the array', function(done) { + it('should save item added to array after populating the array', function() { const Car = db.model('Car', { model: Number }); @@ -570,7 +513,7 @@ describe('document.populate', function() { let person; - Car.create({ model: 0 }).then(car => { + return Car.create({ model: 0 }).then(car => { return Person.create({ cars: [car._id] }); }).then(() => { return Person.findOne({}); @@ -591,7 +534,6 @@ describe('document.populate', function() { return Person.findOne({}); }).then(person => { assert.equal(person.cars.length, 3); - done(); }); }); }); @@ -663,24 +605,18 @@ describe('document.populate', function() { assert.ok(!band.populated('lead')); }); - it('doesn\'t throw when called on a doc that is not populated (gh-6075)', function(done) { + it('doesn\'t throw when called on a doc that is not populated (gh-6075)', async function() { const Person = db.model('Person', { name: String }); const person = new Person({ name: 'Greg Dulli' }); - person.save(function(err, doc) { - assert.ifError(err); - try { - doc.depopulate(); - } catch (e) { - assert.ifError(e); - } - done(); - }); + await person.save(); + + await person.depopulate(); }); - it('depopulates virtuals (gh-6075)', function(done) { + it('depopulates virtuals (gh-6075)', async function() { const otherSchema = new Schema({ val: String, prop: String @@ -726,27 +662,21 @@ describe('document.populate', function() { single: others[1].val }); - Other.create(others). - then(() => { - return test.save(); - }). - then((saved) => { - return saved.populate('$others $single'); - }). - then((populated) => { - assert.strictEqual(populated.$others.length, 3); - assert.strictEqual(populated.$single.prop, 'xyzb'); - populated.depopulate(); - assert.equal(populated.$others, null); - assert.equal(populated.$single, null); - return populated.populate('$last'); - }). - then((populatedAgain) => { - assert.strictEqual(populatedAgain.$last.prop, 'xyzb'); - populatedAgain.depopulate('$last'); - assert.equal(populatedAgain.$last, null); - done(); - }); + await Other.create(others); + await test.save(); + await test.populate('$others $single'); + + assert.strictEqual(test.$others.length, 3); + assert.strictEqual(test.$single.prop, 'xyzb'); + test.depopulate(); + assert.equal(test.$others, null); + assert.equal(test.$single, null); + + await test.populate('$last'); + + assert.strictEqual(test.$last.prop, 'xyzb'); + test.depopulate('$last'); + assert.equal(test.$last, null); }); it('depopulates field with empty array (gh-7740)', async function() { diff --git a/test/document.test.js b/test/document.test.js index 977f933c6e9..115d87a10d8 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -1035,6 +1035,25 @@ describe('document', function() { assert.ok(posts[0].postedBy._id); }); + it('handles infinite recursion (gh-11756)', function() { + const User = db.model('User', Schema({ + name: { type: String, required: true }, + posts: [{ type: mongoose.Types.ObjectId, ref: 'Post' }] + })); + + const Post = db.model('Post', Schema({ + creator: { type: Schema.Types.ObjectId, ref: 'User' } + })); + + const user = new User({ name: 'Test', posts: [] }); + const post = new Post({ creator: user }); + user.posts.push(post); + + const inspected = post.inspect(); + assert.ok(inspected); + assert.equal(inspected.creator.posts[0].creator.name, 'Test'); + }); + it('populate on nested path (gh-5703)', function() { const toySchema = new mongoose.Schema({ color: String }); const Toy = db.model('Cat', toySchema); @@ -11325,4 +11344,114 @@ describe('document', function() { assert.ok(err.message.includes('failed for path'), err.message); assert.ok(err.message.includes('value `-1`'), err.message); }); + + it('avoids setting nested paths to null when they are set to `undefined` (gh-11723)', async function() { + const nestedSchema = new mongoose.Schema({ + count: Number + }, { _id: false }); + + const mySchema = new mongoose.Schema({ + name: String, + nested: { count: Number }, + nestedSchema: nestedSchema + }, { minimize: false }); + + const Test = db.model('Test', mySchema); + + const instance1 = new Test({ name: 'test1', nested: { count: 1 }, nestedSchema: { count: 1 } }); + await instance1.save(); + + const update = { nested: { count: undefined }, nestedSchema: { count: undefined } }; + instance1.set(update); + await instance1.save(); + + const doc = await Test.findById(instance1); + assert.strictEqual(doc.nested.count, undefined); + assert.strictEqual(doc.nestedSchema.count, undefined); + }); + + it('cleans modified subpaths when setting nested path under array to null when subpaths are modified (gh-11764)', async function() { + const Test = db.model('Test', new Schema({ + list: [{ + quantity: { + value: Number, + unit: String + } + }] + })); + + let doc = await Test.create({ list: [{ quantity: { value: 1, unit: 'case' } }] }); + + doc = await Test.findById(doc); + doc.list[0].quantity.value = null; + doc.list[0].quantity.unit = null; + doc.list[0].quantity = null; + + await doc.save(); + + doc = await Test.findById(doc); + assert.strictEqual(doc.list[0].toObject().quantity, null); + }); + + it('avoids manually populating document that is manually populated in another doc with different unpopulatedValue (gh-11442) (gh-11008)', async function() { + const BarSchema = new Schema({ + name: String, + more: String + }); + const Bar = db.model('Bar', BarSchema); + + // Denormalised Bar schema with just the name, for use on the Foo model + const BarNameSchema = new Schema({ + _id: { + type: Schema.Types.ObjectId, + ref: 'Bar' + }, + name: String + }); + + // Foo model, which contains denormalized bar data (just the name) + const FooSchema = new Schema({ + something: String, + other: Number, + bar: { + type: BarNameSchema, + ref: 'Bar' + } + }); + const Foo = db.model('Foo', FooSchema); + + const Baz = db.model('Baz', new Schema({ bar: { type: 'ObjectId', ref: 'Bar' } })); + + const bar = await Bar.create({ + name: 'I am another Bar', + more: 'With even more data' + }); + const foo = await Foo.create({ + something: 'I am another Foo', + other: 4 + }); + foo.bar = bar; + const baz = await Baz.create({}); + baz.bar = bar; + + assert.ok(foo.populated('bar')); + assert.ok(!baz.populated('bar')); + + let res = foo.toObject({ depopulate: true }); + assert.strictEqual(res.bar._id.toString(), bar._id.toString()); + assert.strictEqual(res.bar.name, 'I am another Bar'); + + res = baz.toObject({ depopulate: true }); + assert.strictEqual(res.bar.toString(), bar._id.toString()); + + const bar2 = await Bar.create({ + name: 'test2' + }); + baz.bar = bar2; + assert.ok(baz.populated('bar')); + + const baz2 = await Baz.create({}); + baz2.bar = bar2; + assert.ok(baz.populated('bar')); + }); }); diff --git a/test/helpers/isBsonType.test.js b/test/helpers/isBsonType.test.js index 54396787926..61ae5adc216 100644 --- a/test/helpers/isBsonType.test.js +++ b/test/helpers/isBsonType.test.js @@ -3,6 +3,9 @@ const assert = require('assert'); const isBsonType = require('../../lib/helpers/isBsonType'); +const Decimal128 = require('mongodb').Decimal128; +const ObjectId = require('mongodb').ObjectId; + describe('isBsonType', () => { it('true for any object with _bsontype property equal typename', () => { assert.ok(isBsonType({ _bsontype: 'MyType' }, 'MyType')); @@ -19,4 +22,12 @@ describe('isBsonType', () => { it('false for any object without _bsontype property', () => { assert.ok(!isBsonType({ }, 'OtherType')); }); + + it('true for Decimal128', () => { + assert.ok(isBsonType(new Decimal128('123'), 'Decimal128')); + }); + + it('true for ObjectId', () => { + assert.ok(isBsonType(new ObjectId(), 'ObjectID')); + }); }); \ No newline at end of file diff --git a/test/model.discriminator.test.js b/test/model.discriminator.test.js index 37d3ddd2013..337dae25d6d 100644 --- a/test/model.discriminator.test.js +++ b/test/model.discriminator.test.js @@ -1958,4 +1958,31 @@ describe('model', function() { assert(array.arrayEvent[0].element); }); + + it('handles discriminators on maps of subdocuments (gh-11720)', async function() { + const shapeSchema = Schema({ name: String }, { discriminatorKey: 'kind' }); + const schema = Schema({ shape: { type: Map, of: shapeSchema } }); + + schema.path('shape.$*').discriminator('Circle', Schema({ radius: String })); + schema.path('shape.$*').discriminator('Square', Schema({ side: Number })); + + const Test = db.model('Test', schema); + + let doc = new Test({ + shape: { + a: { kind: 'Circle', radius: 5 }, + b: { kind: 'Square', side: 10 } + } + }); + + assert.strictEqual(doc.shape.get('a').radius, '5'); + assert.strictEqual(doc.shape.get('b').side, 10); + + await doc.save(); + + doc = await Test.findById(doc); + + assert.strictEqual(doc.shape.get('a').radius, '5'); + assert.strictEqual(doc.shape.get('b').side, 10); + }); }); diff --git a/test/model.test.js b/test/model.test.js index 10ff93d0c89..52a864058df 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -5189,7 +5189,7 @@ describe('Model', function() { it('watch() before connecting (gh-5964)', async function() { const db = start(); - const MyModel = db.model('Test', new Schema({ name: String })); + const MyModel = db.model('Test5964', new Schema({ name: String })); // Synchronous, before connection happens const changeStream = MyModel.watch(); @@ -8503,24 +8503,6 @@ describe('Model', function() { assert.deepEqual(indexes[1].key, { name: 1 }); assert.strictEqual(indexes[1].collation.locale, 'en'); }); - it('prevents .$* from being put in projections by avoiding drilling down into maps (gh-11698)', async function() { - const subSchema = new Schema({ - selected: { type: Number }, - not_selected: { type: Number, select: false } - }); - - const testSchema = new Schema({ - subdocument_mapping: { - type: Map, - of: subSchema - } - }); - - - const Test = db.model('Test', testSchema); - - assert.ok(await Test.find()); - }); }); diff --git a/test/schema.test.js b/test/schema.test.js index c47cdd4b11c..5acbb08102d 100644 --- a/test/schema.test.js +++ b/test/schema.test.js @@ -2763,4 +2763,20 @@ describe('schema', function() { }); assert(batch.message); }); + + it('disallows using schemas with schema-level projections with map subdocuments (gh-11698)', async function() { + const subSchema = new Schema({ + selected: { type: Number }, + not_selected: { type: Number, select: false } + }); + + assert.throws(() => { + new Schema({ + subdocument_mapping: { + type: Map, + of: subSchema + } + }); + }, /Cannot use schema-level projections.*subdocument_mapping.not_selected/); + }); }); diff --git a/test/types/connection.test.ts b/test/types/connection.test.ts index 9f07632bbd2..3f2bc848d34 100644 --- a/test/types/connection.test.ts +++ b/test/types/connection.test.ts @@ -38,6 +38,7 @@ expectType(conn.dropCollection('some', () => { expectError(conn.deleteModel()); expectType(conn.deleteModel('something')); +expectType(conn.deleteModel(/.+/)); expectType>(conn.modelNames()); @@ -62,6 +63,10 @@ expectType>(conn.transaction(async(res) => { expectType(res); return 'a'; })); +expectType>(conn.transaction(async(res) => { + expectType(res); + return 'a'; +}, { readConcern: 'majority' })); expectError(conn.user = 'invalid'); expectError(conn.pass = 'invalid'); diff --git a/test/types/populate.test.ts b/test/types/populate.test.ts index 88bad155b4d..e122cf7f47c 100644 --- a/test/types/populate.test.ts +++ b/test/types/populate.test.ts @@ -11,7 +11,7 @@ const childSchema: Schema = new Schema({ name: String }); const ChildModel = model('Child', childSchema); interface Parent { - child?: PopulatedDoc>, + child: PopulatedDoc & Child>, name?: string } @@ -20,7 +20,7 @@ const ParentModel = model('Parent', new Schema({ name: String })); -ParentModel.findOne({}).populate('child').orFail().then((doc: Parent & Document) => { +ParentModel.findOne({}).populate('child').orFail().then((doc: Document & Parent) => { const child = doc.child; if (child == null || child instanceof ObjectId) { throw new Error('should be populated'); diff --git a/test/types/queryhelpers.test.ts b/test/types/queryhelpers.test.ts new file mode 100644 index 00000000000..c4cec3d1c26 --- /dev/null +++ b/test/types/queryhelpers.test.ts @@ -0,0 +1,27 @@ +import { HydratedDocument, Model, Query, Schema, model } from 'mongoose'; + +interface Project { + name: string; + stars: number; +} + +type ProjectModelType = Model; +// Query helpers should return `Query> & ProjectQueryHelpers` +// to enable chaining. +type ProjectModelQuery = Query, ProjectQueryHelpers> & ProjectQueryHelpers; +interface ProjectQueryHelpers { + byName(this: ProjectModelQuery, name: string): ProjectModelQuery; +} + +const schema = new Schema({ + name: { type: String, required: true }, + stars: { type: Number, required: true } +}); +schema.query.byName = function(name: string): ProjectModelQuery { + return this.find({ name: name }); +}; + +// 2nd param to `model()` is the Model class to return. +const ProjectModel = model('Project', schema); + +ProjectModel.find().where('stars').gt(1000).byName('mongoose').exec(); \ No newline at end of file diff --git a/test/types/schemaTypeOptions.test.ts b/test/types/schemaTypeOptions.test.ts new file mode 100644 index 00000000000..9c9c897e474 --- /dev/null +++ b/test/types/schemaTypeOptions.test.ts @@ -0,0 +1,33 @@ +import { + AnyArray, + Schema, + SchemaDefinition, + SchemaType, + SchemaTypeOptions, + Types, + ObjectId, + Unpacked, + + BooleanSchemaDefinition, + DateSchemaDefinition, + NumberSchemaDefinition, + ObjectIdSchemaDefinition, + StringSchemaDefinition +} from 'mongoose'; +import { expectType } from 'tsd'; + +(new SchemaTypeOptions()) instanceof SchemaTypeOptions; + +expectType(new SchemaTypeOptions().type); +expectType(new SchemaTypeOptions().type); +expectType(new SchemaTypeOptions().type); +expectType(new SchemaTypeOptions().type); +expectType | undefined>(new SchemaTypeOptions>().type); +expectType | undefined>(new SchemaTypeOptions().type); +expectType(new SchemaTypeOptions().type); +expectType | AnyArray> | undefined>(new SchemaTypeOptions().type); +expectType<(AnyArray> | AnyArray>> | AnyArray>>) | undefined>(new SchemaTypeOptions().type); +expectType | AnyArray> | undefined>(new SchemaTypeOptions().type); +expectType | AnyArray> | undefined>(new SchemaTypeOptions().type); +expectType | AnyArray> | undefined>(new SchemaTypeOptions().type); +expectType<(Function | typeof SchemaType | Schema | SchemaDefinition | Function | AnyArray) | undefined>(new SchemaTypeOptions().type); diff --git a/test/types/virtuals.test.ts b/test/types/virtuals.test.ts index 1dae77adf52..2011634bd5f 100644 --- a/test/types/virtuals.test.ts +++ b/test/types/virtuals.test.ts @@ -1,4 +1,5 @@ import { Document, Model, Schema, model } from 'mongoose'; +import { expectType } from 'tsd'; interface IPerson { _id: number; @@ -16,6 +17,10 @@ interface IPet { owner: IPerson; } +interface PetVirtuals { + owner: IPerson; +} + const personSchema = new Schema, IPerson>({ _id: { type: Number, required: true }, firstName: { type: String, required: true }, @@ -71,3 +76,13 @@ const Pet = model('Pet', petSchema); const pet = await Pet.findOne().orFail().populate('owner'); console.log(pet.owner.fullName); // John Wick })(); + +function gh11543() { + const personSchema = new Schema, {}, {}, PetVirtuals>({ + _id: { type: Number, required: true }, + firstName: { type: String, required: true }, + lastName: { type: String, required: true } + }); + + expectType(personSchema.virtuals); +} diff --git a/types/connection.d.ts b/types/connection.d.ts index a88d275beae..57dca6b1e3b 100644 --- a/types/connection.d.ts +++ b/types/connection.d.ts @@ -72,7 +72,7 @@ declare module 'mongoose' { * use this function to clean up any models you created in your tests to * prevent OverwriteModelErrors. */ - deleteModel(name: string): this; + deleteModel(name: string | RegExp): this; /** * Helper for `dropCollection()`. Will delete the given collection, including @@ -196,7 +196,7 @@ declare module 'mongoose' { * async function executes successfully and attempt to retry if * there was a retryable error. */ - transaction(fn: (session: mongodb.ClientSession) => Promise): Promise; + transaction(fn: (session: mongodb.ClientSession) => Promise, options?: mongodb.TransactionOptions): Promise; /** Switches to a different database using the same connection pool. */ useDb(name: string, options?: { useCache?: boolean, noListener?: boolean }): Connection; diff --git a/types/index.d.ts b/types/index.d.ts index 518784e20f6..2d90edcf312 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -6,6 +6,7 @@ /// /// /// +/// declare class NativeDate extends global.Date { } @@ -134,7 +135,7 @@ declare module 'mongoose' { export function model(name: string, schema?: Schema | Schema, collection?: string, options?: CompileModelOptions): Model; export function model( name: string, - schema?: Schema, + schema?: Schema, collection?: string, options?: CompileModelOptions ): U; @@ -840,7 +841,7 @@ declare module 'mongoose' { export type PostMiddlewareFunction = (this: ThisType, res: ResType, next: CallbackWithoutResultAndOptionalError) => void | Promise; export type ErrorHandlingMiddlewareFunction = (this: ThisType, err: NativeError, res: ResType, next: CallbackWithoutResultAndOptionalError) => void; - export class Schema, TInstanceMethods = {}, TQueryHelpers = {}> extends events.EventEmitter { + export class Schema, TInstanceMethods = {}, TQueryHelpers = {}, TVirtuals = any> extends events.EventEmitter { /** * Create a new schema */ @@ -981,7 +982,7 @@ declare module 'mongoose' { ): VirtualType; /** Object of currently defined virtuals on this schema */ - virtuals: any; + virtuals: TVirtuals; /** Returns the virtual type with the given `name`. */ virtualpath>(name: string): VirtualType | null; @@ -1032,148 +1033,6 @@ declare module 'mongoose' { type: typeof Schema.Types.Mixed; } - export interface SchemaTypeOptions { - type?: - T extends string ? StringSchemaDefinition : - T extends number ? NumberSchemaDefinition : - T extends boolean ? BooleanSchemaDefinition : - T extends NativeDate ? DateSchemaDefinition : - T extends Map ? SchemaDefinition : - T extends Buffer ? SchemaDefinition : - T extends Types.ObjectId ? ObjectIdSchemaDefinition : - T extends Types.ObjectId[] ? AnyArray | AnyArray> : - T extends object[] ? (AnyArray> | AnyArray>> | AnyArray>>) : - T extends string[] ? AnyArray | AnyArray> : - T extends number[] ? AnyArray | AnyArray> : - T extends boolean[] ? AnyArray | AnyArray> : - T extends Function[] ? AnyArray | AnyArray>> : - T | typeof SchemaType | Schema | SchemaDefinition | Function | AnyArray; - - /** Defines a virtual with the given name that gets/sets this path. */ - alias?: string; - - /** Function or object describing how to validate this schematype. See [validation docs](https://mongoosejs.com/docs/validation.html). */ - validate?: SchemaValidator | AnyArray>; - - /** Allows overriding casting logic for this individual path. If a string, the given string overwrites Mongoose's default cast error message. */ - cast?: string; - - /** - * If true, attach a required validator to this path, which ensures this path - * path cannot be set to a nullish value. If a function, Mongoose calls the - * function and only checks for nullish values if the function returns a truthy value. - */ - required?: boolean | (() => boolean) | [boolean, string] | [() => boolean, string]; - - /** - * The default value for this path. If a function, Mongoose executes the function - * and uses the return value as the default. - */ - default?: T extends Schema.Types.Mixed ? ({} | ((this: any, doc: any) => any)) : (ExtractMongooseArray | ((this: any, doc: any) => Partial>)); - - /** - * The model that `populate()` should use if populating this path. - */ - ref?: string | Model | ((this: any, doc: any) => string | Model); - - /** - * Whether to include or exclude this path by default when loading documents - * using `find()`, `findOne()`, etc. - */ - select?: boolean | number; - - /** - * If [truthy](https://masteringjs.io/tutorials/fundamentals/truthy), Mongoose will - * build an index on this path when the model is compiled. - */ - index?: boolean | number | IndexOptions | '2d' | '2dsphere' | 'hashed' | 'text'; - - /** - * If [truthy](https://masteringjs.io/tutorials/fundamentals/truthy), Mongoose - * will build a unique index on this path when the - * model is compiled. [The `unique` option is **not** a validator](/docs/validation.html#the-unique-option-is-not-a-validator). - */ - unique?: boolean | number; - - /** - * If [truthy](https://masteringjs.io/tutorials/fundamentals/truthy), Mongoose will - * disallow changes to this path once the document is saved to the database for the first time. Read more - * about [immutability in Mongoose here](http://thecodebarbarian.com/whats-new-in-mongoose-5-6-immutable-properties.html). - */ - immutable?: boolean | ((this: any, doc: any) => boolean); - - /** - * If [truthy](https://masteringjs.io/tutorials/fundamentals/truthy), Mongoose will - * build a sparse index on this path. - */ - sparse?: boolean | number; - - /** - * If [truthy](https://masteringjs.io/tutorials/fundamentals/truthy), Mongoose - * will build a text index on this path. - */ - text?: boolean | number | any; - - /** - * Define a transform function for this individual schema type. - * Only called when calling `toJSON()` or `toObject()`. - */ - transform?: (this: any, val: T) => any; - - /** defines a custom getter for this property using [`Object.defineProperty()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty). */ - get?: (value: any, doc?: this) => T; - - /** defines a custom setter for this property using [`Object.defineProperty()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty). */ - set?: (value: any, priorVal?: T, doc?: this) => any; - - /** array of allowed values for this path. Allowed for strings, numbers, and arrays of strings */ - enum?: Array | ReadonlyArray | { values: Array | ReadonlyArray, message?: string } | { [path: string]: string | number | null }; - - /** The default [subtype](http://bsonspec.org/spec.html) associated with this buffer when it is stored in MongoDB. Only allowed for buffer paths */ - subtype?: number; - - /** The minimum value allowed for this path. Only allowed for numbers and dates. */ - min?: number | NativeDate | [number, string] | [NativeDate, string] | readonly [number, string] | readonly [NativeDate, string]; - - /** The maximum value allowed for this path. Only allowed for numbers and dates. */ - max?: number | NativeDate | [number, string] | [NativeDate, string] | readonly [number, string] | readonly [NativeDate, string]; - - /** Defines a TTL index on this path. Only allowed for dates. */ - expires?: string | number; - - /** If `true`, Mongoose will skip gathering indexes on subpaths. Only allowed for subdocuments and subdocument arrays. */ - excludeIndexes?: boolean; - - /** If set, overrides the child schema's `_id` option. Only allowed for subdocuments and subdocument arrays. */ - _id?: boolean; - - /** If set, specifies the type of this map's values. Mongoose will cast this map's values to the given type. */ - of?: Function | SchemaDefinitionProperty; - - /** If true, uses Mongoose's default `_id` settings. Only allowed for ObjectIds */ - auto?: boolean; - - /** Attaches a validator that succeeds if the data string matches the given regular expression, and fails otherwise. */ - match?: RegExp | [RegExp, string] | readonly [RegExp, string]; - - /** If truthy, Mongoose will add a custom setter that lowercases this string using JavaScript's built-in `String#toLowerCase()`. */ - lowercase?: boolean; - - /** If truthy, Mongoose will add a custom setter that removes leading and trailing whitespace using JavaScript's built-in `String#trim()`. */ - trim?: boolean; - - /** If truthy, Mongoose will add a custom setter that uppercases this string using JavaScript's built-in `String#toUpperCase()`. */ - uppercase?: boolean; - - /** If set, Mongoose will add a custom validator that ensures the given string's `length` is at least the given number. */ - minlength?: number | [number, string] | readonly [number, string]; - - /** If set, Mongoose will add a custom validator that ensures the given string's `length` is at most the given number. */ - maxlength?: number | [number, string] | readonly [number, string]; - - [other: string]: any; - } - export type RefType = | number | string diff --git a/types/schematypeoptions.d.ts b/types/schematypeoptions.d.ts new file mode 100644 index 00000000000..49d8b248926 --- /dev/null +++ b/types/schematypeoptions.d.ts @@ -0,0 +1,144 @@ +declare module 'mongoose' { + + export class SchemaTypeOptions { + type?: + T extends string ? StringSchemaDefinition : + T extends number ? NumberSchemaDefinition : + T extends boolean ? BooleanSchemaDefinition : + T extends NativeDate ? DateSchemaDefinition : + T extends Map ? SchemaDefinition : + T extends Buffer ? SchemaDefinition : + T extends Types.ObjectId ? ObjectIdSchemaDefinition : + T extends Types.ObjectId[] ? AnyArray | AnyArray> : + T extends object[] ? (AnyArray> | AnyArray>> | AnyArray>>) : + T extends string[] ? AnyArray | AnyArray> : + T extends number[] ? AnyArray | AnyArray> : + T extends boolean[] ? AnyArray | AnyArray> : + T extends Function[] ? AnyArray | AnyArray>> : + T | typeof SchemaType | Schema | SchemaDefinition | Function | AnyArray; + + /** Defines a virtual with the given name that gets/sets this path. */ + alias?: string; + + /** Function or object describing how to validate this schematype. See [validation docs](https://mongoosejs.com/docs/validation.html). */ + validate?: SchemaValidator | AnyArray>; + + /** Allows overriding casting logic for this individual path. If a string, the given string overwrites Mongoose's default cast error message. */ + cast?: string; + + /** + * If true, attach a required validator to this path, which ensures this path + * path cannot be set to a nullish value. If a function, Mongoose calls the + * function and only checks for nullish values if the function returns a truthy value. + */ + required?: boolean | (() => boolean) | [boolean, string] | [() => boolean, string]; + + /** + * The default value for this path. If a function, Mongoose executes the function + * and uses the return value as the default. + */ + default?: T extends Schema.Types.Mixed ? ({} | ((this: any, doc: any) => any)) : (ExtractMongooseArray | ((this: any, doc: any) => Partial>)); + + /** + * The model that `populate()` should use if populating this path. + */ + ref?: string | Model | ((this: any, doc: any) => string | Model); + + /** + * Whether to include or exclude this path by default when loading documents + * using `find()`, `findOne()`, etc. + */ + select?: boolean | number; + + /** + * If [truthy](https://masteringjs.io/tutorials/fundamentals/truthy), Mongoose will + * build an index on this path when the model is compiled. + */ + index?: boolean | number | IndexOptions | '2d' | '2dsphere' | 'hashed' | 'text'; + + /** + * If [truthy](https://masteringjs.io/tutorials/fundamentals/truthy), Mongoose + * will build a unique index on this path when the + * model is compiled. [The `unique` option is **not** a validator](/docs/validation.html#the-unique-option-is-not-a-validator). + */ + unique?: boolean | number; + + /** + * If [truthy](https://masteringjs.io/tutorials/fundamentals/truthy), Mongoose will + * disallow changes to this path once the document is saved to the database for the first time. Read more + * about [immutability in Mongoose here](http://thecodebarbarian.com/whats-new-in-mongoose-5-6-immutable-properties.html). + */ + immutable?: boolean | ((this: any, doc: any) => boolean); + + /** + * If [truthy](https://masteringjs.io/tutorials/fundamentals/truthy), Mongoose will + * build a sparse index on this path. + */ + sparse?: boolean | number; + + /** + * If [truthy](https://masteringjs.io/tutorials/fundamentals/truthy), Mongoose + * will build a text index on this path. + */ + text?: boolean | number | any; + + /** + * Define a transform function for this individual schema type. + * Only called when calling `toJSON()` or `toObject()`. + */ + transform?: (this: any, val: T) => any; + + /** defines a custom getter for this property using [`Object.defineProperty()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty). */ + get?: (value: any, doc?: this) => T; + + /** defines a custom setter for this property using [`Object.defineProperty()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty). */ + set?: (value: any, priorVal?: T, doc?: this) => any; + + /** array of allowed values for this path. Allowed for strings, numbers, and arrays of strings */ + enum?: Array | ReadonlyArray | { values: Array | ReadonlyArray, message?: string } | { [path: string]: string | number | null }; + + /** The default [subtype](http://bsonspec.org/spec.html) associated with this buffer when it is stored in MongoDB. Only allowed for buffer paths */ + subtype?: number; + + /** The minimum value allowed for this path. Only allowed for numbers and dates. */ + min?: number | NativeDate | [number, string] | [NativeDate, string] | readonly [number, string] | readonly [NativeDate, string]; + + /** The maximum value allowed for this path. Only allowed for numbers and dates. */ + max?: number | NativeDate | [number, string] | [NativeDate, string] | readonly [number, string] | readonly [NativeDate, string]; + + /** Defines a TTL index on this path. Only allowed for dates. */ + expires?: string | number; + + /** If `true`, Mongoose will skip gathering indexes on subpaths. Only allowed for subdocuments and subdocument arrays. */ + excludeIndexes?: boolean; + + /** If set, overrides the child schema's `_id` option. Only allowed for subdocuments and subdocument arrays. */ + _id?: boolean; + + /** If set, specifies the type of this map's values. Mongoose will cast this map's values to the given type. */ + of?: Function | SchemaDefinitionProperty; + + /** If true, uses Mongoose's default `_id` settings. Only allowed for ObjectIds */ + auto?: boolean; + + /** Attaches a validator that succeeds if the data string matches the given regular expression, and fails otherwise. */ + match?: RegExp | [RegExp, string] | readonly [RegExp, string]; + + /** If truthy, Mongoose will add a custom setter that lowercases this string using JavaScript's built-in `String#toLowerCase()`. */ + lowercase?: boolean; + + /** If truthy, Mongoose will add a custom setter that removes leading and trailing whitespace using JavaScript's built-in `String#trim()`. */ + trim?: boolean; + + /** If truthy, Mongoose will add a custom setter that uppercases this string using JavaScript's built-in `String#toUpperCase()`. */ + uppercase?: boolean; + + /** If set, Mongoose will add a custom validator that ensures the given string's `length` is at least the given number. */ + minlength?: number | [number, string] | readonly [number, string]; + + /** If set, Mongoose will add a custom validator that ensures the given string's `length` is at most the given number. */ + maxlength?: number | [number, string] | readonly [number, string]; + + [other: string]: any; + } +}