diff --git a/.eslintrc.js b/.eslintrc.js
index 0b98da74134..b4d7d1652d9 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -11,7 +11,10 @@ module.exports = {
'test/files/*',
'benchmarks',
'*.min.js',
- 'docs/js/native.js'
+ '**/docs/js/native.js',
+ '!.*',
+ 'node_modules',
+ '.git'
],
overrides: [
{
@@ -20,6 +23,9 @@ module.exports = {
'**/*.md/*.ts',
'**/*.md/*.typescript'
],
+ parserOptions: {
+ project: './tsconfig.json'
+ },
extends: [
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended'
@@ -50,34 +56,20 @@ module.exports = {
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
- '@typescript-eslint/indent': [
- 'warn',
- 2,
- {
- SwitchCase: 1,
- ignoredNodes: ['TSTypeParameterInstantiation']
- }
- ],
'@typescript-eslint/prefer-optional-chain': 'error',
- '@typescript-eslint/brace-style': 'error',
'@typescript-eslint/no-dupe-class-members': 'error',
'@typescript-eslint/no-redeclare': 'error',
- '@typescript-eslint/type-annotation-spacing': 'error',
- '@typescript-eslint/object-curly-spacing': [
- 'error',
- 'always'
- ],
- '@typescript-eslint/semi': 'error',
- '@typescript-eslint/space-before-function-paren': [
- 'error',
- 'never'
- ],
- '@typescript-eslint/space-infix-ops': 'off'
+ '@typescript-eslint/space-infix-ops': 'off',
+ '@typescript-eslint/no-require-imports': 'off',
+ '@typescript-eslint/no-empty-object-type': 'off',
+ '@typescript-eslint/no-wrapper-object-types': 'off',
+ '@typescript-eslint/no-unused-expressions': 'off',
+ '@typescript-eslint/no-unsafe-function-type': 'off'
}
},
{
files: [
- 'docs/js/**/*.js'
+ '**/docs/js/**/*.js'
],
env: {
node: false,
@@ -116,7 +108,8 @@ module.exports = {
},
env: {
node: true,
- es6: true
+ es6: true,
+ es2020: true
},
rules: {
'comma-style': 'error',
diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml
index 388b9b78a21..5afe4965aeb 100644
--- a/.github/workflows/benchmark.yml
+++ b/.github/workflows/benchmark.yml
@@ -22,13 +22,13 @@ jobs:
runs-on: ubuntu-20.04
name: Benchmark TypeScript Types
steps:
- - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- name: Setup node
- uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
+ uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
- node-version: 16
+ node-version: 22
- run: npm install
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 7df61155d4d..d0cd8cca68d 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -21,11 +21,11 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@v2
+ uses: github/codeql-action/init@v3
with:
config-file: ./.github/codeql/codeql-config.yml
# Override language selection by uncommenting this and choosing your languages
@@ -44,4 +44,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v2
+ uses: github/codeql-action/analyze@v3
diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml
index 7a7ad5f6661..134fbe5ed5e 100644
--- a/.github/workflows/documentation.yml
+++ b/.github/workflows/documentation.yml
@@ -24,18 +24,51 @@ permissions:
contents: read
jobs:
+ lint-documentation:
+ runs-on: ubuntu-latest
+ name: Lint Markdown files
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+
+ - name: Setup node
+ uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
+ with:
+ node-version: 22
+
+ - run: npm install
+
+ - name: Lint MD-Files # run markdownlint
+ run: npm run lint-md
+
+ # enable when "eslint-markdown" can be used without errors
+ # - name: Lint JS-Files # run eslint to lint the code-blocks themself
+ # run: npm run lint-js
+
test-documentation:
runs-on: ubuntu-20.04
name: Test Generating Docs
steps:
- - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: git fetch --depth=1 --tags # download all tags for documentation
- name: Setup node
- uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
+ uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
- node-version: 16
+ node-version: 22
- run: npm install
+ - name: Setup MongoDB
+ run: |
+ wget -q https://downloads.mongodb.org/linux/mongodb-linux-x86_64-ubuntu2004-5.0.14.tgz
+ tar xf mongodb-linux-x86_64-ubuntu2004-5.0.14.tgz
+ mkdir -p ./data/db/27017 ./data/db/27000
+ printf "\n--timeout 8000" >> ./test/mocha.opts
+ ./mongodb-linux-x86_64-ubuntu2004-5.0.14/bin/mongod --setParameter ttlMonitorSleepSecs=1 --fork --dbpath ./data/db/27017 --syslog --port 27017
+ sleep 2
+ mongod --version
+ echo `pwd`/mongodb-linux-x86_64-ubuntu2004-5.0.14/bin >> $GITHUB_PATH
+ - name: Setup config
+ run: |
+ echo "module.exports = {uri:'mongodb://127.0.0.1:27017/mongoose_test'};" > ./.config.js
- run: npm run docs:clean
- run: npm run docs:generate
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
index 1f035a7d146..fe66c0ac97e 100644
--- a/.github/workflows/stale.yml
+++ b/.github/workflows/stale.yml
@@ -11,11 +11,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: stale
- uses: actions/stale@v7
+ uses: actions/stale@v9
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'This issue is stale because it has been open 14 days with no activity. Remove stale label or comment or this will be closed in 5 days'
- close-issue-message: "This issue was closed because it has been inactive for 19 days since being marked as stale."
+ close-issue-message: "This issue was closed because it has been inactive for 19 days and has been marked as stale."
days-before-stale: 14
days-before-close: 5
- any-of-labels: can't reproduce,help,needs clarification
\ No newline at end of file
+ any-of-labels: can't reproduce,help,needs clarification
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index c9b01207ac1..f17b792e2b4 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -22,12 +22,12 @@ jobs:
runs-on: ubuntu-latest
name: Lint JS-Files
steps:
- - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup node
- uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
+ uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
- node-version: 14
+ node-version: 22
- run: npm install
@@ -39,24 +39,35 @@ jobs:
strategy:
fail-fast: false
matrix:
- node: [12, 14, 16, 18]
- os: [ubuntu-20.04]
- mongodb: [4.0.28, 5.0.8, 6.0.0]
+ node: [16, 18, 20, 22]
+ os: [ubuntu-20.04, ubuntu-22.04]
+ mongodb: [4.4.29, 5.0.26, 6.0.15, 7.0.12, 8.0.0]
+ include:
+ - os: ubuntu-20.04 # customize on which matrix the coverage will be collected on
+ mongodb: 5.0.26
+ node: 16
+ coverage: true
+ exclude:
+ - os: ubuntu-22.04 # exclude because there are no 4.x mongodb builds for 2204
+ mongodb: 4.4.29
+ - os: ubuntu-22.04 # exclude because there are no 5.x mongodb builds for 2204
+ mongodb: 5.0.26
name: Node ${{ matrix.node }} MongoDB ${{ matrix.mongodb }} OS ${{ matrix.os }}
env:
MONGOMS_VERSION: ${{ matrix.mongodb }}
MONGOMS_PREFER_GLOBAL_PATH: 1
+ FORCE_COLOR: true
steps:
- - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup node
- uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
+ uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: ${{ matrix.node }}
- name: Load MongoDB binary cache
id: cache-mongodb-binaries
- uses: actions/cache@v3
+ uses: actions/cache@v4
with:
path: ~/.cache/mongodb-binaries
key: ${{ matrix.os }}-${{ matrix.mongodb }}
@@ -69,7 +80,7 @@ jobs:
run: npm run test-coverage
if: matrix.coverage == true
- name: Archive code coverage results
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
if: matrix.coverage == true
with:
name: coverage
@@ -79,24 +90,25 @@ jobs:
runs-on: ubuntu-20.04
name: Deno tests
env:
- MONGOMS_VERSION: 6.0.0
+ MONGOMS_VERSION: 6.0.15
MONGOMS_PREFER_GLOBAL_PATH: 1
+ FORCE_COLOR: true
steps:
- - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup node
- uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
+ uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
- node-version: 16
+ node-version: 22
- name: Load MongoDB binary cache
id: cache-mongodb-binaries
- uses: actions/cache@v3
+ uses: actions/cache@v4
with:
path: ~/.cache/mongodb-binaries
key: deno-${{ env.MONGOMS_VERSION }}
- name: Setup Deno
- uses: denoland/setup-deno@v1
+ uses: denoland/setup-deno@v2
with:
- deno-version: v1.34.x
+ deno-version: v1.37.x
- run: deno --version
- run: npm install
- name: Run Deno tests
@@ -107,12 +119,14 @@ jobs:
- test
runs-on: ubuntu-latest
name: Replica Set tests
+ env:
+ FORCE_COLOR: true
steps:
- - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup node
- uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
+ uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
- node-version: 16
+ node-version: 22
- run: npm install
- name: Test
run: npm run test-rs
@@ -125,6 +139,6 @@ jobs:
contents: read
steps:
- name: Check out repo
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Dependency review
- uses: actions/dependency-review-action@v3
+ uses: actions/dependency-review-action@v4
diff --git a/.github/workflows/tidelift-alignment.yml b/.github/workflows/tidelift-alignment.yml
index b3b245f2248..552493a7cbc 100644
--- a/.github/workflows/tidelift-alignment.yml
+++ b/.github/workflows/tidelift-alignment.yml
@@ -15,11 +15,11 @@ jobs:
if: github.repository == 'Automattic/mongoose'
steps:
- name: Checkout
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup node
- uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
+ uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
- node-version: 16
+ node-version: 22
- name: Alignment
uses: tidelift/alignment-action@8d7700fe795fc01179c1f9fa05b72a089873027d # main
env:
diff --git a/.github/workflows/tsd.yml b/.github/workflows/tsd.yml
index 2a220877fea..672bd36229f 100644
--- a/.github/workflows/tsd.yml
+++ b/.github/workflows/tsd.yml
@@ -7,8 +7,6 @@ on:
- 'types/**'
- 'test/types/**'
push:
- branches:
- - master
paths:
- '.github/workflows/tsd.yml'
- 'package.json'
@@ -22,12 +20,12 @@ jobs:
runs-on: ubuntu-latest
name: Lint TS-Files
steps:
- - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup node
- uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
+ uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
- node-version: 14
+ node-version: 22
- run: npm install
@@ -40,12 +38,12 @@ jobs:
runs-on: ubuntu-latest
name: Test Typescript Types
steps:
- - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup node
- uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
+ uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
- node-version: 12
+ node-version: 22
- run: npm install
diff --git a/.gitignore b/.gitignore
index 3ef56b48a3b..47c0742bb12 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,6 +21,7 @@ npm-debug.log
data/
.nyc_output/
.env
+
tools/31*
# Visual Studio
diff --git a/.markdownlint-cli2.cjs b/.markdownlint-cli2.cjs
new file mode 100644
index 00000000000..5cd93450bb0
--- /dev/null
+++ b/.markdownlint-cli2.cjs
@@ -0,0 +1,69 @@
+'use strict';
+// this file is named ".cjs" instead of ".js" because markdownlint-cli2 only looks for ".cjs" or ".mjs"
+
+// use aliases instead of "MD000" naming
+
+module.exports = {
+ ignores: [
+ // the following are ignored because they are just redirects
+ 'History.md',
+ 'SECURITY.md',
+ 'migrating_to_5.md', // this does not affect "docs/migrating_to_5.md"
+
+ // ignored for now, but should be changes later
+ '.github/PULL_REQUEST_TEMPLATE.md',
+
+ // ignore changelog because it uses different heading style than other documents and older versions use different formatting
+ 'CHANGELOG.md',
+
+ // exclude node_modules because it isnt excluded by default
+ 'node_modules'
+ ]
+};
+
+module.exports.config = {
+ // disable default rules
+ default: false,
+
+ // alt-text for images
+ accessibility: true,
+ // consistent blank lines
+ blank_lines: true,
+ // consistent unordered lists
+ bullet: { style: 'asterisk' },
+ // consistent and read-able code
+ code: true,
+ // consistent emphasis characters
+ emphasis: { style: 'asterisk' },
+ // ensure consistent header usage
+ headers: { style: 'atx' },
+ // ensure consistent "hr" usage
+ hr: { style: '---' },
+ // disable disalloing html tags, because
+ // mongoose currently uses html tags directly for heading ID's and style
+ html: false,
+ // consistent indentation
+ indentation: true,
+ // consistent links and good links
+ links: true,
+ // enabled by "links"
+ // mongoose currently does not wrap plain links in "<>"
+ 'no-bare-urls': false,
+ // consistent ordered lists
+ ol: true,
+ // consistent whitespace usage
+ whitespace: true
+
+ // atx: undefined, // covered by "headers"
+ // atx_closed: undefined, // covered by "headers"
+ // blockquote: true, // covered by "whitespace"
+ // hard_tab: undefined, // covered by "whitespace"
+ // headings: undefined, // covered by "headers"
+ // images: true, // covered by "accessibility" and "links"
+ // language: undefined, // covered by "code"
+ // line_length: undefined, // mongoose currently uses a mix of max-line-length and relying on editor auto-wrap
+ // spaces: undefined, // covered by "atx", "atx_closed", "headers"
+ // spelling: undefined, // mongoose currently only uses short-form language specifiers and so does not need this
+ // ul: undefined, // covered by "whitespace" and "bullet"
+ // url: undefined, // covered by "links"
+};
diff --git a/.mocharc.yml b/.mocharc.yml
index efa9bf8a87b..4ba8d185851 100644
--- a/.mocharc.yml
+++ b/.mocharc.yml
@@ -2,3 +2,7 @@ reporter: spec # better to identify failing / slow tests than "dot"
ui: bdd # explicitly setting, even though it is mocha default
require:
- test/mocha-fixtures.js
+extension:
+ - test.js
+watch-files:
+ - test/**/*.js
diff --git a/.npmignore b/.npmignore
index efbbf2be524..3341fe912a3 100644
--- a/.npmignore
+++ b/.npmignore
@@ -43,3 +43,19 @@ webpack.base.config.js
.nyc-output
*.tgz
+
+notes.md
+list.out
+
+# config files
+lgtm.yml
+.mocharc.yml
+.eslintrc.js
+.markdownlint-cli2.cjs
+tsconfig.json
+
+# scripts
+scripts/
+tools/
+
+*.0x
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fb85f4fcec7..827a8d07aa5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,12 +1,949 @@
+8.9.5 / 2025-01-13
+==================
+ * fix: disallow nested $where in populate match
+ * fix(schema): handle bitwise operators on Int32 #15176 #15170
+
+7.8.4 / 2025-01-13
+===================
+ * fix: disallow nested $where in populate match
+
+6.13.6 / 2025-01-13
+===================
+ * fix: disallow nested $where in populate match
+
+8.9.4 / 2025-01-09
+==================
+ * fix(document): fix document not applying manual populate when using a function in schema.options.ref #15138 [IchirokuXVI](https://github.com/IchirokuXVI)
+ * fix(model): make Model.validate() static correctly cast document arrays #15169 #15164
+ * fix(model): allow passing validateBeforeSave option to bulkSave() to skip validation #15161 #15156
+ * fix(schema): allow multiple self-referencing discriminator schemas using Schema.prototype.discriminator #15142 #15120
+ * types: avoid BufferToBinary<> wiping lean types when passed to generic functions #15160 #15158
+ * docs: fix `` in header ids #15159
+ * docs: fix header in field-level-encryption.md #15137 [damieng](https://github.com/damieng)
+
+8.9.3 / 2024-12-30
+==================
+ * fix(schema): make duplicate index error a warning for now to prevent blocking upgrading #15135 #15112 #15109
+ * fix(model): handle document array paths set to non-array values in Model.castObject() #15124 #15075
+ * fix(document): avoid using childSchemas.path for compatibility with pre-Mongoose-8.8 schemas #15131 #15071
+ * fix(model): avoid throwing unnecessary error if updateOne() returns null in save() #15126
+ * perf(cursor): clear the stack every time if using populate with batchSize to avoid stack overflows with large docs #15136 #10449
+ * types: make BufferToBinary avoid Document instances #15123 #15122
+ * types(model+query): avoid stripping out virtuals when calling populate with paths generic #15132 #15111
+ * types(schema): add missing removeIndex #15134
+ * types: add cleanIndexes() to IndexManager interface #15127
+ * docs: move search endpoint to netlify #15119
+
+8.9.2 / 2024-12-19
+==================
+ * fix(schema): avoid throwing duplicate index error if index spec keys have different order or index has a custom name #15112 #15109
+ * fix(map): clean modified subpaths when overwriting values in map of subdocs #15114 #15108
+ * fix(aggregate): pull session from transaction local storage for aggregation cursors #15094 [IchirokuXVI](https://github.com/IchirokuXVI)
+ * types: correctly handle union types in BufferToBinary and related helpers #15103 #15102 #15057
+ * types: add UUID to RefType #15115 #15101
+ * docs: remove link to Mongoose 5.x docs from dropdown #15116
+ * docs(connection+document+model): remove remaining references to remove(), clarify that deleteOne() does not execute until then() or exec() #15113 #15107
+
+8.9.1 / 2024-12-16
+==================
+ * fix(connection): remove heartbeat check in load balanced mode #15089 #15042 #14812
+ * fix(discriminator): gather childSchemas when creating discriminator to ensure $getAllSubdocs() can properly get all subdocs #15099 #15088 #15092
+ * fix(model): handle discriminators in castObject() #15096 #15075
+ * fix(schema): throw error if duplicate index definition using unique in schema path and subsequent .index() call #15093 #15056
+ * fix: mark documents that are populated using hydratedPopulatedDocs option as populated in top-level doc #15080 #15048
+ * fix(document+schema): improve error message for get() on invalid path #15098 #15071
+ * docs: remove more callback doc references & some small other changes #15095
+
+8.9.0 / 2024-12-13
+==================
+ * feat: upgrade mongodb -> 6.12
+ * feat: add int32 schematype #15054 [aditi-khare-mongoDB](https://github.com/aditi-khare-mongoDB)
+ * feat: add double schematype #15061 [aditi-khare-mongoDB](https://github.com/aditi-khare-mongoDB)
+ * feat: allow specifying error message override for duplicate key errors unique: true #15059 #12844
+ * feat(connection): add support for Connection.prototype.bulkWrite() with MongoDB server 8.0 #15058 #15028
+ * feat: add forceRepopulate option for populate() to allow avoiding repopulating already populated docs #15044 #14979
+ * fix(connection): remove heartbeat check in load balanced mode #15089 #15042
+ * fix(query): clone PopulateOptions when setting _localModel to avoid state leaking between subpopulate instances #15082 #15026
+ * types: add splice() to DocumentArray to allow adding partial objects with splice() #15085 #15041
+ * types(aggregate): add $firstN, $lastN, $bottom, $bottomN, $minN and $maxN operators #15087 [mlomnicki](https://github.com/mlomnicki)
+ * docs: Remove merge conflict markers #15090 [sponrad](https://github.com/sponrad)
+
+8.8.4 / 2024-12-05
+==================
+ * fix: cast using overwritten embedded discriminator key when set #15076 #15051
+ * fix: avoid throwing error if saveOptions undefined when invalidating subdoc cache #15062
+
+8.8.3 / 2024-11-26
+==================
+ * fix: disallow using $where in match
+ * perf: cache results from getAllSubdocs() on saveOptions, only loop through known subdoc properties #15055 #15029
+ * fix(model+query): support overwriteDiscriminatorKey for bulkWrite updateOne and updateMany, allow inferring discriminator key from update #15046 #15040
+
+7.8.3 / 2024-11-26
+==================
+ * fix: disallow using $where in match
+ * fix(projection): avoid setting projection to unknown exclusive/inclusive if elemMatch on a Date, ObjectId, etc. #14894 #14893
+ * docs(migrating_to_7): add note about keepAlive to Mongoose 7 migration guide #15032 #13431
+
+6.13.5 / 2024-11-26
+===================
+ * fix: disallow using $where in match
+
+8.8.2 / 2024-11-18
+==================
+ * fix(model): handle array filters when casting bulkWrite #15036 #14978
+ * fix(model): make diffIndexes() avoid trying to drop default timeseries collection index #15035 #14984
+ * fix: save execution stack in query as string #15039 [durran](https://github.com/durran)
+ * types(cursor): correct asyncIterator and asyncDispose for TypeScript with lib: 'esnext' #15038
+ * docs(migrating_to_8): add note about removing findByIdAndRemove #15024 [dragontaek-lee](https://github.com/dragontaek-lee)
+
+6.13.4 / 2024-11-15
+===================
+ * fix: save execution stack in query as string #15043 #15039
+ * docs: clarify strictQuery default will flip-flop in "Migrating to 6.x" #14998 [markstos](https://github.com/markstos)
+
+8.8.1 / 2024-11-08
+==================
+ * perf: make a few micro-optimizations to help speed up findOne() #15022 #14906
+ * fix: apply embedded discriminators to subdoc schemas before compiling top level model so middleware applies correctly #15001 #14961
+ * fix(query): add overwriteImmutable option to allow updating immutable properties without disabling strict mode #15000 #8619
+
+8.8.0 / 2024-10-31
+==================
+ * feat: upgrade mongodb -> ~6.10 #14991 #14877
+ * feat(query): add schemaLevelProjections option to query to disable schema-level select: false #14986 #11474
+ * feat: allow defining virtuals on arrays, not just array elements #14955 #2326
+ * feat(model): add applyTimestamps() function to apply all schema timestamps, including subdocuments, to a given POJO #14943 #14698
+ * feat(model): add hideIndexes option to syncIndexes() and cleanIndexes() #14987 #14868
+ * fix(query): make sanitizeFilter disable implicit $in #14985 #14657
+ * fix(model): avoid unhandled error if createIndex() throws a sync error #14995
+ * fix(model): avoid throwing TypeError if bulkSave()'s bulkWrite() fails with a non-BulkWriteError #14993
+ * types: added toJSON:flattenObjectIds effect #14989
+ * types: add `__v` to lean() result type and ModifyResult #14990 #12959
+ * types: use globalThis instead of global for NativeDate #14992 #14988
+ * docs(change-streams): fix markdown syntax highlighting for script output example #14994
+
+8.7.3 / 2024-10-25
+==================
+ * fix(cursor): close underlying query cursor when calling destroy() #14982 #14966
+ * types: add JSONSerialized helper that can convert HydratedDocument to JSON output type #14981 #14451
+ * types(model): convert InsertManyResult to interface and remove unnecessary insertedIds override #14977
+ * types(connection): add missing sanitizeFilter option #14975
+ * types: improve goto definition for inferred schema definitions #14968 [forivall](https://github.com/forivall)
+ * docs(migration-guide-v7): correct link to the section "Id Setter" #14973 [rb-ntnx](https://github.com/rb-ntnx)
+
+8.7.2 / 2024-10-17
+==================
+ * fix(document): recursively clear modified subpaths when setting deeply nested subdoc to null #14963 #14952
+ * fix(populate): handle array of ids with parent refPath #14965
+ * types: make Buffers into mongodb.Binary in lean result type to match runtime behavior #14967
+ * types: correct schema type inference when using nested typeKey like type: { type: String } #14956 #14950
+ * types: re-export DeleteResult and UpdateResult from MongoDB Node.js driver #14947 #14946
+ * docs(documents): add section on setting deeply nested properties, including warning about nullish coalescing assignment #14972
+ * docs(model): add more info on acknowledged: false, specifically that Mongoose may return that if the update was empty #14957
+
+8.7.1 / 2024-10-09
+==================
+ * fix: set flattenObjectIds to false when calling toObject() for internal purposes #14938
+ * fix: add mongodb 8 to test matrix #14937
+ * fix: handle buffers stored in MongoDB as EJSON representation with { $binary } #14932
+ * docs: indicate that Mongoose 8.7 is required for full MongoDB 8 support #14937
+
+8.7.0 / 2024-09-27
+==================
+ * feat(model): add Model.applyVirtuals() to apply virtuals to a POJO #14905 #14818
+ * feat: upgrade mongodb -> 6.9.0 #14914
+ * feat(query): cast $rename to string #14887 #3027
+ * feat(SchemaType): add getEmbeddedSchemaType() method to SchemaTypes #14880 #8389
+ * fix(model): throw MongooseBulkSaveIncompleteError if bulkSave() didn't completely succeed #14884 #14763
+ * fix(connection): avoid returning readyState = connected if connection state is stale #14812 #14727
+ * fix: depopulate if push() or addToSet() with an ObjectId on a populated array #14883 #1635
+ * types: make __v a number, only set __v on top-level documents #14892
+
+8.6.4 / 2024-09-26
+==================
+ * fix(document): avoid massive perf degradation when saving new doc with 10 level deep subdocs #14910 #14897
+ * fix(model): skip applying static hooks by default if static name conflicts with aggregate middleware #14904 [dragontaek-lee](https://github.com/dragontaek-lee)
+ * fix(model): filter applying static hooks by default if static name conflicts with mongoose middleware #14908 [dragontaek-lee](https://github.com/dragontaek-lee)
+
+7.8.2 / 2024-09-25
+==================
+ * fix(projection): avoid setting projection to unknown exclusive/inclusive if elemMatch on a Date, ObjectId, etc. #14894 #14893
+
+6.13.3 / 2024-09-23
+===================
+ * docs(migrating_to_6): document that Lodash _.isEmpty() with ObjectId() as a parameter returns true in Mongoose 6 #11152
+
+8.6.3 / 2024-09-17
+==================
+ * fix: make getters convert uuid to string when calling toObject() and toJSON() #14890 #14869
+ * fix: fix missing Aggregate re-exports for ESM #14886 [wongsean](https://github.com/wongsean)
+ * types(document): add generic param to depopulate() to allow updating properties #14891 #14876
+
+6.13.2 / 2024-09-12
+===================
+ * fix(document): make set() respect merge option on deeply nested objects #14870 #14878
+
+8.6.2 / 2024-09-11
+==================
+ * fix: make set merge deeply nested objects #14870 #14861 [ianHeydoc](https://github.com/ianHeydoc)
+ * types: allow arbitrary keys in query filters again (revert #14764) #14874 #14863 #14862 #14842
+ * types: make SchemaType static setters property accessible in TypeScript #14881 #14879
+ * type(inferrawdoctype): infer Date types as JS dates rather than Mongoose SchemaType Date #14882 #14839
+
+8.6.1 / 2024-09-03
+==================
+ * fix(document): avoid unnecessary clone() in applyGetters() that was preventing getters from running on 3-level deep subdocuments #14844 #14840 #14835
+ * fix(model): throw error if bulkSave() did not insert or update any documents #14837 #14763
+ * fix(cursor): throw error in ChangeStream constructor if changeStreamThunk() throws a sync error #14846
+ * types(query): add $expr to RootQuerySelector #14845
+ * docs: update populate.md to fix missing match: { } #14847 [makhoulshbeeb](https://github.com/makhoulshbeeb)
+
+8.6.0 / 2024-08-28
+==================
+ * feat: upgrade mongodb -> 6.8.0, handle throwing error on closed cursor in Mongoose with `MongooseError` instead of `MongoCursorExhaustedError` #14813
+ * feat(model+query): support options parameter for distinct() #14772 #8006
+ * feat(QueryCursor): add getDriverCursor() function that returns the raw driver cursor #14745
+ * types: change query selector to disallow unknown top-level keys by default #14764 [alex-statsig](https://github.com/alex-statsig)
+ * types: make toObject() and toJSON() not generic by default to avoid type widening #14819 #12883
+ * types: avoid automatically inferring lean result type when assigning to explicitly typed variable #14734
+
+8.5.5 / 2024-08-28
+==================
+ * fix(populate): fix a couple of other places where Mongoose gets the document's _id with getters #14833 #14827 #14759
+ * fix(discriminator): shallow clone Schema.prototype.obj before merging schemas to avoid modifying original obj #14821
+ * types: fix schema type based on timestamps schema options value #14829 #14825 [ark23CIS](https://github.com/ark23CIS)
+
+8.5.4 / 2024-08-23
+==================
+ * fix: add empty string check for collection name passed #14806 [Shubham2552](https://github.com/Shubham2552)
+ * docs(model): add 'throw' as valid strict value for bulkWrite() and add some more clarification on throwOnValidationError #14809
+
+7.8.1 / 2024-08-19
+==================
+ * fix(query): handle casting $switch in $expr #14761
+ * docs(mongoose): remove out-of-date callback-based example for mongoose.connect() #14811 #14810
+
+8.5.3 / 2024-08-13
+==================
+ * fix(document): call required functions on subdocuments underneath nested paths with correct context #14801 #14788
+ * fix(populate): avoid throwing error when no result and `lean()` set #14799 #14794 #14759 [MohOraby](https://github.com/MohOraby)
+ * fix(document): apply virtuals to subdocuments if parent schema has virtuals: true for backwards compatibility #14774 #14771 #14623 #14394
+ * types: make HydratedSingleSubdocument and HydratedArraySubdocument merge types instead of using & #14800 #14793
+ * types: support schema type inference based on schema options timestamps as well #14773 #13215 [ark23CIS](https://github.com/ark23CIS)
+ * types(cursor): indicate that cursor.next() can return null #14798 #14787
+ * types: allow mongoose.connection.db to be undefined #14797 #14789
+ * docs: add schema type widening advice #14790 [JstnMcBrd](https://github.com/JstnMcBrd)
+
+8.5.2 / 2024-07-30
+==================
+ * perf(clone): avoid further unnecessary checks if cloning a primitive value #14762 #14394
+ * fix: allow setting document array default to null #14769 #14717 #6691
+ * fix(model): support session: null option for save() to opt out of automatic session option with transactionAsyncLocalStorage #14744 #14736
+ * fix(model+document): avoid depopulating manually populated doc as getter value #14760 #14759
+ * fix: correct shardkey access in buildBulkWriteOps #14753 #14752 [adf0nt3s](https://github.com/adf0nt3s)
+ * fix(query): handle casting $switch in $expr #14755 #14751
+ * types: allow calling SchemaType.cast() without parent and init parameters #14756 #14748 #9076
+ * docs: fix a wrong example in v6 migration guide #14758 [abdelrahman-elkady](https://github.com/abdelrahman-elkady)
+
+7.8.0 / 2024-07-23
+==================
+ * feat: add transactionAsyncLocalStorage option to opt in to automatically setting session on all transactions #14744 #14742 #14583 #13889
+ * types(query): fix usage of "RawDocType" where "DocType" should be passed #14737 [hasezoey](https://github.com/hasezoey)
+
+8.5.1 / 2024-07-12
+==================
+ * perf(model): performance improvements for insertMany() #14724
+ * fix(model): avoid leaving subdoc defaults on top-level doc when setting subdocument to same value #14728 #14722
+ * fix(model): handle transactionAsyncLocalStorage option with insertMany() #14743
+ * types: make _id required on Document type #14735 #14660
+ * types: fix ChangeStream.close to return a Promise like the driver #14740 [orgads](https://github.com/orgads)
+
+8.5.0 / 2024-07-08
+==================
+ * perf: memoize toJSON / toObject default options #14672
+ * feat(document): add $createModifiedPathsSnapshot(), $restoreModifiedPathsSnapshot(), $clearModifiedPaths() #14699 #14268
+ * feat(query): make sanitizeProjection prevent projecting in paths deselected in the schema #14691
+ * feat: allow setting array default value to null #14717 #6691
+ * feat(mongoose): allow drivers to set global plugins #14682
+ * feat(connection): bubble up monitorCommands events to Mongoose connection if monitorCommands option set #14681 #14611
+ * fix(document): ensure post('deleteOne') hooks are called when calling save() after subdoc.deleteOne() #14732 #9885
+ * fix(query): remove count() and findOneAndRemove() from query chaining #14692 #14689
+ * fix: remove default connection if setting createInitialConnection to false after Mongoose instance created #14679 #8302
+ * types(models+query): infer return type from schema for 1-level deep nested paths #14632
+ * types(connection): make transaction() return type match the executor function #14661 #14656
+ * docs: fix docs links in index.md [mirasayon](https://github.com/mirasayon)
+
+8.4.5 / 2024-07-05
+==================
+ * types: correct this for validate.validator schematype option #14720 #14696
+ * docs(model): note that insertMany() with lean skips applying defaults #14723 #14698
+
+8.4.4 / 2024-06-25
+==================
+ * perf: avoid unnecesary get() call and use faster approach for converting to string #14673 #14394
+ * fix(projection): handle projections on arrays in Model.hydrate() projection option #14686 #14680
+ * fix(document): avoid passing validateModifiedOnly to subdocs so subdocs get fully validating if they're directly modified #14685 #14677
+ * fix: handle casting primitive array with $elemMatch in bulkWrite() #14687 #14678
+ * fix(query): cast $pull using embedded discriminator schema when discriminator key is set in filter #14676 #14675
+ * types(connection): fix return type of withSession() #14690 [tt-public](https://github.com/tt-public)
+ * types: add $documents pipeline stage and fix $unionWith type #14666 [nick-statsig](https://github.com/nick-statsig)
+ * docs(findoneandupdate): improve example that shows findOneAndUpdate() returning doc before updates were applied #14671 #14670
+
+7.7.0 / 2024-06-18
+==================
+ * feat(model): add throwOnValidationError option for opting into getting MongooseBulkWriteError if all valid operations succeed in bulkWrite() and insertMany() #14599 #14587 #14572 #13410
+
+8.4.3 / 2024-06-17
+==================
+ * fix: remove 0x flamegraph files from release
+
+8.4.2 / 2024-06-17
+==================
+ * perf: more toObject() perf improvements #14623 #14606 #14394
+ * fix(model): check the value of overwriteModels in options when calling discriminator() #14646 [uditha-g](https://github.com/uditha-g)
+ * fix: avoid throwing TypeError when deleting an null entry on a populated Map #14654 [futurliberta](https://github.com/futurliberta)
+ * fix(connection): fix up some inconsistencies in operation-end event and add to docs #14659 #14648
+ * types: avoid inferring Boolean, Buffer, ObjectId as Date in schema definitions under certain circumstances #14667 #14630
+ * docs: add note about parallelism in transations #14647 [fiws](https://github.com/fiws)
+
+6.13.1 / 2024-09-06
+===================
+ * fix: remove empty $and, $or, $not that were made empty by scrict mode #14749 #13086 [0x0a0d](https://github.com/0x0a0d)
+
+6.13.0 / 2024-06-06
+===================
+ * feat(model): add throwOnValidationError option for opting into getting MongooseBulkWriteError if all valid operations succeed in bulkWrite() and insertMany() #14599 #14587 #14572 #13410
+
+7.6.13 / 2024-06-05
+===================
+ * fix(query): shallow clone $or and $and array elements to avoid mutating query filter arguments #14614 #14610
+ * types: pass DocType down to subdocuments so HydratedSingleSubdocument and HydratedArraySubdocument toObject() returns correct type #14612 #14601
+ * docs(migrating_to_7): add id setter to Mongoose 7 migration guide #14645 #13672
+
+8.4.1 / 2024-05-31
+==================
+ * fix: pass options to clone instead of get in applyVirtuals #14606 #14543 [andrews05](https://github.com/andrews05)
+ * fix(document): fire pre validate hooks on 5 level deep single nested subdoc when modifying after save() #14604 #14591
+ * fix: ensure buildBulkWriteOperations target shard if shardKey is set #14622 #14621 [matlpriceshape](https://github.com/matlpriceshape)
+ * types: pass DocType down to subdocuments so HydratedSingleSubdocument and HydratedArraySubdocument toObject() returns correct type #14612 #14601
+
+6.12.9 / 2024-05-24
+===================
+ * fix(cast): cast $comment to string in query filters #14590 #14576
+ * types(model): allow passing strict type checking override to create() #14571 #14548
+
+7.6.12 / 2024-05-21
+===================
+ * fix(array): avoid converting to $set when calling pull() on an element in the middle of the array #14531 #14502
+ * fix: bump mongodb driver to 5.9.2 #14561 [lorand-horvath](https://github.com/lorand-horvath)
+ * fix(update): cast array of strings underneath doc array with array filters #14605 #14595
+
+8.4.0 / 2024-05-17
+==================
+ * feat: upgrade mongodb -> 6.6.2 #14584
+ * feat: add transactionAsyncLocalStorage option to opt in to automatically setting session on all transactions #14583 #13889
+ * feat: handle initially null driver when instantiating Mongoose for Rollup support #14577 #12335
+ * feat(mongoose): export omitUndefined() helper #14582 #14569
+ * feat: add Model.listSearchIndexes() #14519 #14450
+ * feat(connection): add listDatabases() function #14506 #9048
+ * feat(schema): add schema-level readConcern option to apply default readConcern for all queries #14579 #14511
+ * fix(error): remove model property from CastError to avoid printing all model properties to console #14568 #14529
+ * fix(model): make bulkWrite() and insertMany() throw if throwOnValidationError set and all ops invalid #14587 #14572
+ * fix(document): ensure transform function passed to toObject() options applies to subdocs #14600 #14589
+ * types: add inferRawDocType helper #13900 #13772
+ * types(document): make document _id type default to unknown instead of any #14541
+
+8.3.5 / 2024-05-15
+==================
+ * fix(query): shallow clone $or, $and if merging onto empty query filter #14580 #14567
+ * types(model+query): pass TInstanceMethods to QueryWithHelpers so populated docs have methods #14581 #14574
+ * docs(typescript): clarify that setting THydratedDocumentType on schemas is necessary for correct method context #14575 #14573
+
+8.3.4 / 2024-05-06
+==================
+ * perf(document): avoid cloning options using spread operator for perf reasons #14565 #14394
+ * fix(query): apply translateAliases before casting to avoid strictMode error when using aliases #14562 #14521
+ * fix(model): consistent top-level timestamps option for bulkWrite operations
+#14546 #14536
+ * docs(connections): improve description of connection creation patterns #14564 #14528
+
+8.3.3 / 2024-04-29
+==================
+ * perf(document): add fast path for applying non-nested virtuals to JSON #14543
+ * fix: make hydrate() recursively hydrate virtual populate docs if hydratedPopulatedDocs is set #14533 #14503
+ * fix: improve timestamps option handling in bulkWrite #14546 #14536 [sderrow](https://github.com/sderrow)
+ * fix(model): make recompileSchema() overwrite existing document array discriminators #14527
+ * types(schema): correctly infer Array #14534 #14367
+ * types(query+populate): apply populate overrides to doc toObject() result #14525 #14441
+ * types: add null to select override return type for findOne #14545 [sderrow](https://github.com/sderrow)
+
+8.3.2 / 2024-04-16
+==================
+ * fix(populate): avoid match function filtering out null values in populate result #14518 #14494
+ * types(query): make FilterQuery props resolve to any for generics support #14510 #14473 #14459
+ * types(DocumentArray): pass DocType generic to Document for correct toJSON() and toObject() return types #14526 #14469
+ * types(models): fix incorrect bulk write options #14513 [emiljanitzek](https://github.com/emiljanitzek)
+ * docs: add documentation for calling schema.post() with async function #14514 #14305
+
+7.6.11 / 2024-04-11
+===================
+ * fix(populate): avoid match function filtering out null values in populate result #14518
+ * fix(schema): support setting discriminator options in Schema.prototype.discriminator() #14493 #14448
+ * fix(schema): deduplicate idGetter so creating multiple models with same schema doesn't result in multiple id getters #14492 #14457
+
+6.12.8 / 2024-04-10
+===================
+ * fix(document): handle virtuals that are stored as objects but getter returns string with toJSON #14468 #14446
+ * fix(schematype): consistently set wasPopulated to object with `value` property rather than boolean #14418
+ * docs(model): add extra note about lean option for insertMany() skipping casting #14415 #14376
+
+8.3.1 / 2024-04-08
+==================
+ * fix(document): make update minimization unset property rather than setting to null #14504 #14445
+ * fix(model): make Model.recompileSchema() also re-apply discriminators #14500 #14444
+ * fix(schema): deduplicate idGetter so creating multiple models with same schema doesn't result in multiple id getters #14492
+ * fix: update kareem -> 2.6.3 for index.d.ts #14508 #14497
+ * fix(mongoose): make setDriver() update mongoose.model() connections and collections #14505
+ * types(validation): support function for validator message property, and add support for accessing validator reason #14499 #14496
+ * docs: remove typo #14501 [epmartini](https://github.com/epmartini)
+
+8.3.0 / 2024-04-03
+==================
+ * feat: use mongodb@6.5.0
+ * feat(document): add validateAllPaths option to validate() and validateSync() #14467 #14414
+ * feat: pathsToSave option to save() function #14385 #9583
+ * feat(query): add options parameter to Query.prototype.sort() #14375 #14365
+ * feat: add function SchemaType.prototype.validateAll #14434 #6910
+ * fix: handle array schema definitions with of keyword #14447 #14416
+ * types: add overwriteMiddlewareResult and skipMiddlewareFunction to types #14328 #14829
+
+8.2.4 / 2024-03-28
+==================
+ * types(query): bring "getFilter" and "getQuery" in-line with "find" and other types #14463 [hasezoey](https://github.com/hasezoey)
+ * types(schema): re-export the defintion for SearchIndexDescription #14464 [noseworthy](https://github.com/noseworthy)
+ * docs: removed unused hook from docs #14461 [bernardarhia](https://github.com/bernardarhia)
+
+8.2.3 / 2024-03-21
+==================
+ * fix(schema): avoid returning string 'nested' as schematype #14453 #14443 #14435
+ * types(schema): add missing search index types #14449 [noseworthy](https://github.com/noseworthy)
+ * types: improve the typing of FilterQuery type to prevent it from only getting typed to any #14436 #14398 #14397
+
+8.2.2 / 2024-03-15
+==================
+ * fix(model): improve update minimizing to only minimize top-level properties in the update #14437 #14420 #13782
+ * fix: add Null check in case schema.options['type'][0] is undefined #14431 [Atharv-Bobde](https://github.com/Atharv-Bobde)
+ * types: consistently infer array of objects in schema as a DocumentArray #14430 #14367
+ * types: add TypeScript interface for the new PipelineStage - Vector Search - solving issue #14428 #14429 [jkorach](https://github.com/jkorach)
+ * types: add pre and post function types on Query class #14433 #14432 [IICarst](https://github.com/IICarst)
+ * types(model): make bulkWrite() types more flexible to account for casting #14423
+ * docs: update version support documentation for mongoose 5 & 6 #14427 [hasezoey](https://github.com/hasezoey)
+
+7.6.10 / 2024-03-13
+===================
+ * docs(model): add extra note about lean option for insertMany() skipping casting #14415
+ * docs(mongoose): add options.overwriteModel details to mongoose.model() docs #14422
+
+8.2.1 / 2024-03-04
+==================
+ * fix(document): make $clone avoid converting subdocs into POJOs #14395 #14353
+ * fix(connection): avoid unhandled error on createConnection() if on('error') handler registered #14390 #14377
+ * fix(schema): avoid applying default write concern to operations that are in a transaction #14391 #11382
+ * types(querycursor): correct cursor async iterator type with populate() support #14384 #14374
+ * types: missing typescript details on options params of updateMany, updateOne, etc. #14382 #14379 #14378 [FaizBShah](https://github.com/FaizBShah) [sderrow](https://github.com/sderrow)
+ * types: allow Record as valid query select argument #14371 [sderrow](https://github.com/sderrow)
+
+6.12.7 / 2024-03-01
+===================
+ * perf(model): make insertMany() lean option skip hydrating Mongoose docs #14376 #14372
+ * perf(document+schema): small optimizations to make init() faster #14383 #14113
+ * fix(connection): don't modify passed options object to `openUri()` #14370 #13376 #13335
+ * fix(ChangeStream): bubble up resumeTokenChanged changeStream event #14355 #14349 [3150](https://github.com/3150)
+
+7.6.9 / 2024-02-26
+==================
+ * fix(document): handle embedded recursive discriminators on nested path defined using Schema.prototype.discriminator #14256 #14245
+ * types(model): correct return type for findByIdAndDelete() #14233 #14190
+ * docs(connections): add note about using asPromise() with createConnection() for error handling #14364 #14266
+ * docs(model+query+findoneandupdate): add more details about overwriteDiscriminatorKey option to docs #14264 #14246
+
+8.2.0 / 2024-02-22
+==================
+ * feat(model): add recompileSchema() function to models to allow applying schema changes after compiling #14306 #14296
+ * feat: add middleware for bulkWrite() and createCollection() #14358 #14263 #7893
+ * feat(model): add `hydratedPopulatedDocs` option to make hydrate recursively hydrate populated docs #14352 #4727
+ * feat(connection): add withSession helper #14339 #14330
+
+8.1.3 / 2024-02-16
+==================
+ * fix: avoid corrupting $set-ed arrays when transaction error occurs #14346 #14340
+ * fix(populate): handle ref() functions that return a model instance #14343 #14249
+ * fix: insert version key when using insertMany even if `toObject.versionKey` set to false #14344
+ * fix(cursor): make aggregation cursor support transform option to match query cursor #14348 #14331
+ * docs(document): clarify that transform function option applies to subdocs #13757
+
+8.1.2 / 2024-02-08
+==================
+ * fix: include virtuals in document array toString() output if toObject.virtuals set #14335 #14315
+ * fix(document): handle setting nested path to spread doc with extra properties #14287 #14269
+ * fix(populate): call setter on virtual populated path with populated doc instead of undefined #14314
+ * fix(QueryCursor): remove callback parameter of AggregationCursor and QueryCursor #14299 [DevooKim](https://github.com/DevooKim)
+ * types: add typescript support for arbitrary fields for the options parameter of Model functions which are of type MongooseQueryOptions #14342 #14341 [FaizBShah](https://github.com/FaizBShah)
+ * types(model): correct return type for findOneAndUpdate with includeResultMetadata and lean set #14336 #14303
+ * types(connection): add type definition for `createCollections()` #14295 #14279
+ * docs(timestamps): clarify that replaceOne() and findOneAndReplace() overwrite timestamps #14337 #14309
+
+8.1.1 / 2024-01-24
+==================
+ * fix(model): throw readable error when calling Model() with a string instead of model() #14288 #14281
+ * fix(document): handle setting nested path to spread doc with extra properties #14287 #14269
+ * types(query): add back context and setDefaultsOnInsert as Mongoose-specific query options #14284 #14282
+ * types(query): add missing runValidators back to MongooseQueryOptions #14278 #14275
+
+6.12.6 / 2024-01-22
+===================
+ * fix(collection): correctly handle buffer timeouts with find() #14277
+ * fix(document): allow calling push() with different $position arguments #14254
+
+8.1.0 / 2024-01-16
+==================
+ * feat: upgrade MongoDB driver -> 6.3.0 #14241 #14189 #14108 #14104
+ * feat: add Atlas search index helpers to Models and Schemas #14251 #14232
+ * feat(connection): add listCollections() helper to connections #14257
+ * feat(schematype): merge rather than overwrite default schematype validators #14124 #14070
+ * feat(types): support type hints in InferSchemaType #14008 [JavaScriptBach](https://github.com/JavaScriptBach)
+
+8.0.4 / 2024-01-08
+==================
+ * fix(update): set CastError path to full path if casting update fails #14161 #14114
+ * fix: cast error when there is an elemMatch in the and clause #14171 [tosaka-n](https://github.com/tosaka-n)
+ * fix: allow defining index on base model that applies to all discriminators #14176 [peplin](https://github.com/peplin)
+ * fix(model): deep clone bulkWrite() updateOne arguments to avoid mutating documents in update #14197 #14164
+ * fix(populate): handle deselecting _id with array of fields in populate() #14242 #14231
+ * types(model+query): use stricter typings for updateX(), replaceOne(),deleteX() Model functions #14228 #14204
+ * types: fix return types for findByIdAndDelete overrides #14196 #14190
+ * types(schema): add missing omit() method #14235 [amitbeck](https://github.com/amitbeck)
+ * types(model): add missing strict property to bulkWrite() top level options #14239
+ * docs(compatibility): add note that Mongoose 5.13 is fully compatible with MongoDB server 5 #14230 #14149
+ * docs: add shared schemas guide #14211
+ * docs: update TLS/SSL guide for Mongoose v8 - MongoDB v6 driver deprecations #14170 [andylwelch](https://github.com/andylwelch)
+ * docs: update findOneAndUpdate tutorial to use includeResultMetadata #14208 #14207
+ * docs: clarify disabling _id on subdocs #14195 #14194
+
+7.6.8 / 2024-01-08
+==================
+ * perf(schema): remove unnecessary lookahead in numeric subpath check
+ * fix(discriminator): handle reusing schema with embedded discriminators defined using Schema.prototype.discriminator #14202 #14162
+ * fix(ChangeStream): avoid suppressing errors in closed change stream #14206 #14177
+
+6.12.5 / 2024-01-03
+===================
+ * perf(schema): remove unnecessary lookahead in numeric subpath check
+ * fix(document): allow setting nested path to null #14226
+ * fix(document): avoid flattening dotted paths in mixed path underneath nested path #14198 #14178
+ * fix: add ignoreAtomics option to isModified() for better backwards compatibility with Mongoose 5 #14213
+
+6.12.4 / 2023-12-27
+===================
+ * fix: upgrade mongodb driver -> 4.17.2
+ * fix(document): avoid treating nested projection as inclusive when applying defaults #14173 #14115
+ * fix: account for null values when assigning isNew property #14172 #13883
+
+8.0.3 / 2023-12-07
+==================
+ * fix(schema): avoid creating unnecessary clone of schematype in nested array so nested document arrays use correct constructor #14128 #14101
+ * docs(connections): add example of registering connection event handlers #14150
+ * docs(populate): add example of using `refPath` and `ref` functions #14133 #13834
+ * types: handle using BigInt global class in schema definitions #14160 #14147
+ * types: make findOneAndDelete() without options return result doc, not ModifyResult #14153 #14130
+ * types(model): add no-generic override for insertMany() with options #14152 #13999
+ * types: add missing Type for applyDefaults #14159 [jaypea](https://github.com/jaypea)
+
+7.6.7 / 2023-12-06
+==================
+ * fix: avoid minimizing single nested subdocs if they are required #14151 #14058
+ * fix(populate): allow deselecting discriminator key when populating #14155 #3230
+ * fix: allow adding discriminators using Schema.prototype.discriminator() to subdocuments after defining parent schema #14131 #14109
+ * fix(schema): avoid creating unnecessary clone of schematype in nested array so nested document arrays use correct constructor #14128 #14101
+ * fix(populate): call transform object with single id instead of array when populating a justOne path under an array #14135 #14073
+ * types: add back mistakenly removed findByIdAndRemove() function signature #14136 #14132
+
+8.0.2 / 2023-11-28
+==================
+ * fix(populate): set populated docs in correct order when populating virtual underneath doc array with justOne #14105
+ * fix(populate): fix curPath to update appropriately #14099 #14098 [csy1204](https://github.com/csy1204)
+ * types: make property names show up in intellisense for UpdateQuery #14123 #14090
+ * types(document): correct return type for doc.deleteOne() re: Mongoose 8 breaking change #14110 #14081
+ * types: correct types for when includeResultMetadata: true is set #14078
+ * types(models): allow specifying timestamps as inline option for bulkWrite() operations #14112 #14072
+ * docs: fix rendering of 7.x server compatibility #14086 [laupow](https://github.com/laupow)
+ * docs(source/api): fix "index.js" -> "mongoose.js" rename #14125
+ * docs(README): update breaking change version #14126
+
+7.6.6 / 2023-11-27
+==================
+ * perf: avoid double-running setter logic when calling `push()` #14120 #11380
+ * fix(populate): set populated docs in correct order when populating virtual underneath doc array with justOne #14105 #14018
+ * fix: bump mongodb driver -> 5.9.1 #14084 #13829 [lorand-horvath](https://github.com/lorand-horvath)
+ * types: allow defining document array using [{ prop: String }] syntax #14095 #13424
+ * types: correct types for when includeResultMetadata: true is set #14078 #13987 [prathamVaidya](https://github.com/prathamVaidya)
+ * types(query): base filters and projections off of RawDocType instead of DocType so autocomplete doesn't show populate #14118 #14077
+ * types: make property names show up in intellisense for UpdateQuery #14123 #14090
+ * types(model): support calling Model.validate() with pathsToSkip option #14088 #14003
+ * docs: remove "DEPRECATED" warning mistakenly added to read() tags param #13980
+
+8.0.1 / 2023-11-15
+==================
+ * fix: retain key order with aliases when creating indexes with alias #14042 [meabed](https://github.com/meabed)
+ * fix: handle nonexistent collection with diffIndexes #14029 #14010
+ * types(model+query): correctly remove count from TypeScript types to reflect removal of runtime support #14076 #14067 #14062
+ * types: correct `this` parameter for methods and statics #14028 #14027 [ruxxzebre](https://github.com/ruxxzebre)
+ * types(model+query): unpack arrays in distinct return type #14047 #14026
+ * types: add missing Types.UUID typings #14023 #13103 [k725](https://github.com/k725)
+ * docs: add mongoose 8 to mongodb server compatibility guide #14064
+ * docs: fix typo in queries.md #14065 [MuhibAhmed](https://github.com/MuhibAhmed)
+
+7.6.5 / 2023-11-14
+==================
+ * fix: handle update validators and single nested doc with numeric paths #14066 #13977
+ * fix: handle recursive schema array in discriminator definition #14068 #14055
+ * fix: diffIndexes treats namespace error as empty #14048 #14029
+ * docs(migrating_to_7): add note about requiring new with ObjectId #14021 #14020
+
+6.12.3 / 2023-11-07
+===================
+ * fix(ChangeStream): correctly handle hydrate option when using change stream as stream instead of iterator #14052
+ * fix(schema): fix dangling reference to virtual in tree after `removeVirtual()` #14019 #13085
+ * fix(document): avoid unmarking modified on nested path if no initial value stored and already modified #14053 #14024
+ * fix(document): consistently avoid marking subpaths of nested paths as modified #14053 #14022
+
+8.0.0 / 2023-10-31
+==================
+ * docs: add version support notes for Mongoose 8, including EOL date for Mongoose 6
+
+7.6.4 / 2023-10-30
+==================
+ * fix(connection): retain modified status for documents created outside a transaction during transaction retries #14017 #13973
+ * fix(schema): handle recursive schemas in discriminator definitions #14011 #13978
+ * fix: handle casting $or underneath $elemMatch #14007 #13974
+ * fix(populate): allow using options: { strictPopulate: false } to disable strict populate #13863
+ * docs: fix differences between sample codes and documentation #13998 [suzuki](https://github.com/suzuki)
+ * docs: fix missing import and change wrong variable name #13992 [suzuki](https://github.com/suzuki)
+
+6.12.2 / 2023-10-25
+===================
+ * fix: add fullPath to ValidatorProps #13995 [Freezystem](https://github.com/Freezystem)
+
+8.0.0-rc0 / 2023-10-24
+======================
+ * BREAKING CHANGE: use MongoDB node driver 6, drop support for rawResult option and findOneAndRemove() #13753
+ * BREAKING CHANGE: apply minimize by default when updating document #13843
+ * BREAKING CHANGE: remove `id` setter #13784
+ * BREAKING CHANGE: remove overwrite option for updateOne(), findOneAndUpdate(), etc. #13989 #13578
+ * BREAKING CHANGE: make model.prototype.deleteOne() return query, not promise #13660 #13369
+ * BREAKING CHANGE: remove `Model.count()`, `Query.prototype.count()` #13618 #13598
+ * BREAKING CHANGE: allow null values for string enum #13620 #3044
+ * BREAKING CHANGE: make base schema paths come before discriminator schema paths when running setters, validators, etc. #13846 #13794
+ * BREAKING CHANGE: make Model.validate() use Model.castObject() to cast, and return casted copy of object instead of modifying in place #13287 #12668
+ * BREAKING CHANGE: make internal file names all camelCase #13950 #13909 #13308
+ * BREAKING CHANGE: make create() wait for all documents to finish inserting or error out before throwing an error if ordered = false #13621 #4628
+ * BREAKING CHANGE: refactor out `mongoose/lib/mongoose.js` file to allow importing Mongoose without MongoDB driver #13905
+ * BREAKING CHANGE(types): allow `null` for optional fields #13901
+ * BREAKING CHANGE(types): infer return types types for Model.distinct and Query.distinct #13836 [kaulshashank](https://github.com/kaulshashank)
+
+7.6.3 / 2023-10-17
+==================
+ * fix(populate): handle multiple spaces when specifying paths to populate using space-delimited paths #13984 #13951
+ * fix(update): avoid applying defaults on query filter when upserting with empty update #13983 #13962
+ * fix(model): add versionKey to bulkWrite when inserting or upserting #13981 #13944
+ * docs: fix typo in timestamps docs #13976 [danielcoker](https://github.com/danielcoker)
+
+7.6.2 / 2023-10-13
+==================
+ * perf: avoid storing a separate entry in schema subpaths for every element in an array #13953 #13874
+ * fix(document): avoid triggering setter when initializing Model.prototype.collection to allow defining collection as a schema path name #13968 #13956
+ * fix(model): make bulkSave() save changes in discriminator paths if calling bulkSave() on base model #13959 #13907
+ * fix(document): allow calling $model() with no args for TypeScript #13963 #13878
+ * fix(schema): handle embedded discriminators defined using Schema.prototype.discriminator() #13958 #13898
+ * types(model): make InsertManyResult consistent with return type of insertMany #13965 #13904
+ * types(models): add cleaner type definitions for insertMany() with no generics to prevent errors when using insertMany() in generic classes #13964 #13957
+ * types(schematypes): allow defining map path using type: 'Map' in addition to type: Map #13960 #13755
+
+6.12.1 / 2023-10-12
+===================
+ * fix(mongoose): correctly handle global applyPluginsToChildSchemas option #13945 #13887 [hasezoey](https://github.com/hasezoey)
+ * fix: Document.prototype.isModified support for a string of keys as first parameter #13940 #13674 [k-chop](https://github.com/k-chop)
+
+7.6.1 / 2023-10-09
+==================
+ * fix: bump bson to match mongodb@5.9.0 exactly #13947 [hasezoey](https://github.com/hasezoey)
+ * fix: raw result deprecation message #13954 [simllll](https://github.com/simllll)
+ * type: add types for includeResultMetadata #13955 [simllll](https://github.com/simllll)
+ * perf(npmignore): ignore newer files #13946 [hasezoey](https://github.com/hasezoey)
+ * perf: move mocha config from package.json to mocharc #13948 [hasezoey](https://github.com/hasezoey)
+
+7.6.0 / 2023-10-06
+==================
+ * feat: upgrade mongodb node driver -> 5.9.0 #13927 #13926 [sanguineti](https://github.com/sanguineti)
+ * fix: avoid CastError when passing different value of discriminator key in `$or` #13938 #13906
+
+7.5.4 / 2023-10-04
+==================
+ * fix: avoid stripping out `id` property when `_id` is set #13933 #13892 #13867
+ * fix(QueryCursor): avoid double-applying schema paths so you can include select: false fields with + projection using cursors #13932 #13773
+ * fix(query): allow deselecting discriminator key using - syntax #13929 #13760
+ * fix(query): handle $round in $expr as array #13928 #13881
+ * fix(document): call pre('validate') hooks when modifying a path underneath triply nested subdoc #13912 #13876
+ * fix(mongoose): correctly handle global applyPluginsToChildSchemas option #13911 #13887
+ * types: add insertMany array overload with options #13931 [t1bb4r](https://github.com/t1bb4r)
+ * docs(compatibility): add Mongoose 7 support to compatibility matrix #13875
+ * docs: amend some awkward FAQ wording #13925 [peteboere](https://github.com/peteboere)
+
+7.5.3 / 2023-09-25
+==================
+ * fix(document): handle MongoDB Long when casting BigInts #13869 #13791
+ * fix(model): make bulkSave() persist changes that happen in pre('save') middleware #13885 #13799
+ * fix: handle casting $elemMatch underneath $not underneath another $elemMatch #13893 #13880
+ * fix(model): make bulkWrite casting respect global setDefaultsOnInsert #13870 #13823
+ * fix(document): handle default values for discriminator key with embedded discriminators #13891 #13835
+ * fix: account for null values when assigning isNew property within document array #13883
+ * types: avoid "interface can only extend object types with statically known members" error in TypeScript 4 #13871
+ * docs(deprecations): fix typo in includeResultMetadata deprecation docs #13884 #13844
+ * docs: fix pre element overflow in home page #13868 [ghoshRitesh12](https://github.com/ghoshRitesh12)
+
+7.5.2 / 2023-09-15
+==================
+ * fix(schema): handle number discriminator keys when using Schema.prototype.discriminator() #13858 #13788
+ * fix: ignore `id` property when calling `set()` with both `id` and `_id` specified to avoid `id` setter overwriting #13762
+ * types: pass correct document type to required and default function #13851 #13797
+ * docs(model): add examples of using diffIndexes() to syncIndexes()and diffIndexes() api docs #13850 #13771
+
+7.5.1 / 2023-09-11
+==================
+ * fix: set default value for _update when no update object is provided and versionKey is set to false #13795 #13783 [MohOraby](https://github.com/MohOraby)
+ * fix: avoid unexpected error when accessing null array element on discriminator array when populating #13716 [ZSabakh](https://github.com/ZSabakh)
+ * types(schematypes): use DocType for instance method this #13822 #13800 [pshaddel](https://github.com/pshaddel)
+ * types: remove duplicated 'exists' method in Model interface in models.d.ts #13818 [ohzeno](https://github.com/ohzeno)
+ * docs(model): replace outdated docs on deprecated findOneAndUpdate() overwrite option #13821 #13715
+ * docs: add example of using `virtuals.pathsToSkip` option for `toObject()` and `toJSON()` #13798 [RobertHunter-Pluto](https://github.com/RobertHunter-Pluto)
+
+7.5.0 / 2023-08-29
+==================
+ * feat: use mongodb driver v5.18.1
+ * feat: allow top level dollar keys with findOneAndUpdate(), update() for MongoDB 5 #13786
+ * fix(document): make array getters avoid unintentionally modifying array, defer getters until index access instead #13774
+ * feat: deprecate `overwrite` option for findOneAndUpdate() #13578
+ * feat: add pathsToSkip option for Model.validate #13663 #10353
+ * feat: support alias when declaring index #13659 #13276
+ * fix(query): remove unnecessary check for atomic operators in findOneAndReplace() #13678
+ * types: add SearchMeta Interface for Atlas Search #13792 [mreouven](https://github.com/mreouven)
+ * types(schematypes): add missing BigInt SchemaType #13787
+
+7.4.5 / 2023-08-25
+==================
+ * fix(debug): avoid putting virtuals and getters in debug output #13778
+ * fix(model): make Model.bulkWrite() with empty array and ordered false not throw an error #13664
+ * fix(document): correctly handle inclusive/exclusive projections when applying subdocument defaults #13763 #13720
+
+6.12.0 / 2023-08-24
+===================
+ * feat: use mongodb driver v4.17.1
+ * fix(model): make Model.bulkWrite() with empty array and ordered false not throw an error #13664
+ * fix(document): correctly handle inclusive/exclusive projections when applying subdocument defaults #13763 #13720
+
+7.4.4 / 2023-08-22
+==================
+ * fix(connection): reset document state in between transaction retries #13726 #13698
+ * fix(cursor): bubble up resumeTokenChanged event from change streams #13736 #13607
+ * fix(query+populate): add refPath to projection by default, unless explicitly excluded #13758
+ * fix(schema): support 'ascending', 'asc', 'descending', 'desc' for index direction #13761 #13725
+ * fix(ChangeStream): add _bindEvents to addListener function for observable support #13759 [yury-ivaniutsenka](https://github.com/yury-ivaniutsenka)
+ * types: infer return type when using `get()`, `markModified()`, etc. with known property name literal #13739 [maybesmurf](https://github.com/maybesmurf)
+ * types: add missing typings for option includeResultMetadata #13747 #13746 [Idnan](https://github.com/Idnan)
+ * types: export InferSchemaType #13737
+ * docs(middleware): clarify that query middleware applies to document by default #13734 #13713
+ * docs: add brief note on TypeScript generic usage for embedded discriminator path() calls #13728 #10435
+ * docs: link v7 migration guide #13742 [Cooldogyum](https://github.com/Cooldogyum)
+ * docs(migrating_to_6): add note about incompatible packages #13733
+
+6.11.6 / 2023-08-21
+===================
+ * fix(model): avoid hanging on empty bulkWrite() with ordered: false #13701 #13684 [JavaScriptBach](https://github.com/JavaScriptBach)
+ * types: augment bson.ObjectId instead of adding on own type #13515 #12537 [hasezoey](https://github.com/hasezoey)
+
+7.4.3 / 2023-08-11
+==================
+ * fix: avoid applying map property getters when saving #13704 #13657
+ * fix(query): allow deselecting discriminator key #13722 #13679
+ * types(models+query): return lean type when passing QueryOptions with lean: true to relevant model functions like find() and findOne() #13721 #13705
+ * types(schema): correct return type for Schema.prototype.indexes() #13718 #13702
+ * types: allow accessing options from pre middleware #13708 #13633
+ * types: add UpdateQueryKnownOnly type for stricter UpdateQuery type checking #13699 #13630
+ * types(schema): support required: { isRequired: true } syntax in schema definition #13680
+ * docs(middleware): clarify that doc.deleteOne() doesn't run query middleware currently #13707 #13669
+
+7.4.2 / 2023-08-03
+==================
+ * fix(model): avoid hanging on empty bulkWrite() with ordered: false #13684 #13664
+ * fix: Document.prototype.isModified support for a string of keys as first parameter #13674 #13667 [gastoncasini](https://github.com/gastoncasini)
+ * fix: disable id virtual if alias:id set #13654 #13650
+ * fix: support timestamps:false on bulkWrite with updateOne and updateMany #13649 #13611
+ * docs(typescript): highlight auto type inference for methods and statics, add info on using methods with generics #13696 #12942
+ * docs(middleware): fix old example using post('remove') #13683 #13518
+ * docs(deprecations): quick fix for includeResultMetadata docs #13695
+
+6.11.5 / 2023-08-01
+===================
+ * fix(schema): make Schema.prototype.clone() avoid creating different copies of subdocuments and single nested paths underneath single nested paths #13671 #13626
+ * fix: custom debug function not processing all args #13418
+
+7.4.1 / 2023-07-24
+==================
+ * fix(document): correctly clean up nested subdocs modified state on save() #13644 #13609
+ * fix(schema): avoid propagating toObject.transform and toJSON.transform option to implicitly created schemas #13634 #13599
+ * fix: prevent schema options overwriting user defined writeConcern #13612 #13592
+ * types: correctly handle pre('deleteOne', { document: true }) #13632
+ * types(schema): handle type: Schema.Types.Map in TypeScript #13628
+ * types: Add inline comment to to tell the default value of the runValidator flag in the queryOptions types #13636 [omran95](https://github.com/omran95)
+ * docs: rework several code examples that still use callbacks #13635 #13616
+ * docs: remove callbacks from validation description #13638 #13501
+
+7.4.0 / 2023-07-18
+==================
+ * perf: speed up mapOfSubdocs benchmark by 4x by avoiding unnecessary O(n^2) loop in getPathsToValidate() #13614
+ * feat: upgrade to MongoDB Node.js driver 5.7.0 #13591
+ * BREAKING CHANGE: add `id` setter which allows modifying `_id` by setting `id` (Note this change was originally shipped as a `feat`, but later reverted in Mongoose 8 due to compatibility issues) #13517
+ * feat: support generating custom cast error message with a function #13608 #3162
+ * feat(query): support MongoDB driver's includeResultMetadata option for findOneAndUpdate #13584 #13539
+ * feat(connection): add Connection.prototype.removeDb() for removing a related connection #13580 #11821
+ * feat(query): delay converting documents into POJOs until query execution, allow querying subdocuments with defaults disabled #13522
+ * feat(model): add option "aggregateErrors" for create() #13544 [hasezoey](https://github.com/hasezoey)
+ * feat(schema): add collectionOptions option to schemas #13513
+ * fix: move all MongoDB-specific connection logic into driver layer, add createClient() method to handle creating MongoClient #13542
+ * fix(document): allow setting keys with dots in mixed paths underneath nested paths #13536
+ * types: augment bson.ObjectId instead of adding on own type #13515 #12537 [hasezoey](https://github.com/hasezoey)
+ * docs(guide): fix md lint #13593 [hasezoey](https://github.com/hasezoey)
+ * docs: changed the code from 'await author.save()' to 'await story1.save()' #13596 [SomSingh23](https://github.com/SomSingh23)
+
+6.11.4 / 2023-07-17
+===================
+ * perf: speed up mapOfSubdocs benchmark by 4x by avoiding unnecessary O(n^2) loop in getPathsToValidate() #13614
+
+7.3.4 / 2023-07-12
+==================
+ * chore: release 7.4.4 to overwrite accidental publish of 5.13.20 to latest tag
+
6.11.3 / 2023-07-11
===================
* fix: avoid prototype pollution on init
* fix(schema): correctly handle uuids with populate() #13317 #13595
+7.3.3 / 2023-07-10
+==================
+ * fix: avoid prototype pollution on init
+ * fix(document): clean up all array subdocument modified paths on save() #13589 #13582
+ * types: avoid unnecessary MergeType<> if TOverrides not set, clean up statics and insertMany() type issues #13577 #13529
+
+7.3.2 / 2023-07-06
+==================
+ * fix(model): avoid TypeError if insertMany() fails with error that does not have writeErrors property #13579 #13531
+ * fix(query): convert findOneAndUpdate to findOneAndReplace when overwrite set for backwards compat with Mongoose 6 #13572 #13550
+ * fix(query): throw readable error when executing a Query instance without an associated model #13571 #13570
+ * types: support mongoose.Schema.ObjectId as alias for mongoose.Schema.Types.ObjectId #13543 #13534
+ * docs(connections): clarify that socketTimeoutMS now defaults to 0 #13576 #13537
+ * docs(migrating_to_7): add mapReduce() removal to migration guide #13568 #13548
+ * docs(schemas): fix typo in schemas.md #13540 [Metehan-Altuntekin](https://github.com/Metehan-Altuntekin)
+
+7.3.1 / 2023-06-21
+==================
+ * fix(query): respect query-level strict option on findOneAndReplace() #13516 #13507
+ * docs(connections): expand docs on serverSelectionTimeoutMS #13533 #12967
+ * docs: add example of accessing save options in pre save #13498
+ * docs(connections+faq): add info on localhost vs 127.0.0.1
+ * docs(SchemaType): validate members are validator & message (not msg) #13521 [lorand-horvath](https://github.com/lorand-horvath)
+
+7.3.0 / 2023-06-14
+==================
+ * feat: upgrade mongodb -> 5.6.0 #13455 [lorand-horvath](https://github.com/lorand-horvath)
+ * feat(aggregate): add Aggregate.prototype.finally() to be consistent with Promise API for TypeScript #13509
+ * feat(schema): support selecting subset of fields to apply optimistic concurrency to #13506 #10591
+ * feat(model): add `ordered` option to `Model.create()` #13472 #4038
+ * feat(schema): consistently add .get() function to all SchemaType classes
+ * feat(populate): pass virtual to match function to allow merging match options #13477 #12443
+ * types: allow overwriting Paths in select() to tell TypeScript which fields are projected #13478 #13224
+ * types(schema): add validateModifiedOnly as schema option #13503 #10153
+ * docs: add note about validateModifiedOnly as a schema option #13503 #10153
+ * docs(migrating_to_7): update migrating_to_7.md to include Model.countDocuments #13508 [Climax777](https://github.com/Climax777)
+ * docs(further_reading): remove style for "img" [hasezoey](https://github.com/hasezoey)
+
+7.2.4 / 2023-06-12
+==================
+ * fix(query): handle non-string discriminator key values in query #13496 #13492
+
+7.2.3 / 2023-06-09
+==================
+ * fix(model): ignore falsy last argument to create() for backwards compatibility #13493 #13491 #13487 [MohOraby](https://github.com/MohOraby)
+ * types: remove generic param that's causing issues for typegoose #13494 #13482
+ * types(aggregate): allow object syntax for $mergeObjects #13470 #13060
+ * docs(connection): clarify how Connection.prototype.destroy() is different from close() #13475
+ * docs(populate): fix accidental removal of text #13480
+ * docs: add additional notes for Atlas X.509 authentication #13452 [alexbevi](https://github.com/alexbevi)
+ * docs(populate): add a little more info on why we recommend using ObjectId for _id #13474 #13400
+
6.11.2 / 2023-06-08
===================
* fix(cursor): allow find middleware to modify query cursor options #13476 #13453 #13435
+7.2.2 / 2023-05-30
+==================
+ * fix(schema): make bulkWrite updateOne() and updateMany() respect timestamps option when set by merging schemas #13445
+ * fix(schema): recursively copy schemas from different modules when calling new Schema() #13441 #13275
+ * fix(update): allow setting paths with dots under non-strict paths #13450 #13434
+ * types: improve function parameter types for ToObjectOptions transform option #13446 #13421
+ * docs: add nextjs page with link to next starter app and couple FAQs #13444 #13430
+ * docs(connections): add section on multi tenant #13449 #11187
+ * docs(connection+model): expand docs on accessors for underlying collections #13448 #13334
+
+7.2.1 / 2023-05-24
+==================
+ * fix(array): track correct changes when setting nested array of primitives #13422 #13372
+ * fix(query): handle plus path in projection with findOneAndUpdate() #13437 #13413
+ * fix(cursor): handle calling skipMiddlewareFunction() in pre('find') middleware with cursors #13436 #13411
+ * fix(model): include inspect output in castBulkWrite() error #13426
+ * fix: avoid setting null property when updating using update pipeline with child timestamps but no top-level timestamps #13427 #13379
+ * docs: remove callback based examples #13433 #13401
+ * docs(connections): add details about keepAlive deprecation #13431
+ * docs: add list of supported patterns for error message templating #13425 #13311
+
+7.2.0 / 2023-05-19
+==================
+ * feat: upgrade mongodb -> 5.5.0
+ * feat(document): add flattenObjectIds option to toObject() and toJSON() #13383 #13341
+ * feat(query): add translateAliases option to automatically call translate aliases on query fields #13397 #8678 #7511
+ * feat(schema): propagate toObject and toJSON options to implicitly created schemas #13325
+ * feat(model): add throwOnValidationError option for opting into getting MongooseBulkWriteError if all valid operations succeed in bulkWrite() and insertMany() #13410 #13256
+ * feat(types+mongoose): export MongooseError #13403 #13387 [ramos-ph](https://github.com/ramos-ph)
+
+7.1.2 / 2023-05-18
+==================
+ * fix: set timestamps on single nested subdoc in insertMany() #13416 #13343
+ * fix: mention model name in missing virtual option in getModelsMapForPopulate #13408 #13406 [hasezoey](https://github.com/hasezoey)
+ * fix: custom debug function not processing all args #13418 #13364
+ * docs: add virtuals schema options #13407 [hasezoey](https://github.com/hasezoey)
+ * docs: clarify `JSON.stringify()` virtuals docs #13273 [iatenine](https://github.com/iatenine)
+
+7.1.1 / 2023-05-10
+==================
+ * fix(document): handle set() from top-level underneath a map of mixed #13386
+ * fix: don't modify passed options object to `createConnection()` #13376
+ * types: make lean() not clobber result type for updateOne(), etc. #13389 #13382
+ * types: handle union types in FlattenMaps #13368 #13346 [Jokero](https://github.com/Jokero)
+ * types(document): correct return type for Model.prototype.deleteOne(): promise, not query #13367 #13223
+ * types: update document.d.ts $set function params to match set #13304 [jeffersonlipsky](https://github.com/jeffersonlipsky)
+ * docs: add excludeIndexes to the guide schema options list #13377 #13287
+ * docs: fix broken "fork me" on home page #13336
+
6.11.1 / 2023-05-08
===================
* fix(query): apply schema-level paths before calculating projection for findOneAndUpdate() #13348 #13340
@@ -22,6 +959,43 @@
* fix(update): handle casting doubly nested arrays with $pullAll #13285
* docs: backport documentation versioning changes to 6.x #13253 #13190 [hasezoey](https://github.com/hasezoey)
+7.1.0 / 2023-04-27
+==================
+ * feat: upgrade mongodb -> 5.3.0
+ * feat(schema): add BigInt support, upgrade mongodb -> 5.3.0 #13318 #13081 #6936
+ * feat: handle MongoDB's new UUID type, export mongoose.Types.UUID #13323 #13103
+ * feat: implement createCollections() #13324
+ * feat(query): add isPathSelectedInclusive function on query #13177
+ * types: added overloads for Schema.pre/post with different values for SchemaPreOptions #12680 [jpilgrim](https://github.com/jpilgrim)
+ * types(query): make lean() flatten out inferred maps into Record #13326 #13010
+ * docs: update README deno url #13332
+ * docs: update jsdoc to use full URLs instead of non-prefix absolute urls (also fix some urls) #13328 [hasezoey](https://github.com/hasezoey)
+ * docs: reload api js files on change #13313 [hasezoey](https://github.com/hasezoey)
+ * docs: update website sidebar to be better use-able #13321 [hasezoey](https://github.com/hasezoey)
+ * docs: fix schematype @see links #13310 [hasezoey](https://github.com/hasezoey)
+ * docs(subdocuments): remove callback usage, use deleteOne() rather than remove() re: #13284 #13316
+
+7.0.5 / 2023-04-24
+==================
+ * fix(schema): correctly handle uuids with populate() #13317 #13267
+ * fix(schema): add clusteredIndex to schema options #13286 [jakesjews](https://github.com/jakesjews)
+ * fix(document): use collection.findOne() for saving docs with no changes to avoid firing findOne middleware #13298
+ * types(schema): avoid circular constraint in TSchemaOptions with --incremental by deferring ResolveSchemaOptions<> #13291 #13129
+ * docs(subdocs): fix mention of subdocument ".remove" function #13312 [hasezoey](https://github.com/hasezoey)
+ * docs: add mongoose.Promise removal to migrating to 7 guide #13295
+ * docs: updated formatting of Error Handling section to better highlight the two kinds of possible errors #13279 [Ankit-Mandal](https://github.com/Ankit-Mandal)
+ * docs: fix broken link #13301 #13281
+
+7.0.4 / 2023-04-17
+==================
+ * fix(schema): fix dangling reference to virtual in tree after removeVirtual() #13255 #13085
+ * fix(query): cast query filters on `findOneAndUpdate()` #13220 #13219 [dermasmid](https://github.com/dermasmid)
+ * types(model): aligned watch() type for mongodb 4.6.0 #13208 #13206
+ * docs: fix async function anchors #13226 [hasezoey](https://github.com/hasezoey)
+ * docs: fix schema syntax in exemple #13262 [c-marc](https://github.com/c-marc)
+ * docs: rework scripts to allow easier setting of current and past versions #13222
+#13148 [hasezoey](https://github.com/hasezoey)
+
6.10.5 / 2023-04-06
===================
* perf(document): avoid unnecessary loops, conditionals, string manipulation on Document.prototype.get() for 10x speedup on top-level properties #12953
@@ -35,12 +1009,35 @@
* fix: backport fix for array filters handling $or and $and #13195 #13192 #10696 [raj-goguardian](https://github.com/raj-goguardian)
* fix: update the isIndexEqual function to take into account non-text indexes when checking compound indexes that include both text and non-text indexes #13138 #13136 [rdeavila94](https://github.com/rdeavila94)
+7.0.3 / 2023-03-23
+==================
+ * fix(query): avoid executing transforms if query wasn't executed #13185 #13165
+ * fix(schema): make creating top-level virtual underneath subdocument equivalent to creating virtual on the subdocument #13197 #13189
+ * fix(timestamps): set timestamps on empty replaceOne() #13196 #13170
+ * fix(types): change return type of lean() to include null if nullable #13155 #13151 [lpizzinidev](https://github.com/lpizzinidev)
+ * fix(types): fixed type of DocumentArray constructor parameter #13183 #13087 [lpizzinidev](https://github.com/lpizzinidev)
+ * docs: refactor header naming to lessen conflicts #12901 [hasezoey](https://github.com/hasezoey)
+ * docs: change header levels to be consistent across files #13173 [hasezoey](https://github.com/hasezoey)
+
6.10.4 / 2023-03-21
===================
* fix(document): apply setters on resulting value when calling Document.prototype.$inc() #13178 #13158
* fix(model): add results property to unordered insertMany() to make it easy to identify exactly which documents were inserted #13163 #12791
* docs(guide+schematypes): add UUID to schematypes guide #13184
+7.0.2 / 2023-03-15
+==================
+ * fix: validate array elements when passing array path to validateSync() in pathsToValidate #13167 #13159
+ * fix(schema): propagate typeKey down to implicitly created subdocuments #13164 #13154
+ * fix(types): add index param to eachAsync fn #13153 [krosenk729](https://github.com/krosenk729)
+ * fix(types/documentarray): type DocumentArray constructor parameter as object #13089 #13087 [lpizzinidev](https://github.com/lpizzinidev)
+ * fix(types): type query `select()` as string, string[], or record; not `any` #13146 #13142 [rbereziuk](https://github.com/rbereziuk)
+ * fix(types/query): change QueryOptions lean type to Record #13150 [lpizzinidev](https://github.com/lpizzinidev)
+ * docs: add and run eslint-plugin-markdown #13156 [hasezoey](https://github.com/hasezoey)
+ * docs(generateSearch): fix search generation for API #13161 [hasezoey](https://github.com/hasezoey)
+ * docs(generateSearch): move config missing error to require #13160 [hasezoey](https://github.com/hasezoey)
+ * chore: remove unused docs libraries #13172 [hasezoey](https://github.com/hasezoey)
+
6.10.3 / 2023-03-13
===================
* fix(connection): add stub implementation of doClose to base connection class #13157
@@ -55,6 +1052,15 @@
* fix: undo accidental change to `engines` in `package.json` #13124 [lorand-horvath](https://github.com/lorand-horvath)
* docs: quick improvement to Model.init() docs #13054
+7.0.1 / 2023-03-06
+==================
+ * fix(aggregate): added await to prevent exception in aggregate exec #13126 [lpizzinidev](https://github.com/lpizzinidev)
+ * fix(types): handle Record as value for HydratedDocument TOverrides parameter #13123 #13094
+ * fix(types): remove "update" function #13120 [hasezoey](https://github.com/hasezoey)
+ * docs(compatibility): added mongoDB server compatibility for mongoose 7 #13102 [lpizzinidev](https://github.com/lpizzinidev)
+ * docs: Updated callback method for Model.findOne() #13096 [Arghyahub](https://github.com/Arghyahub)
+ * chore: update github actions to not use ubuntu-18.04 anymore #13137 [hasezoey](https://github.com/hasezoey)
+
6.10.1 / 2023-03-03
===================
* fix: avoid removing empty query filters in `$and` and `$or` #13086 #12898
@@ -64,6 +1070,29 @@
* docs: fix code in headers for migrating_to_5 #13077 [hasezoey](https://github.com/hasezoey)
* docs: backport misc documentation changes into 6.x #13091 [hasezoey](https://github.com/hasezoey)
+7.0.0 / 2023-02-27
+==================
+ * BREAKING CHANGE: copy schema options when merging schemas using new Schema() or Schema.prototype.add() #13092
+ * feat(types): export mongodb types more robustly #12948 [simon-abbott](https://github.com/simon-abbott)
+ * docs: fix populate docs #13090 [hasezoey](https://github.com/hasezoey)
+ * docs(migrating_to_6): added info about removal of reconnectTries and reconnectInterval options #13083 [lpizzinidev](https://github.com/lpizzinidev)
+
+7.0.0-rc0 / 2023-02-23
+======================
+ * BREAKING CHANGE: remove support for callbacks #11431
+ * BREAKING CHANGE: upgrade to MongoDB node driver 5.x, bson 5.x #12955
+ * BREAKING CHANGE: make `strictQuery: false` by default #11861 #11807 #11514
+ * BREAKING CHANGE: remove support for setting schema path definitions to primitives, except `_id: false` #12832 #7558 [lpizzinidev](https://github.com/lpizzinidev)
+ * BREAKING CHANGE: discriminator schemas now inherit base schema options by default #12928 #12135
+ * BREAKING CHANGE: orFail() now throws on updateOne() and updateMany() if matchedCount === 0, not modifiedCount === 0 #11620
+ * BREAKING CHANGE: remove support for custom promise libraries #12878 #12872 [lpizzinidev](https://github.com/lpizzinidev)
+ * BREAKING CHANGE: select('name -path') behaves as select('name') if path has schema-level select: true #11694
+ * BREAKING CHANGE(types): remove support for document interfaces that extends Document #11615
+ * BREAKING CHANGE: pluralize 'human' as 'humans', not 'humen' #13037
+ * BREAKING CHANGE: renamed schema option supressReservedKeysWarning -> suppressReservedKeysWarning #11495
+ * BREAKING CHANGE: remove unused DisconnectedError #13028 [lpizzinidev](https://github.com/lpizzinidev)
+ * BREAKING CHANGE: remove unsupported query options maxScan and snapshot #13023 #13022 [hasezoey](https://github.com/hasezoey)
+
6.10.0 / 2023-02-22
===================
* feat: upgrade to mongodb driver 4.14.0 #13036
@@ -103,7 +1132,7 @@
* fix(document): isModified should not be triggered when setting a nested boolean to the same value as previously #12994 [lpizzinidev](https://github.com/lpizzinidev)
* fix(document): save newly set defaults underneath single nested subdocuments #13002 #12905
* fix(update): handle custom discriminator model name when casting update #12947 [wassil](https://github.com/wassil)
- * fix(connection): handles unique autoincrement ID for connections #12990 [lpizzinidev](https://github.com/lpizzinidev)
+ * fix(connection): handles unique autoincrement ID for connections #12990 [lpizzinidev](https://github.com/lpizzinidev)
* fix(types): fix type of options of Model.aggregate #12933 [ghost91-](https://github.com/ghost91-)
* fix(types): fix "near" aggregation operator input type #12954 [Jokero](https://github.com/Jokero)
* fix(types): add missing Top operator to AccumulatorOperator type declaration #12952 [lpizzinidev](https://github.com/lpizzinidev)
@@ -132,7 +1161,7 @@
* docs(typescript): add notes about virtual context to Mongoose 6 migration and TypeScript virtuals docs #12912 #12806
* docs(schematypes): removed dead link and fixed formatting #12897 #12885 [lpizzinidev](https://github.com/lpizzinidev)
* docs: fix link to lean api #12910 [manniL](https://github.com/manniL)
- * docs: list all possible strings for schema.pre in one place #12868
+ * docs: list all possible strings for schema.pre in one place #12868
* docs: add list of known incompatible npm packages #12892 [IslandRhythms](https://github.com/IslandRhythms)
6.8.3 / 2023-01-06
@@ -711,7 +1740,7 @@
* fix(index.d.ts): ValidationError `errors` only contains CastError or ValidationError #11369 [Uzlopak](https://github.com/Uzlopak)
* fix(index.d.ts): make InsertManyResult.insertedIds return an array of Types.ObjectId by default #11197
* fix(index.d.ts): allow pre('save') middleware with pre options #11257
- * fix(index.d.ts): add `supressReservedKeysWarning` option to schema #11439 [hiukky](https://github.com/hiukky)
+ * fix(index.d.ts): add `suppressReservedKeysWarning` option to schema #11439 [hiukky](https://github.com/hiukky)
* docs(connections): improve replica set hostname docs with correct error message and info about `err.reason.servers` #11200
* docs(populate): add virtual populate match option documentation #11411 [remirobichet](https://github.com/remirobichet)
* docs(document): add note to API docs that flattenMaps defaults to `true` for `toJSON()` but not `toObject()` #11213
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 4b5246b66ce..6ba098d3897 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,53 +1,53 @@
-## Contributing to Mongoose
+# Contributing to Mongoose
If you have a question about Mongoose (not a bug report) please post it to either [StackOverflow](http://stackoverflow.com/questions/tagged/mongoose), or on [Gitter](https://gitter.im/Automattic/mongoose?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
-### Reporting bugs
+## Reporting bugs
-- Before opening a new issue, look for existing [issues](https://github.com/Automattic/mongoose/issues) to avoid duplication. If the issue does not yet exist, [create one](https://github.com/Automattic/mongoose/issues/new).
- - Please post any relevant code samples, preferably a standalone script that
+* Before opening a new issue, look for existing [issues](https://github.com/Automattic/mongoose/issues) to avoid duplication. If the issue does not yet exist, [create one](https://github.com/Automattic/mongoose/issues/new).
+ * Please post any relevant code samples, preferably a standalone script that
reproduces your issue. Do **not** describe your issue in prose. **Show your code.**
- - If the bug involves an error, please post the stack trace.
- - Please post the version of Mongoose and MongoDB that you're using.
- - Please write bug reports in JavaScript (ES5, ES6, etc) that runs in Node.js, **not** CoffeeScript, TypeScript, JSX, etc.
-
-### Requesting new features
-
-- Before opening a new issue, look for existing [issues](https://github.com/learnboost/mongoose/issues) to avoid duplication. If the issue does not yet exist, [create one](https://github.com/learnboost/mongoose/issues/new).
-- Please describe a use case for it
-- Please include test cases if possible
-
-### Fixing bugs / Adding features
-
-- Before starting to write code, look for existing [issues](https://github.com/learnboost/mongoose/issues). That way you avoid working on something that might not be of interest or that has been addressed already in a different branch. You can create a new issue [here](https://github.com/learnboost/mongoose/issues/new).
- - _The source of this project is written in JavaScript, not CoffeeScript or TypeScript. Please write your bug reports in JavaScript that can run in vanilla Node.js_.
-- Fork the [repo](https://github.com/Automattic/mongoose) _or_ for small documentation changes, navigate to the source on github and click the [Edit](https://github.com/blog/844-forking-with-the-edit-button) button.
-- Follow the general coding style of the rest of the project:
- - 2 space tabs
- - no trailing whitespace
- - inline documentation for new methods, class members, etc.
- - 1 space between conditionals, no space before function parenthesis
- - `if (..) {`
- - `for (..) {`
- - `while (..) {`
- - `function(err) {`
-- Write tests and make sure they pass (tests are in the [test](https://github.com/Automattic/mongoose/tree/master/test) directory).
-- Write typings-tests if you modify the typescript-typings. (tests are in the [test/types](https://github.com/Automattic/mongoose/tree/master/test/types) directory).
-
-### Running the tests
-
-- Open a terminal and navigate to the root of the project
-- execute `npm install` to install the necessary dependencies
-- execute `npm run mongo` to start a MongoDB instance on port 27017. This step is optional, if you have already a database running on port 27017. To spin up a specific mongo version, you can do it by executing `npm run mongo -- {version}`. E.g. you want to spin up a mongo 4.2.2 server, you execute `npm run mongo -- 4.2.2`
-- execute `npm test` to run the tests (we're using [mocha](http://mochajs.org/))
- - or to execute a single test `npm test -- -g 'some regexp that matches the test description'`
- - any mocha flags can be specified with `-- `
- - For example, you can use `npm test -- -R spec` to use the spec reporter, rather than the dot reporter (by default, the test output looks like a bunch of dots)
- - execute `npm run test-tsd` to run the typescript tests
- - execute `npm run ts-benchmark` to run the typescript benchmark "performance test" for a single time.
- - execute `npm run ts-benchmark-watch` to run the typescript benchmark "performance test" while watching changes on types folder. Note: Make sure to commit all changes before executing this command.
-
-### Documentation
+ * If the bug involves an error, please post the stack trace.
+ * Please post the version of Mongoose and MongoDB that you're using.
+ * Please write bug reports in JavaScript (ES5, ES6, etc) that runs in Node.js, **not** CoffeeScript, TypeScript, JSX, etc.
+
+## Requesting new features
+
+* Before opening a new issue, look for existing [issues](https://github.com/learnboost/mongoose/issues) to avoid duplication. If the issue does not yet exist, [create one](https://github.com/learnboost/mongoose/issues/new).
+* Please describe a use case for it
+* Please include test cases if possible
+
+## Fixing bugs / Adding features
+
+* Before starting to write code, look for existing [issues](https://github.com/learnboost/mongoose/issues). That way you avoid working on something that might not be of interest or that has been addressed already in a different branch. You can create a new issue [here](https://github.com/learnboost/mongoose/issues/new).
+ * *The source of this project is written in JavaScript, not CoffeeScript or TypeScript. Please write your bug reports in JavaScript that can run in vanilla Node.js*.
+* Fork the [repo](https://github.com/Automattic/mongoose) *or* for small documentation changes, navigate to the source on github and click the [Edit](https://github.com/blog/844-forking-with-the-edit-button) button.
+* Follow the general coding style of the rest of the project:
+ * 2 space tabs
+ * no trailing whitespace
+ * inline documentation for new methods, class members, etc.
+ * 1 space between conditionals, no space before function parenthesis
+ * `if (..) {`
+ * `for (..) {`
+ * `while (..) {`
+ * `function(err) {`
+* Write tests and make sure they pass (tests are in the [test](https://github.com/Automattic/mongoose/tree/master/test) directory).
+* Write typings-tests if you modify the typescript-typings. (tests are in the [test/types](https://github.com/Automattic/mongoose/tree/master/test/types) directory).
+
+## Running the tests
+
+* Open a terminal and navigate to the root of the project
+* execute `npm install` to install the necessary dependencies
+* execute `npm run mongo` to start a MongoDB instance on port 27017. This step is optional, if you have already a database running on port 27017. To spin up a specific mongo version, you can do it by executing `npm run mongo -- {version}`. E.g. you want to spin up a mongo 4.2.2 server, you execute `npm run mongo -- 4.2.2`
+* execute `npm test` to run the tests (we're using [mocha](http://mochajs.org/))
+ * or to execute a single test `npm test -- -g 'some regexp that matches the test description'`
+ * any mocha flags can be specified with `-- `
+ * For example, you can use `npm test -- -R spec` to use the spec reporter, rather than the dot reporter (by default, the test output looks like a bunch of dots)
+ * execute `npm run test-tsd` to run the typescript tests
+ * execute `npm run ts-benchmark` to run the typescript benchmark "performance test" for a single time.
+ * execute `npm run ts-benchmark-watch` to run the typescript benchmark "performance test" while watching changes on types folder. Note: Make sure to commit all changes before executing this command.
+
+## Documentation
To contribute to the [API documentation](http://mongoosejs.com/docs/api/mongoose.html) just make your changes to the inline documentation of the appropriate [source code](https://github.com/Automattic/mongoose/tree/master/lib) in the master branch and submit a [pull request](https://help.github.com/articles/using-pull-requests/). You might also use the github [Edit](https://github.com/blog/844-forking-with-the-edit-button) button.
@@ -60,13 +60,13 @@ If you'd like to preview your documentation changes, first commit your changes t
Visit `http://127.0.0.1:8089` and you should see the docs with your local changes. Make sure you `npm run docs:clean` before committing, because automated generated files to `docs/*` should **not** be in PRs.
-#### Documentation Style Guidelines
+### Documentation Style Guidelines
There are some guidelines to keep the style for the documentation consistent:
-- All links that refer to some other file in the mongoose documentation needs to be relative without a prefix unless required (use `guide.html` over `./guide.html` or `/docs/guide.html`)
+* All links that refer to some other file in the mongoose documentation needs to be relative without a prefix unless required (use `guide.html` over `./guide.html` or `/docs/guide.html`)
-### Plugins website
+## Plugins website
The [plugins](http://plugins.mongoosejs.io/) site is also an [open source project](https://github.com/vkarpov15/mongooseplugins) that you can get involved with. Feel free to fork and improve it as well!
@@ -80,10 +80,10 @@ Anyone can file an expense. If the expense makes sense for the development of th
### Contributors
Thank you to all the people who have already contributed to mongoose!
-
+
### Backers
Thank you to all our backers! [[Become a backer](https://opencollective.com/mongoose#backer)]
-
+
diff --git a/README.md b/README.md
index dc855397156..b8f0836bc48 100644
--- a/README.md
+++ b/README.md
@@ -13,15 +13,15 @@ Mongoose is a [MongoDB](https://www.mongodb.org/) object modeling tool designed
The official documentation website is [mongoosejs.com](http://mongoosejs.com/).
-Mongoose 6.0.0 was released on August 24, 2021. You can find more details on [backwards breaking changes in 6.0.0 on our docs site](https://mongoosejs.com/docs/migrating_to_6.html).
+Mongoose 8.0.0 was released on October 31, 2023. You can find more details on [backwards breaking changes in 8.0.0 on our docs site](https://mongoosejs.com/docs/migrating_to_8.html).
## Support
- - [Stack Overflow](http://stackoverflow.com/questions/tagged/mongoose)
- - [Bug Reports](https://github.com/Automattic/mongoose/issues/)
- - [Mongoose Slack Channel](http://slack.mongoosejs.io/)
- - [Help Forum](http://groups.google.com/group/mongoose-orm)
- - [MongoDB Support](https://www.mongodb.com/docs/manual/support/)
+* [Stack Overflow](http://stackoverflow.com/questions/tagged/mongoose)
+* [Bug Reports](https://github.com/Automattic/mongoose/issues/)
+* [Mongoose Slack Channel](http://slack.mongoosejs.io/)
+* [Help Forum](http://groups.google.com/group/mongoose-orm)
+* [MongoDB Support](https://www.mongodb.com/docs/manual/support/)
## Plugins
@@ -43,7 +43,7 @@ View all 400+ [contributors](https://github.com/Automattic/mongoose/graphs/contr
First install [Node.js](http://nodejs.org/) and [MongoDB](https://www.mongodb.org/downloads). Then:
```sh
-$ npm install mongoose
+npm install mongoose
```
Mongoose 6.8.0 also includes alpha support for [Deno](https://deno.land/).
@@ -61,7 +61,7 @@ import mongoose from 'mongoose';
Or, using [Deno's `createRequire()` for CommonJS support](https://deno.land/std@0.113.0/node/README.md?source=#commonjs-modules-loading) as follows.
```javascript
-import { createRequire } from 'https://deno.land/std/node/module.ts';
+import { createRequire } from 'https://deno.land/std@0.177.0/node/module.ts';
const require = createRequire(import.meta.url);
const mongoose = require('mongoose');
@@ -72,7 +72,7 @@ mongoose.connect('mongodb://127.0.0.1:27017/test')
You can then run the above script using the following.
-```
+```sh
deno run --allow-net --allow-read --allow-sys --allow-env mongoose-test.js
```
@@ -96,7 +96,7 @@ await mongoose.connect('mongodb://127.0.0.1/my_database');
Once connected, the `open` event is fired on the `Connection` instance. If you're using `mongoose.connect`, the `Connection` is `mongoose.connection`. Otherwise, `mongoose.createConnection` return value is a `Connection`.
-**Note:** _If the local connection fails then try using 127.0.0.1 instead of localhost. Sometimes issues may arise when the local hostname has been changed._
+**Note:** *If the local connection fails then try using 127.0.0.1 instead of localhost. Sometimes issues may arise when the local hostname has been changed.*
**Important!** Mongoose buffers all the commands until it's connected to the database. This means that you don't have to wait until it connects to MongoDB in order to define models, run queries, etc.
@@ -168,30 +168,26 @@ Or just do it all at once
const MyModel = mongoose.model('ModelName', mySchema);
```
-The first argument is the _singular_ name of the collection your model is for. **Mongoose automatically looks for the _plural_ version of your model name.** For example, if you use
+The first argument is the *singular* name of the collection your model is for. **Mongoose automatically looks for the *plural* version of your model name.** For example, if you use
```js
const MyModel = mongoose.model('Ticket', mySchema);
```
-Then `MyModel` will use the __tickets__ collection, not the __ticket__ collection. For more details read the [model docs](https://mongoosejs.com/docs/api/mongoose.html#mongoose_Mongoose-model).
+Then `MyModel` will use the **tickets** collection, not the **ticket** collection. For more details read the [model docs](https://mongoosejs.com/docs/api/mongoose.html#mongoose_Mongoose-model).
Once we have our model, we can then instantiate it, and save it:
```js
const instance = new MyModel();
instance.my.key = 'hello';
-instance.save(function(err) {
- //
-});
+await instance.save();
```
Or we can find documents from the same collection
```js
-MyModel.find({}, function(err, docs) {
- // docs.forEach
-});
+await MyModel.find({});
```
You can also `findOne`, `findById`, `update`, etc.
@@ -208,8 +204,8 @@ For more details check out [the docs](http://mongoosejs.com/docs/queries.html).
```js
const conn = mongoose.createConnection('your connection string');
const MyModel = conn.model('ModelName', schema);
-const m = new MyModel;
-m.save(); // works
+const m = new MyModel();
+await m.save(); // works
```
vs
@@ -217,15 +213,15 @@ vs
```js
const conn = mongoose.createConnection('your connection string');
const MyModel = mongoose.model('ModelName', schema);
-const m = new MyModel;
-m.save(); // does not work b/c the default connection object was never connected
+const m = new MyModel();
+await m.save(); // does not work b/c the default connection object was never connected
```
### Embedded Documents
In the first example snippet, we defined a key in the Schema that looks like:
-```
+```txt
comments: [Comment]
```
@@ -241,26 +237,18 @@ const post = new BlogPost();
// create a comment
post.comments.push({ title: 'My comment' });
-post.save(function(err) {
- if (!err) console.log('Success!');
-});
+await post.save();
```
The same goes for removing them:
```js
-BlogPost.findById(myId, function(err, post) {
- if (!err) {
- post.comments[0].remove();
- post.save(function(err) {
- // do something
- });
- }
-});
+const post = await BlogPost.findById(myId);
+post.comments[0].deleteOne();
+await post.save();
```
-Embedded documents enjoy all the same features as your models. Defaults, validators, middleware. Whenever an error occurs, it's bubbled to the `save()` error callback, so error handling is a snap!
-
+Embedded documents enjoy all the same features as your models. Defaults, validators, middleware.
### Middleware
@@ -342,26 +330,26 @@ and [acquit](https://github.com/vkarpov15/acquit).
## Related Projects
-#### MongoDB Runners
+### MongoDB Runners
-- [run-rs](https://www.npmjs.com/package/run-rs)
-- [mongodb-memory-server](https://www.npmjs.com/package/mongodb-memory-server)
-- [mongodb-topology-manager](https://www.npmjs.com/package/mongodb-topology-manager)
+* [run-rs](https://www.npmjs.com/package/run-rs)
+* [mongodb-memory-server](https://www.npmjs.com/package/mongodb-memory-server)
+* [mongodb-topology-manager](https://www.npmjs.com/package/mongodb-topology-manager)
-#### Unofficial CLIs
+### Unofficial CLIs
-- [mongoosejs-cli](https://www.npmjs.com/package/mongoosejs-cli)
+* [mongoosejs-cli](https://www.npmjs.com/package/mongoosejs-cli)
-#### Data Seeding
+### Data Seeding
-- [dookie](https://www.npmjs.com/package/dookie)
-- [seedgoose](https://www.npmjs.com/package/seedgoose)
-- [mongoose-data-seed](https://www.npmjs.com/package/mongoose-data-seed)
+* [dookie](https://www.npmjs.com/package/dookie)
+* [seedgoose](https://www.npmjs.com/package/seedgoose)
+* [mongoose-data-seed](https://www.npmjs.com/package/mongoose-data-seed)
-#### Express Session Stores
+### Express Session Stores
-- [connect-mongodb-session](https://www.npmjs.com/package/connect-mongodb-session)
-- [connect-mongo](https://www.npmjs.com/package/connect-mongo)
+* [connect-mongodb-session](https://www.npmjs.com/package/connect-mongodb-session)
+* [connect-mongo](https://www.npmjs.com/package/connect-mongo)
## License
diff --git a/benchmarks/benchjs/casting.js b/benchmarks/benchjs/casting.js
deleted file mode 100644
index f7027eed6b0..00000000000
--- a/benchmarks/benchjs/casting.js
+++ /dev/null
@@ -1,151 +0,0 @@
-'use strict';
-const mongoose = require('../../lib');
-const Benchmark = require('benchmark');
-
-const suite = new Benchmark.Suite();
-
-const Schema = mongoose.Schema;
-const ObjectId = Schema.Types.ObjectId;
-const utils = require('../../lib/utils.js');
-
-// to make things work in the way the are normally described online...
-/*
- *global.Schema = Schema;
- *global.mongoose = mongoose;
- */
-
-/**
- * These are all the benchmark tests for casting stuff
- */
-
-const Comments = new Schema();
-
-Comments.add({
- title: String,
- date: Date,
- body: String,
- comments: [Comments],
-});
-
-const BlogPost = new Schema({
- title: String,
- author: String,
- slug: String,
- date: Date,
- meta: {
- date: Date,
- visitors: Number,
- },
- published: Boolean,
- mixed: {},
- numbers: [Number],
- tags: [String],
- owners: [ObjectId],
- comments: [Comments],
- def: {
- type: String,
- default: 'kandinsky',
- },
-});
-
-const commentData = {
- title: 'test comment',
- date: new Date(),
- body: 'this be some crazzzyyyyy text that would go in a comment',
- comments: [{ title: 'second level', date: new Date(), body: 'texttt' }],
-};
-
-const blogData = {
- title: 'dummy post',
- author: 'somebody',
- slug: 'test.post',
- date: new Date(),
- meta: {
- date: new Date(),
- visitors: 9001,
- },
- published: true,
- mixed: {
- thisIsRandom: true,
- },
- numbers: [1, 2, 7, 10, 23432],
- tags: ['test', 'BENCH', 'things', 'more things'],
- def: 'THANGS!!!',
- comments: [],
-};
-
-const blogData10 = utils.clone(blogData);
-const blogData100 = utils.clone(blogData);
-const blogData1000 = utils.clone(blogData);
-const blogData10000 = utils.clone(blogData);
-
-for (let i = 0; i < 10; i++) {
- blogData10.comments.push(commentData);
-}
-for (let i = 0; i < 100; i++) {
- blogData100.comments.push(commentData);
-}
-for (let i = 0; i < 1000; i++) {
- blogData1000.comments.push(commentData);
-}
-for (let i = 0; i < 10000; i++) {
- blogData10000.comments.push(commentData);
-}
-
-mongoose.model('BlogPost', BlogPost);
-
-suite
- .add('Casting - Embedded Docs - 0 Docs', {
- fn: function () {
- const BlogPost = mongoose.model('BlogPost');
- const bp = new BlogPost();
- bp.init(blogData);
- },
- })
- .add('Casting - Embedded Docs - 10 Docs', {
- fn: function () {
- const BlogPost = mongoose.model('BlogPost');
- const bp = new BlogPost();
- bp.init(blogData10);
- },
- })
- .add('Casting - Embedded Docs - 100 Docs', {
- fn: function () {
- const BlogPost = mongoose.model('BlogPost');
- const bp = new BlogPost();
- bp.init(blogData100);
- },
- })
- .add('Casting - Embedded Docs - 1000 Docs', {
- fn: function () {
- const BlogPost = mongoose.model('BlogPost');
- const bp = new BlogPost();
- bp.init(blogData1000);
- },
- })
- .add('Casting - Embedded Docs - 10000 Docs', {
- fn: function () {
- const BlogPost = mongoose.model('BlogPost');
- const bp = new BlogPost();
- bp.init(blogData10000);
- },
- })
- .on('cycle', function (evt) {
- if (process.env.MONGOOSE_DEV || process.env.PULL_REQUEST) {
- console.log(String(evt.target));
- }
- })
- .on('complete', function () {
- if (!process.env.MONGOOSE_DEV && !process.env.PULL_REQUEST) {
- const outObj = {};
- this.forEach(function (item) {
- const out = {};
- out.stats = item.stats;
- delete out.stats.sample;
- out.ops = item.hz;
- outObj[item.name.replace(/\s/g, '')] = out;
- });
- console.dir(outObj, { depth: null, colors: true });
- }
- })
- .run({ async: true });
diff --git a/benchmarks/benchjs/delete.js b/benchmarks/benchjs/delete.js
deleted file mode 100644
index 1d717f7f6e3..00000000000
--- a/benchmarks/benchjs/delete.js
+++ /dev/null
@@ -1,136 +0,0 @@
-'use strict';
-const mongoose = require('../../lib');
-const Benchmark = require('benchmark');
-
-const suite = new Benchmark.Suite();
-
-const Schema = mongoose.Schema;
-const mongoClient = require('mongodb').MongoClient;
-
-// to make things work in the way the are normally described online...
-/*
- *global.Schema = Schema;
- *global.mongoose = mongoose;
- */
-
-/**
- * These are all the benchmark tests for deleting data
- */
-
-mongoose.connect('mongodb://127.0.0.1/mongoose-bench', function (err) {
- if (err) {
- throw err;
- }
- mongoClient.connect('mongodb://127.0.0.1', function (err, client) {
- if (err) {
- throw err;
- }
-
- const db = client.db('mongoose-bench');
-
- const UserSchema = new Schema({
- name: String,
- age: Number,
- likes: [String],
- address: String,
- });
-
- const User = mongoose.model('User', UserSchema);
- const user = db.collection('user');
-
- const mIds = [];
- const dIds = [];
-
- const data = {
- name: 'name',
- age: 0,
- likes: ['dogs', 'cats', 'pizza'],
- address: ' Nowhere-ville USA',
- };
-
- // insert all of the data here
- let count = 1000;
- for (let i = 0; i < 500; i++) {
- User.create(data, function (err, u) {
- if (err) {
- throw err;
- }
- mIds.push(u.id);
- --count || next();
- });
- const nData = {};
- nData.name = data.name;
- nData.age = data.age;
- nData.likes = data.likes;
- nData.address = data.address;
- user.insertOne(nData, function (err, res) {
- dIds.push(res.insertedId);
- --count || next();
- });
- }
-
- function closeDB() {
- User.count(function (err, res) {
- if (res !== 0) {
- console.log('Still mongoose entries left...');
- }
- mongoose.disconnect();
- });
- user.count({}, function (err, res) {
- if (res !== 0) {
- console.log('Still driver entries left...');
- }
- if (err) {
- throw err;
- }
- client.close();
- });
- }
-
- suite
- .add('Delete - Mongoose', {
- defer: true,
- fn: function (deferred) {
- User.remove({ _id: mIds.pop() }, function (err) {
- if (err) {
- throw err;
- }
- deferred.resolve();
- });
- },
- })
- .add('Delete - Driver', {
- defer: true,
- fn: function (deferred) {
- user.deleteOne({ _id: dIds.pop() }, function (err) {
- if (err) {
- throw err;
- }
- deferred.resolve();
- });
- },
- })
- .on('cycle', function (evt) {
- if (process.env.MONGOOSE_DEV || process.env.PULL_REQUEST) {
- console.log(String(evt.target));
- }
- })
- .on('complete', function () {
- closeDB();
- if (!process.env.MONGOOSE_DEV && !process.env.PULL_REQUEST) {
- const outObj = {};
- this.forEach(function (item) {
- const out = {};
- out.stats = item.stats;
- delete out.stats.sample;
- out.ops = item.hz;
- outObj[item.name.replace(/\s/g, '')] = out;
- });
- console.dir(outObj, { depth: null, colors: true });
- }
- });
- function next() {
- suite.run({ async: true });
- }
- });
-});
diff --git a/benchmarks/benchjs/insert.js b/benchmarks/benchjs/insert.js
deleted file mode 100644
index cd01bfb6069..00000000000
--- a/benchmarks/benchjs/insert.js
+++ /dev/null
@@ -1,189 +0,0 @@
-'use strict';
-const mongoose = require('../../lib');
-const Benchmark = require('benchmark');
-
-const suite = new Benchmark.Suite();
-
-const Schema = mongoose.Schema;
-const mongoClient = require('mongodb').MongoClient;
-const utils = require('../../lib/utils.js');
-const ObjectId = Schema.Types.ObjectId;
-
-// to make things work in the way the are normally described online...
-/*
- *global.Schema = Schema;
- *global.mongoose = mongoose;
- */
-
-/**
- * These are all the benchmark tests for inserting data
- */
-
-mongoose.connect('mongodb://127.0.0.1/mongoose-bench', function (err) {
- if (err) {
- throw err;
- }
- mongoClient.connect(
- 'mongodb://127.0.0.1/mongoose-bench',
- function (err, client) {
- if (err) {
- throw err;
- }
-
- const db = client.db('mongoose-bench');
-
- const Comments = new Schema();
- Comments.add({
- title: String,
- date: Date,
- body: String,
- comments: [Comments],
- });
-
- let BlogPost = new Schema({
- title: String,
- author: String,
- slug: String,
- date: Date,
- meta: {
- date: Date,
- visitors: Number,
- },
- published: Boolean,
- mixed: {},
- numbers: [Number],
- tags: [String],
- owners: [ObjectId],
- comments: [Comments],
- def: {
- type: String,
- default: 'kandinsky',
- },
- });
-
- const blogData = {
- title: 'dummy post',
- author: 'somebody',
- slug: 'test.post',
- date: new Date(),
- meta: { date: new Date(), visitors: 9001 },
- published: true,
- mixed: { thisIsRandom: true },
- numbers: [1, 2, 7, 10, 23432],
- tags: ['test', 'BENCH', 'things', 'more things'],
- def: 'THANGS!!!',
- comments: [],
- };
- const commentData = {
- title: 'test comment',
- date: new Date(),
- body: 'this be some crazzzyyyyy text that would go in a comment',
- comments: [
- {
- title: 'second level',
- date: new Date(),
- body: 'texttt',
- },
- ],
- };
- for (let i = 0; i < 5; i++) {
- blogData.comments.push(commentData);
- }
- const data = {
- name: 'name',
- age: 0,
- likes: ['dogs', 'cats', 'pizza'],
- address: ' Nowhere-ville USA',
- };
-
- const UserSchema = new Schema({
- name: String,
- age: Number,
- likes: [String],
- address: String,
- });
-
- const User = mongoose.model('User', UserSchema);
- BlogPost = mongoose.model('BlogPost', BlogPost);
- const user = db.collection('user');
- const blogpost = db.collection('blogpost');
-
- function closeDB() {
- mongoose.connection.db.dropDatabase(function () {
- mongoose.disconnect();
- process.exit();
- });
- }
-
- suite
- .add('Insert - Mongoose - Basic', {
- defer: true,
- fn: function (deferred) {
- const nData = utils.clone(data);
- User.create(nData, function (err) {
- if (err) {
- throw err;
- }
- deferred.resolve();
- });
- },
- })
- .add('Insert - Driver - Basic', {
- defer: true,
- fn: function (deferred) {
- const nData = utils.clone(data);
- user.insertOne(nData, function (err) {
- if (err) {
- throw err;
- }
- deferred.resolve();
- });
- },
- })
- .add('Insert - Mongoose - Embedded Docs', {
- defer: true,
- fn: function (deferred) {
- const bp = utils.clone(blogData);
- BlogPost.create(bp, function (err) {
- if (err) {
- throw err;
- }
- deferred.resolve();
- });
- },
- })
- .add('Insert - Driver - Embedded Docs', {
- defer: true,
- fn: function (deferred) {
- const bp = utils.clone(blogData);
- blogpost.insertOne(bp, function (err) {
- if (err) {
- throw err;
- }
- deferred.resolve();
- });
- },
- })
- .on('cycle', function (evt) {
- if (process.env.MONGOOSE_DEV || process.env.PULL_REQUEST) {
- console.log(String(evt.target));
- }
- })
- .on('complete', function () {
- closeDB();
- if (!process.env.MONGOOSE_DEV && !process.env.PULL_REQUEST) {
- const outObj = {};
- this.forEach(function (item) {
- const out = {};
- out.stats = item.stats;
- delete out.stats.sample;
- out.ops = item.hz;
- outObj[item.name.replace(/\s/g, '')] = out;
- });
- console.dir(outObj, { depth: null, colors: true });
- }
- })
- .run({ async: true });
- }
- );
-});
diff --git a/benchmarks/benchjs/multiop.js b/benchmarks/benchjs/multiop.js
deleted file mode 100644
index c3f73d1f730..00000000000
--- a/benchmarks/benchjs/multiop.js
+++ /dev/null
@@ -1,482 +0,0 @@
-'use strict';
-const mongoose = require('../../lib');
-const Benchmark = require('benchmark');
-
-const suite = new Benchmark.Suite();
-
-const Schema = mongoose.Schema;
-const ObjectId = Schema.Types.ObjectId;
-const mongoClient = require('mongodb').MongoClient;
-const utils = require('../../lib/utils.js');
-
-// to make things work in the way the are normally described online...
-/*
- *global.Schema = Schema;
- *global.mongoose = mongoose;
- */
-
-/**
- * These are all the benchmark tests for mixed data operations
- */
-
-mongoose.connect('mongodb://127.0.0.1/mongoose-bench', function (err) {
- if (err) {
- throw err;
- }
- mongoClient.connect('mongodb://127.0.0.1', function (err, client) {
- if (err) {
- throw err;
- }
-
- const db = client.db('mongoose-bench');
-
- const Comments = new Schema();
- Comments.add({
- title: String,
- date: Date,
- body: String,
- comments: [Comments],
- });
-
- let BlogPost = new Schema({
- title: String,
- author: String,
- slug: String,
- date: Date,
- meta: {
- date: Date,
- visitors: Number,
- },
- published: Boolean,
- mixed: {},
- numbers: [Number],
- tags: [String],
- owners: [ObjectId],
- comments: [Comments],
- def: { type: String, default: 'kandinsky' },
- });
-
- const blogData = {
- title: 'dummy post',
- author: 'somebody',
- slug: 'test.post',
- date: new Date(),
- meta: {
- date: new Date(),
- visitors: 9001,
- },
- published: true,
- mixed: {
- thisIsRandom: true,
- },
- numbers: [1, 2, 7, 10, 23432],
- tags: ['test', 'BENCH', 'things', 'more things'],
- def: 'THANGS!!!',
- comments: [],
- };
- const commentData = {
- title: 'test comment',
- date: new Date(),
- body: 'this be some crazzzyyyyy text that would go in a comment',
- comments: [
- {
- title: 'second level',
- date: new Date(),
- body: 'texttt',
- },
- ],
- };
- for (let i = 0; i < 5; i++) {
- blogData.comments.push(commentData);
- }
- const UserSchema = new Schema({
- name: String,
- age: Number,
- likes: [String],
- address: String,
- });
-
- const User = mongoose.model('User', UserSchema);
- BlogPost = mongoose.model('BlogPost', BlogPost);
- const user = db.collection('user');
- const blogpost = db.collection('blogpost');
-
- const mIds = [];
- const dIds = [];
-
- const bmIds = [];
- const bdIds = [];
-
- const data = {
- name: 'name',
- age: 0,
- likes: ['dogs', 'cats', 'pizza'],
- address: ' Nowhere-ville USA',
- };
-
- // insert all of the data here
- let count = 4000;
- for (let i = 0; i < 1000; i++) {
- data.age = Math.floor(Math.random() * 50);
- User.create(data, function (err, u) {
- if (err) {
- throw err;
- }
- mIds.push(u.id);
- --count || next();
- });
- const nData = utils.clone(data);
- user.insertOne(nData, function (err, res) {
- if (err) {
- throw err;
- }
- dIds.push(res.insertedIds);
- --count || next();
- });
- BlogPost.create(blogData, function (err, bp) {
- if (err) {
- throw err;
- }
- bmIds.push(bp.id);
- --count || next();
- });
-
- const bpData = utils.clone(blogData);
- blogpost.insertOne(bpData, function (err, res) {
- if (err) {
- throw err;
- }
- bdIds.push(res.insertedId);
- --count || next();
- });
- }
-
- let mi = 0,
- di = 0,
- bmi = 0,
- bdi = 0;
-
- function getNextmId() {
- mi = ++mi % mIds.length;
- return mIds[mi];
- }
-
- function getNextdId() {
- di = ++di % dIds.length;
- return dIds[di];
- }
-
- function getNextbmId() {
- bmi = ++bmi % bmIds.length;
- return bmIds[bmi];
- }
-
- function getNextbdId() {
- bdi = ++bdi % bdIds.length;
- return bdIds[bdi];
- }
-
- function closeDB() {
- mongoose.connection.db.dropDatabase(function () {
- mongoose.disconnect();
- process.exit();
- });
- }
-
- suite
- .add('Multi-Op - Mongoose - Heavy Read, low write', {
- defer: true,
- fn: function (deferred) {
- let count = 150;
- for (let i = 0; i < 150; i++) {
- User.findOne({ _id: getNextmId() }, function (err) {
- if (err) {
- throw err;
- }
- --count || deferred.resolve();
- });
- if (i % 15 === 0) {
- const nData = utils.clone(data);
- User.create(nData, function (err) {
- if (err) {
- throw err;
- }
- --count || deferred.resolve();
- });
- }
- }
- },
- })
- .add('Multi-Op - Driver - Heavy Read, low write', {
- defer: true,
- fn: function (deferred) {
- let count = 150;
- for (let i = 0; i < 150; i++) {
- user.findOne({ _id: getNextdId() }, function (err) {
- if (err) {
- throw err;
- }
- --count || deferred.resolve();
- });
- if (i % 15 === 0) {
- const nData = utils.clone(data);
- user.insertOne(nData, function (err) {
- if (err) {
- throw err;
- }
- --count || deferred.resolve();
- });
- }
- }
- },
- })
- .add('Multi-Op - Mongoose - Embedded Docs - Heavy Read, low write', {
- defer: true,
- fn: function (deferred) {
- let count = 150;
- for (let i = 0; i < 150; i++) {
- BlogPost.findOne({ _id: getNextbmId() }, function (err) {
- if (err) {
- throw err;
- }
- --count || deferred.resolve();
- });
- if (i % 15 === 0) {
- const nData = utils.clone(blogData);
- BlogPost.create(nData, function (err) {
- if (err) {
- throw err;
- }
- --count || deferred.resolve();
- });
- }
- }
- },
- })
- .add('Multi-Op - Driver - Embedded Docs - Heavy Read, low write', {
- defer: true,
- fn: function (deferred) {
- let count = 150;
- for (let i = 0; i < 150; i++) {
- blogpost.findOne({ _id: getNextbdId() }, function (err) {
- if (err) {
- throw err;
- }
- --count || deferred.resolve();
- });
- if (i % 15 === 0) {
- const nData = utils.clone(blogData);
- blogpost.insertOne(nData, function (err) {
- if (err) {
- throw err;
- }
- --count || deferred.resolve();
- });
- }
- }
- },
- })
- .add('Multi-Op - Mongoose - Heavy Write, low read', {
- defer: true,
- fn: function (deferred) {
- let count = 150;
-
- for (let i = 0; i < 150; i++) {
- const nData = utils.clone(data);
- User.create(nData, function (err) {
- if (err) {
- throw err;
- }
- --count || deferred.resolve();
- });
- if (i % 15 === 0) {
- User.findOne({ _id: getNextmId() }, function (err) {
- if (err) {
- throw err;
- }
- --count || deferred.resolve();
- });
- }
- }
- },
- })
- .add('Multi-Op - Driver - Heavy Write, low read', {
- defer: true,
- fn: function (deferred) {
- let count = 150;
-
- for (let i = 0; i < 150; i++) {
- const nData = utils.clone(data);
- user.insertOne(nData, function (err) {
- if (err) {
- throw err;
- }
- --count || deferred.resolve();
- });
- if (i % 15 === 0) {
- user.findOne({ _id: getNextdId() }, function (err) {
- if (err) {
- throw err;
- }
- --count || deferred.resolve();
- });
- }
- }
- },
- })
- .add('Multi-Op - Mongoose - Embedded Docs - Heavy Write, low read', {
- defer: true,
- fn: function (deferred) {
- let count = 150;
-
- for (let i = 0; i < 150; i++) {
- const nData = utils.clone(blogData);
- BlogPost.create(nData, function (err) {
- if (err) {
- throw err;
- }
- --count || deferred.resolve();
- });
- if (i % 15 === 0) {
- BlogPost.findOne({ _id: getNextbmId() }, function (err) {
- if (err) {
- throw err;
- }
- --count || deferred.resolve();
- });
- }
- }
- },
- })
- .add('Multi-Op - Driver - Embedded Docs - Heavy Write, low read', {
- defer: true,
- fn: function (deferred) {
- let count = 150;
-
- for (let i = 0; i < 150; i++) {
- const nData = utils.clone(blogData);
- blogpost.insertOne(nData, function (err) {
- if (err) {
- throw err;
- }
- --count || deferred.resolve();
- });
- if (i % 15 === 0) {
- blogpost.findOne({ _id: getNextbdId() }, function (err) {
- if (err) {
- throw err;
- }
- --count || deferred.resolve();
- });
- }
- }
- },
- })
- .add('Multi-Op - Mongoose - Embedded Docs - Read-write-update', {
- defer: true,
- fn: function (deferred) {
- let count = 150;
- let updates = 0;
- for (let i = 0; i < 150; i++) {
- BlogPost.findOne({ _id: getNextbmId() }, function (err, res) {
- if (err) {
- throw err;
- }
- if (updates < 20) {
- updates++;
- res.author = 'soemthing new';
- res.comments.push(commentData);
- res.title = 'something newerrrr';
- res.save(function (err) {
- if (err) {
- throw err;
- }
- --count || deferred.resolve();
- });
- } else {
- --count || deferred.resolve();
- }
- });
- if (i % 15 === 0) {
- const nData = utils.clone(blogData);
- BlogPost.create(nData, function (err) {
- if (err) {
- throw err;
- }
- --count || deferred.resolve();
- });
- }
- }
- },
- })
- .add('Multi-Op - Driver - Embedded Docs - Read-write-update', {
- defer: true,
- fn: function (deferred) {
- let count = 150;
- let updates = 0;
- for (let i = 0; i < 150; i++) {
- blogpost.findOne({ _id: getNextbdId() }, function (err, bp) {
- if (err) {
- throw err;
- }
- if (updates < 20) {
- updates++;
- blogpost.updateOne(
- { _id: bp._id },
- {
- $set: {
- author: 'something new',
- title: 'something newerrrr',
- },
- $push: {
- comments: commentData,
- },
- },
- { upsert: true },
- function (err) {
- if (err) {
- throw err;
- }
- --count || deferred.resolve();
- }
- );
- } else {
- --count || deferred.resolve();
- }
- });
- if (i % 15 === 0) {
- const nData = utils.clone(blogData);
- blogpost.insertOne(nData, function (err) {
- if (err) {
- throw err;
- }
- --count || deferred.resolve();
- });
- }
- }
- },
- })
- .on('cycle', function (evt) {
- if (process.env.MONGOOSE_DEV || process.env.PULL_REQUEST) {
- console.log(String(evt.target));
- }
- })
- .on('complete', function () {
- closeDB();
- if (!process.env.MONGOOSE_DEV && !process.env.PULL_REQUEST) {
- const outObj = {};
- this.forEach(function (item) {
- const out = {};
- out.stats = item.stats;
- delete out.stats.sample;
- out.ops = item.hz;
- outObj[item.name.replace(/\s/g, '')] = out;
- });
- console.dir(outObj, { depth: null, colors: true });
- }
- });
- function next() {
- suite.run({ async: true });
- }
- });
-});
diff --git a/benchmarks/benchjs/population.js b/benchmarks/benchjs/population.js
deleted file mode 100644
index dc55ac1cae9..00000000000
--- a/benchmarks/benchjs/population.js
+++ /dev/null
@@ -1,410 +0,0 @@
-'use strict';
-const mongoose = require('../../lib');
-const Benchmark = require('benchmark');
-
-const suite = new Benchmark.Suite();
-
-const Schema = mongoose.Schema;
-const ObjectId = Schema.Types.ObjectId;
-const utils = require('../../lib/utils.js');
-
-// to make things work in the way the are normally described online...
-/*
- *global.Schema = Schema;
- *global.mongoose = mongoose;
- */
-
-/**
- * These are all the benchmark tests for population ops
- */
-
-mongoose.connect('mongodb://127.0.0.1/mongoose-bench', function (err) {
- if (err) {
- throw err;
- }
-
- const commentSchema = new Schema();
- commentSchema.add({
- title: String,
- date: Date,
- body: String,
- });
- const dummy1Schema = new Schema({
- title: String,
- isThisTest: Boolean,
- });
- const dummy2Schema = new Schema({
- title: String,
- isThisTest: Boolean,
- });
- const dummy3Schema = new Schema({
- title: String,
- isThisTest: Boolean,
- });
- const dummy4Schema = new Schema({
- title: String,
- isThisTest: Boolean,
- });
- const dummy5Schema = new Schema({
- title: String,
- isThisTest: Boolean,
- });
- const dummy6Schema = new Schema({
- title: String,
- isThisTest: Boolean,
- });
- const dummy7Schema = new Schema({
- title: String,
- isThisTest: Boolean,
- });
- const dummy8Schema = new Schema({
- title: String,
- isThisTest: Boolean,
- });
- const dummy9Schema = new Schema({
- title: String,
- isThisTest: Boolean,
- });
-
- let BlogPost = new Schema({
- title: String,
- author: String,
- slug: String,
- date: Date,
- meta: {
- date: Date,
- visitors: Number,
- },
- published: Boolean,
- mixed: {},
- numbers: [Number],
- tags: [String],
- owners: [ObjectId],
- comments: [{ type: ObjectId, ref: 'Comment' }],
- dummy1: [{ type: ObjectId, ref: 'Dummy1' }],
- dummy2: [{ type: ObjectId, ref: 'Dummy2' }],
- dummy3: [{ type: ObjectId, ref: 'Dummy3' }],
- dummy4: [{ type: ObjectId, ref: 'Dummy4' }],
- dummy5: [{ type: ObjectId, ref: 'Dummy5' }],
- dummy6: [{ type: ObjectId, ref: 'Dummy6' }],
- dummy7: [{ type: ObjectId, ref: 'Dummy7' }],
- dummy8: [{ type: ObjectId, ref: 'Dummy8' }],
- dummy9: [{ type: ObjectId, ref: 'Dummy9' }],
- def: { type: String, default: 'kandinsky' },
- });
-
- const blogData = {
- title: 'dummy post',
- author: 'somebody',
- slug: 'test.post',
- date: new Date(),
- meta: { date: new Date(), visitors: 9001 },
- published: true,
- mixed: { thisIsRandom: true },
- numbers: [1, 2, 7, 10, 23432],
- tags: ['test', 'BENCH', 'things', 'more things'],
- def: 'THANGS!!!',
- comments: [],
- dummy1: [],
- dummy2: [],
- dummy3: [],
- dummy4: [],
- dummy5: [],
- dummy6: [],
- dummy7: [],
- dummy8: [],
- dummy9: [],
- };
- const commentData = {
- title: 'test comment',
- date: new Date(),
- body: 'this be some crazzzyyyyy text that would go in a comment',
- };
- const dummyData = {
- title: 'dummy data~',
- isThisTest: true,
- };
- const Comments = mongoose.model('Comment', commentSchema);
- BlogPost = mongoose.model('BlogPost', BlogPost);
- const Dummy1 = mongoose.model('Dummy1', dummy1Schema);
- const Dummy2 = mongoose.model('Dummy2', dummy2Schema);
- const Dummy3 = mongoose.model('Dummy3', dummy3Schema);
- const Dummy4 = mongoose.model('Dummy4', dummy4Schema);
- const Dummy5 = mongoose.model('Dummy5', dummy5Schema);
- const Dummy6 = mongoose.model('Dummy6', dummy6Schema);
- const Dummy7 = mongoose.model('Dummy7', dummy7Schema);
- const Dummy8 = mongoose.model('Dummy8', dummy8Schema);
- const Dummy9 = mongoose.model('Dummy9', dummy9Schema);
- const cIds = [];
- const dIds = [];
- for (let i = 0; i < 9; i++) {
- dIds.push([]);
- }
-
- let cn = 5000;
- for (let i = 0; i < 500; i++) {
- Comments.create(commentData, function (err, com) {
- cIds.push(com.id);
- --cn || cont();
- });
- Dummy1.create(dummyData, function (err, d) {
- if (err) {
- throw err;
- }
- dIds[0].push(d.id);
- --cn || cont();
- });
- Dummy2.create(dummyData, function (err, d) {
- if (err) {
- throw err;
- }
- dIds[1].push(d.id);
- --cn || cont();
- });
- Dummy3.create(dummyData, function (err, d) {
- if (err) {
- throw err;
- }
- dIds[2].push(d.id);
- --cn || cont();
- });
- Dummy4.create(dummyData, function (err, d) {
- if (err) {
- throw err;
- }
- dIds[3].push(d.id);
- --cn || cont();
- });
- Dummy5.create(dummyData, function (err, d) {
- if (err) {
- throw err;
- }
- dIds[4].push(d.id);
- --cn || cont();
- });
- Dummy6.create(dummyData, function (err, d) {
- if (err) {
- throw err;
- }
- dIds[5].push(d.id);
- --cn || cont();
- });
- Dummy7.create(dummyData, function (err, d) {
- if (err) {
- throw err;
- }
- dIds[6].push(d.id);
- --cn || cont();
- });
- Dummy8.create(dummyData, function (err, d) {
- if (err) {
- throw err;
- }
- dIds[7].push(d.id);
- --cn || cont();
- });
- Dummy9.create(dummyData, function (err, d) {
- if (err) {
- throw err;
- }
- dIds[8].push(d.id);
- --cn || cont();
- });
- }
-
- const blog = [];
-
- function cont() {
- blog[0] = utils.clone(blogData);
- blog[1] = utils.clone(blogData);
- blog[2] = utils.clone(blogData);
- blog[3] = utils.clone(blogData);
- blogData.comments.push(getNextcId());
- blog[4] = blogData;
-
- blog[5] = utils.clone(blogData);
- blog[6] = utils.clone(blogData);
-
- for (let i = 0; i < 10; i++) {
- blog[0].comments.push(getNextcId());
- }
- for (let i = 0; i < 100; i++) {
- blog[1].comments.push(getNextcId());
- }
- for (let i = 0; i < 1000; i++) {
- blog[2].comments.push(getNextcId());
- }
- for (let i = 0; i < 10000; i++) {
- blog[3].comments.push(getNextcId());
- }
- for (let i = 0; i < 100; i++) {
- blog[5].comments.push(getNextcId());
- blog[6].comments.push(getNextcId());
-
- blog[5].dummy1.push(getNextdId(0));
- blog[5].dummy2.push(getNextdId(1));
- blog[5].dummy3.push(getNextdId(2));
- blog[5].dummy4.push(getNextdId(3));
-
- blog[6].dummy1.push(getNextdId(0));
- blog[6].dummy2.push(getNextdId(1));
- blog[6].dummy3.push(getNextdId(2));
- blog[6].dummy4.push(getNextdId(3));
- blog[6].dummy5.push(getNextdId(4));
- blog[6].dummy1.push(getNextdId(5));
- blog[6].dummy2.push(getNextdId(6));
- blog[6].dummy3.push(getNextdId(7));
- blog[6].dummy4.push(getNextdId(8));
- }
-
- let count = 7;
-
- function iter(c) {
- BlogPost.create(blog[c], function (err, bl) {
- if (err) {
- throw err;
- }
- blog[c] = bl;
- --count || next();
- });
- }
-
- // insert all of the data here
- for (let i = 0; i < blog.length; i++) {
- // use some closure magic to make sure we retain the index
- iter(i);
- }
- }
-
- let ci = 0;
- const di = [];
- for (let i = 0; i < 9; i++) {
- di.push(0);
- }
-
- function getNextcId() {
- ci = ++ci % cIds.length;
- return cIds[ci];
- }
-
- function getNextdId(i) {
- di[i] = ++di[i] % dIds[i].length;
- return dIds[i][di[i]];
- }
-
- function closeDB() {
- // just a bit simpler...
- mongoose.connection.db.dropDatabase(function () {
- mongoose.disconnect();
- process.exit();
- });
- }
-
- suite
- .add('Populate - 1 value', {
- defer: true,
- fn: function (deferred) {
- blog[4].populate('comments', function (err) {
- if (err) {
- throw err;
- }
- deferred.resolve();
- });
- },
- })
- .add('Populate - 10 values', {
- defer: true,
- fn: function (deferred) {
- blog[0].populate('comments', function (err) {
- if (err) {
- throw err;
- }
- deferred.resolve();
- });
- },
- })
- .add('Populate - 100 values', {
- defer: true,
- fn: function (deferred) {
- blog[1].populate('comments', function (err) {
- if (err) {
- throw err;
- }
- deferred.resolve();
- });
- },
- })
- .add('Populate - 1000 values', {
- defer: true,
- fn: function (deferred) {
- blog[2].populate('comments', function (err) {
- if (err) {
- throw err;
- }
- deferred.resolve();
- });
- },
- })
- .add('Populate - 10000 values', {
- defer: true,
- fn: function (deferred) {
- blog[3].populate('comments', function (err) {
- if (err) {
- throw err;
- }
- deferred.resolve();
- });
- },
- })
- .add('Populate - 5 properties', {
- defer: true,
- fn: function (deferred) {
- blog[5].populate(
- 'comments dummy1 dummy2 dummy3 dummy4',
- function (err) {
- if (err) {
- throw err;
- }
- deferred.resolve();
- }
- );
- },
- })
- .add('Populate - 10 properties', {
- defer: true,
- fn: function (deferred) {
- blog[6].populate(
- 'comments dummy1 dummy2 dummy3 dummy4 dummy5 dummy6 dummy7 dummy8 dummy9',
- function (err) {
- if (err) {
- throw err;
- }
- deferred.resolve();
- }
- );
- },
- })
-
- .on('cycle', function (evt) {
- if (process.env.MONGOOSE_DEV || process.env.PULL_REQUEST) {
- console.log(String(evt.target));
- }
- })
- .on('complete', function () {
- closeDB();
- if (!process.env.MONGOOSE_DEV && !process.env.PULL_REQUEST) {
- const outObj = {};
- this.forEach(function (item) {
- const out = {};
- out.stats = item.stats;
- delete out.stats.sample;
- out.ops = item.hz;
- outObj[item.name.replace(/\s/g, '')] = out;
- });
- console.dir(outObj, { depth: null, colors: true });
- }
- });
- function next() {
- suite.run({ async: true });
- }
-});
diff --git a/benchmarks/benchjs/read.js b/benchmarks/benchjs/read.js
deleted file mode 100644
index 35d39b90916..00000000000
--- a/benchmarks/benchjs/read.js
+++ /dev/null
@@ -1,319 +0,0 @@
-'use strict';
-
-const mongoose = require('../../lib');
-const Benchmark = require('benchmark');
-
-const suite = new Benchmark.Suite();
-
-const Schema = mongoose.Schema;
-const ObjectId = Schema.Types.ObjectId;
-const mongoClient = require('mongodb').MongoClient;
-const utils = require('../../lib/utils.js');
-
-// to make things work in the way the are normally described online...
-/*
- *global.Schema = Schema;
- *global.mongoose = mongoose;
- */
-
-/**
- * These are all the benchmark tests for reading data
- */
-
-mongoose.connect('mongodb://127.0.0.1/mongoose-bench', function (err) {
- if (err) {
- throw err;
- }
- mongoClient.connect('mongodb://127.0.0.1', function (err, client) {
- if (err) {
- throw err;
- }
-
- const db = client.db('mongoose-bench');
-
- const Comments = new Schema();
- Comments.add({
- title: String,
- date: Date,
- body: String,
- comments: [Comments],
- });
-
- let BlogPost = new Schema({
- title: String,
- author: String,
- slug: String,
- date: Date,
- meta: {
- date: Date,
- visitors: Number,
- },
- published: Boolean,
- mixed: {},
- numbers: [Number],
- tags: [String],
- owners: [ObjectId],
- comments: [Comments],
- def: {
- type: String,
- default: 'kandinsky',
- },
- });
-
- const blogData = {
- title: 'dummy post',
- author: 'somebody',
- slug: 'test.post',
- date: new Date(),
- meta: { date: new Date(), visitors: 9001 },
- published: true,
- mixed: { thisIsRandom: true },
- numbers: [1, 2, 7, 10, 23432],
- tags: ['test', 'BENCH', 'things', 'more things'],
- def: 'THANGS!!!',
- comments: [],
- };
- const commentData = {
- title: 'test comment',
- date: new Date(),
- body: 'this be some crazzzyyyyy text that would go in a comment',
- comments: [{ title: 'second level', date: new Date(), body: 'texttt' }],
- };
- for (let i = 0; i < 5; i++) {
- blogData.comments.push(commentData);
- }
- const UserSchema = new Schema({
- name: String,
- age: Number,
- likes: [String],
- address: String,
- });
-
- const User = mongoose.model('User', UserSchema);
- BlogPost = mongoose.model('BlogPost', BlogPost);
- const user = db.collection('user');
- const blogpost = db.collection('blogpost');
-
- const mIds = [];
- const dIds = [];
-
- const bmIds = [];
- const bdIds = [];
-
- const data = {
- name: 'name',
- age: 0,
- likes: ['dogs', 'cats', 'pizza'],
- address: ' Nowhere-ville USA',
- };
-
- // insert all of the data here
- let count = 4000;
- for (let i = 0; i < 1000; i++) {
- data.age = Math.floor(Math.random() * 50);
- User.create(data, function (err, u) {
- if (err) {
- throw err;
- }
- mIds.push(u.id);
- --count || next();
- });
- const nData = utils.clone(data);
- user.insertOne(nData, function (err, res) {
- if (err) {
- throw err;
- }
- dIds.push(res.insertedId);
- --count || next();
- });
- BlogPost.create(blogData, function (err, bp) {
- if (err) {
- throw err;
- }
- bmIds.push(bp.id);
- --count || next();
- });
-
- const bpData = utils.clone(blogData);
- blogpost.insertOne(bpData, function (err, res) {
- if (err) {
- throw err;
- }
- bdIds.push(res.insertedId);
- --count || next();
- });
- }
-
- let mi = 0,
- di = 0,
- bmi = 0,
- bdi = 0;
-
- function getNextmId() {
- mi = ++mi % mIds.length;
- return mIds[mi];
- }
-
- function getNextdId() {
- di = ++di % dIds.length;
- return dIds[di];
- }
-
- function getNextbmId() {
- bmi = ++bmi % bmIds.length;
- return bmIds[bmi];
- }
-
- function getNextbdId() {
- bdi = ++bdi % bdIds.length;
- return bdIds[bdi];
- }
-
- function closeDB() {
- mongoose.connection.db.dropDatabase(function () {
- mongoose.disconnect();
- process.exit();
- });
- }
-
- suite
- .add('Read - Mongoose - Basic', {
- defer: true,
- fn: function (deferred) {
- User.findOne({ _id: getNextmId() }, function (err) {
- if (err) {
- throw err;
- }
- deferred.resolve();
- });
- },
- })
- .add('Read - Driver - Basic', {
- defer: true,
- fn: function (deferred) {
- user.findOne({ _id: getNextdId() }, function (err) {
- if (err) {
- throw err;
- }
- deferred.resolve();
- });
- },
- })
- .add('Read - Mongoose - With lean', {
- defer: true,
- fn: function (deferred) {
- User.findOne(
- { _id: getNextmId() },
- {},
- { lean: true },
- function (err) {
- if (err) {
- throw err;
- }
- deferred.resolve();
- }
- );
- },
- })
- .add('Read - Mongoose - Multiple Items', {
- defer: true,
- fn: function (deferred) {
- const ids = [];
- for (let i = 0; i < 25; i++) {
- ids.push(getNextmId());
- }
- User.find({ _id: { $in: ids } }, function (err) {
- if (err) {
- throw err;
- }
- deferred.resolve();
- });
- },
- })
- .add('Read - Driver - Multiple Items', {
- defer: true,
- fn: function (deferred) {
- const ids = [];
- for (let i = 0; i < 25; i++) {
- ids.push(getNextdId());
- }
- user.find({ _id: { $in: ids } }).toArray(function (err) {
- if (err) {
- throw err;
- }
- deferred.resolve();
- });
- },
- })
- .add('Read - Mongoose - Non-index', {
- defer: true,
- fn: function (deferred) {
- const age = Math.floor(Math.random() * 50);
-
- User.find({ age: age }, function (err) {
- if (err) {
- throw err;
- }
- deferred.resolve();
- });
- },
- })
- .add('Read - Driver - Non-index', {
- defer: true,
- fn: function (deferred) {
- const age = Math.floor(Math.random() * 50);
-
- user.find({ age: age }).toArray(function (err) {
- if (err) {
- throw err;
- }
- deferred.resolve();
- });
- },
- })
- .add('Read - Mongoose - Embedded Docs', {
- defer: true,
- fn: function (deferred) {
- BlogPost.find({ _id: getNextbmId() }, function (err) {
- if (err) {
- throw err;
- }
- deferred.resolve();
- });
- },
- })
- .add('Read - Driver - Embedded Docs', {
- defer: true,
- fn: function (deferred) {
- blogpost.find({ _id: getNextbdId() }).toArray(function (err) {
- if (err) {
- throw err;
- }
- deferred.resolve();
- });
- },
- })
- .on('cycle', function (evt) {
- if (process.env.MONGOOSE_DEV || process.env.PULL_REQUEST) {
- console.log(String(evt.target));
- }
- })
- .on('complete', function () {
- closeDB();
- if (!process.env.MONGOOSE_DEV && !process.env.PULL_REQUEST) {
- const outObj = {};
- this.forEach(function (item) {
- const out = {};
- out.stats = item.stats;
- delete out.stats.sample;
- out.ops = item.hz;
- outObj[item.name.replace(/\s/g, '')] = out;
- });
- console.dir(outObj, { depth: null, colors: true });
- }
- });
- function next() {
- suite.run({ async: true });
- }
- });
-});
diff --git a/benchmarks/benchjs/update.js b/benchmarks/benchjs/update.js
deleted file mode 100644
index 4447339cf65..00000000000
--- a/benchmarks/benchjs/update.js
+++ /dev/null
@@ -1,401 +0,0 @@
-'use strict';
-
-const mongoose = require('../../lib');
-const Benchmark = require('benchmark');
-
-const suite = new Benchmark.Suite();
-
-const Schema = mongoose.Schema;
-const mongoClient = require('mongodb').MongoClient;
-const ObjectId = Schema.Types.ObjectId;
-const utils = require('../../lib/utils.js');
-
-// to make things work in the way the are normally described online...
-/*
- *global.Schema = Schema;
- *global.mongoose = mongoose;
- */
-
-/**
- * These are all the benchmark tests for updating data
- */
-
-mongoose.connect('mongodb://127.0.0.1/mongoose-bench', function (err) {
- if (err) {
- throw err;
- }
- mongoClient.connect('mongodb://127.0.0.1', function (err, client) {
- if (err) {
- throw err;
- }
-
- const db = client.db('mongoose-bench');
-
- const Comments = new Schema();
- Comments.add({
- title: String,
- date: Date,
- body: String,
- comments: [Comments],
- });
-
- let BlogPost = new Schema({
- title: String,
- author: String,
- slug: String,
- date: Date,
- meta: {
- date: Date,
- visitors: Number,
- },
- published: Boolean,
- mixed: {},
- numbers: [Number],
- tags: [String],
- owners: [ObjectId],
- comments: [Comments],
- def: { type: String, default: 'kandinsky' },
- });
-
- const blogData = {
- title: 'dummy post',
- author: 'somebody',
- slug: 'test.post',
- date: new Date(),
- meta: { date: new Date(), visitors: 9001 },
- published: true,
- mixed: { thisIsRandom: true },
- numbers: [1, 2, 7, 10, 23432],
- tags: ['test', 'BENCH', 'things', 'more things'],
- def: 'THANGS!!!',
- comments: [],
- };
-
- const commentData = {
- title: 'test comment',
- date: new Date(),
- body: 'this be some crazzzyyyyy text that would go in a comment',
- comments: [{ title: 'second level', date: new Date(), body: 'texttt' }],
- };
-
- for (let i = 0; i < 5; i++) {
- blogData.comments.push(commentData);
- }
-
- const UserSchema = new Schema({
- name: String,
- age: Number,
- likes: [String],
- address: String,
- });
-
- const User = mongoose.model('User', UserSchema);
- BlogPost = mongoose.model('BlogPost', BlogPost);
- const user = db.collection('user');
- const blogpost = db.collection('blogpost');
-
- const mIds = [];
- const dIds = [];
-
- const bmIds = [];
- const bdIds = [];
-
- const data = {
- name: 'name',
- age: 0,
- likes: ['dogs', 'cats', 'pizza'],
- address: ' Nowhere-ville USA',
- };
-
- // this is for some of the update tests below
- let testBp;
- // insert all of the data here
- let count = 4000;
- for (let i = 0; i < 1000; i++) {
- User.create(data, function (err, u) {
- if (err) {
- throw err;
- }
- mIds.push(u.id);
- --count || next();
- });
- const nData = utils.clone(data);
- user.insertOne(nData, function (err, res) {
- dIds.push(res.insertedId);
- --count || next();
- });
- BlogPost.create(blogData, function (err, bp) {
- if (err) {
- throw err;
- }
- bmIds.push(bp.id);
- testBp = bp;
- --count || next();
- });
-
- const bpData = utils.clone(blogData);
- blogpost.insertOne(bpData, function (err, res) {
- if (err) {
- throw err;
- }
- bdIds.push(res.insertedId);
- --count || next();
- });
- }
-
- let mi = 0,
- di = 0,
- bmi = 0,
- bdi = 0;
-
- function getNextmId() {
- mi = ++mi % mIds.length;
- return mIds[mi];
- }
-
- function getNextdId() {
- di = ++di % dIds.length;
- return dIds[di];
- }
-
- function getNextbmId() {
- bmi = ++bmi % bmIds.length;
- return bmIds[bmi];
- }
-
- function getNextbdId() {
- bdi = ++bdi % bdIds.length;
- return bdIds[bdi];
- }
-
- function closeDB() {
- mongoose.connection.db.dropDatabase(function () {
- mongoose.disconnect();
- process.exit();
- });
- }
-
- suite
- .add('Update - Mongoose - Basic', {
- defer: true,
- fn: function (deferred) {
- User.update(
- { _id: getNextmId() },
- { $set: { age: 2 }, $push: { likes: 'metal' } },
- function (err) {
- if (err) {
- throw err;
- }
- deferred.resolve();
- }
- );
- },
- })
- .add('Update - Driver - Basic', {
- defer: true,
- fn: function (deferred) {
- user.updateOne(
- { _id: getNextdId() },
- { $set: { age: 2 }, $push: { likes: 'metal' } },
- function (err) {
- if (err) {
- throw err;
- }
- deferred.resolve();
- }
- );
- },
- })
- .add('Update - Mongoose - Embedded Docs', {
- defer: true,
- fn: function (deferred) {
- BlogPost.findOne({ _id: getNextbmId() }, function (err, bp) {
- if (err) {
- throw err;
- }
- bp.comments[3].title = 'this is a new title';
- bp.comments[0].date = new Date();
- bp.comments.push(commentData);
- // save in Mongoose behaves differently than it does in the driver.
- // The driver will send the full document, while mongoose will check
- // and only update fields that have been changed. This is meant to
- // illustrate that difference between the two
- bp.save(function (err) {
- if (err) {
- throw err;
- }
- deferred.resolve();
- });
- });
- },
- })
- .add('Update - Driver - Embdedded Docs', {
- defer: true,
- fn: function (deferred) {
- blogpost.findOne({ _id: getNextbdId() }, function (err, bp) {
- if (err) {
- throw err;
- }
- blogpost.updateOne(
- { _id: bp._id },
- {
- $set: {
- 'comments.3.title': 'this is a new title',
- 'comments.0.date': new Date(),
- },
- },
- function (err) {
- if (err) {
- throw err;
- }
- deferred.resolve();
- }
- );
- });
- },
- })
- .add('Update - Mongoose - Multiple Documents', {
- defer: true,
- fn: function (deferred) {
- const ids = [];
- for (let i = 0; i < 50; i++) {
- ids.push(getNextmId());
- }
- User.update(
- { _id: { $in: ids } },
- { $set: { age: 2 }, $push: { likes: 'metal' } },
- function (err) {
- if (err) {
- throw err;
- }
- deferred.resolve();
- }
- );
- },
- })
- .add('Update - Driver - Multiple Documents', {
- defer: true,
- fn: function (deferred) {
- const ids = [];
- for (let i = 0; i < 50; i++) {
- ids.push(getNextdId());
- }
- user.updateOne(
- { _id: { $in: ids } },
- { $set: { age: 2 }, $push: { likes: 'metal' } },
- function (err) {
- if (err) {
- throw err;
- }
- deferred.resolve();
- }
- );
- },
- })
- .add('Update - Mongoose - pop and push', {
- defer: true,
- fn: function (deferred) {
- testBp.comments.push(commentData);
- testBp.comments.$shift();
- testBp.save(function (err) {
- if (err) {
- throw err;
- }
- deferred.resolve();
- });
- },
- })
- .add('Update - Mongoose - Array Manipulation, parallel ops', {
- defer: true,
- fn: function (deferred) {
- let done = false;
- BlogPost.update(
- { _id: testBp.id },
- { $pop: { comments: -1 } },
- function (err) {
- if (err) {
- throw err;
- }
- done && deferred.resolve();
- done = true;
- }
- );
- BlogPost.update(
- { _id: testBp.id },
- { $push: { comments: commentData } },
- function (err) {
- if (err) {
- throw err;
- }
- done && deferred.resolve();
- done = true;
- }
- );
- },
- })
- .add('Update - Mongoose - findOneAndModify', {
- defer: true,
- fn: function (deferred) {
- BlogPost.findOneAndUpdate(
- { _id: getNextbmId() },
- { $set: { age: 2 }, $push: { likes: 'metal' } },
- function (err) {
- if (err) {
- throw err;
- }
- deferred.resolve();
- }
- );
- },
- })
- .add('Update - Mongoose - find and update, separate ops', {
- defer: true,
- fn: function (deferred) {
- BlogPost.findOne({ _id: getNextbmId() }, function (err, bp) {
- if (err) {
- throw err;
- }
- bp.update(
- { $set: { age: 2 }, $push: { likes: 'metal' } },
- function (err) {
- if (err) {
- throw err;
- }
- deferred.resolve();
- }
- );
- });
- },
- })
- .on('cycle', function (evt) {
- if (process.env.MONGOOSE_DEV || process.env.PULL_REQUEST) {
- console.log(String(evt.target));
- }
- })
- .on('complete', function () {
- closeDB();
- if (!process.env.MONGOOSE_DEV && !process.env.PULL_REQUEST) {
- const outObj = {};
- this.forEach(function (item) {
- const out = {};
- out.stats = item.stats;
- delete out.stats.sample;
- out.ops = item.hz;
- outObj[item.name.replace(/\s/g, '')] = out;
- });
- console.dir(outObj, { depth: null, colors: true });
- }
- });
- function next() {
- for (let i = 0; i < 100; i++) {
- testBp.comments.push(commentData);
- }
- testBp.save(function (err) {
- if (err) {
- throw err;
- }
- suite.run({ async: true });
- });
- }
- });
-});
diff --git a/benchmarks/bigboard.json b/benchmarks/bigboard.json
deleted file mode 100644
index 0c22211c9f5..00000000000
--- a/benchmarks/bigboard.json
+++ /dev/null
@@ -1,1909 +0,0 @@
-{
- "_id": "4d5ea62fd76aa1136000000c",
- "checklists": [{
- "_id": "4daee890aae47fe55305dd72",
- "checkItems": [{
- "_id": "4daee8a2aae47fe55305eabf",
- "pos": 32768,
- "type": "check",
- "name": "delete checklists"
- }, {
- "_id": "4daee8a6aae47fe55305eb4e",
- "pos": 49152,
- "type": "check",
- "name": "delete checklist items"
- }, {
- "_id": "4daeec4caae47fe5530730be",
- "pos": 65536,
- "type": "check",
- "name": "Link to card on action in activity feed"
- }, {
- "_id": "4daeeca6aae47fe5530748b8",
- "pos": 81920,
- "type": "check",
- "name": "Edit controls on new task input"
- }, {
- "_id": "4daeeda4aae47fe553080e92",
- "pos": 98304,
- "type": "check",
- "name": "Actions for checklist item add, change, delete, reorder, etc."
- }, {
- "_id": "4db06f77c9fd63357d0a44ed",
- "pos": 114688,
- "type": "check",
- "name": "Click 'Add checklist' -> focus on new task, let title be 'Checklist', but editable"
- }],
- "name": "Features"
- }, {
- "checkItems": [{
- "_id": "4db09065c9fd63357d118d0f",
- "pos": 0,
- "type": "check",
- "name": "Make it obvious that you can't do stuff to the board"
- }, {
- "_id": "4db0906fc9fd63357d118d75",
- "pos": 16384,
- "type": "check",
- "name": "/#browse/"
- }, {
- "_id": "4db0908ac9fd63357d118e54",
- "pos": 32768,
- "type": "check",
- "name": "Cut you off from further updates when you can't see the board any more"
- }],
- "_id": "4db09054c9fd63357d117284",
- "name": "Remaining"
- }, {
- "checkItems": [{
- "_id": "4db5b8a9697ba7cc023bc737",
- "pos": 0,
- "type": "check",
- "name": "You can check things off of a checklist [brett]"
- }, {
- "_id": "4db5b8bf697ba7cc023bc874",
- "pos": 16384,
- "type": "check",
- "name": "Moving lists [brett]"
- }, {
- "_id": "4db5b8fa697ba7cc023be27f",
- "pos": 32768,
- "type": "check",
- "name": "Assign members by dragging"
- }, {
- "_id": "4db5b8fc697ba7cc023be2bb",
- "pos": 49152,
- "type": "check",
- "name": "Removing members from cards via avatar pop over menu"
- }, {
- "_id": "4db5b909697ba7cc023be3b7",
- "pos": 65536,
- "type": "check",
- "name": "Remove members from board via sidebar"
- }, {
- "_id": "4db5b90e697ba7cc023be406",
- "pos": 81920,
- "type": "check",
- "name": "Close lists via list menu"
- }, {
- "_id": "4db5bae6697ba7cc023dea6a",
- "pos": 98304,
- "type": "check",
- "name": "Do everything via board menu"
- }, {
- "_id": "4db5cef7697ba7cc023f97f4",
- "pos": 114688,
- "type": "check",
- "name": "we lost list reopen"
- }],
- "_id": "4db43519697ba7cc0227e6d7",
- "name": "Places where it still looks like you can edit"
- }, {
- "checkItems": [{
- "_id": "4db747935413f9d20c0b5eaa",
- "pos": 0,
- "type": "check",
- "name": "Finish checklists v2"
- }, {
- "_id": "4db7479b5413f9d20c0b5eeb",
- "pos": 16384,
- "type": "check",
- "name": "Delete this checklist"
- }],
- "_id": "4db745f75413f9d20c0adb0d",
- "name": "Checklist"
- }, {
- "checkItems": [{
- "_id": "4db8384582ab23241000c157",
- "pos": 0,
- "type": "check",
- "name": "Links in comments causing dialog window width problems"
- }, {
- "_id": "4db8386782ab23241000cb40",
- "pos": 16384,
- "type": "check",
- "name": "'card operations' (grey dude, close) showing up on click"
- }, {
- "_id": "4db8388882ab23241000d0ab",
- "pos": 32768,
- "type": "check",
- "name": "scrolltop not being set correctly"
- }],
- "_id": "4db8383582ab232410009fc4",
- "name": "Checklist"
- }, {
- "checkItems": [{
- "_id": "4db852b24b06719a1005014c",
- "pos": 0,
- "type": "check",
- "name": "Cursor is pointer for list and board titles on public board"
- }, {
- "_id": "4db861334b06719a10076026",
- "pos": 16384,
- "type": "check",
- "name": "Looks like I can add checklist items to cards that already hace checklists"
- }, {
- "_id": "4db8615f4b06719a10077a73",
- "pos": 32768,
- "type": "check",
- "name": "Looks like I can edit checklist titles"
- }],
- "_id": "4db852a54b06719a1004f9b9",
- "name": "Checklist"
- }, {
- "checkItems": [{
- "_id": "4dbafb7c6cc6af2018042d9d",
- "pos": 16384,
- "type": "check",
- "name": "Link 'Boards' to member/boards from the member page"
- }, {
- "_id": "4dbafb836cc6af2018042e0c",
- "pos": 32768,
- "type": "check",
- "name": "Link to 'Boards' from the menu"
- }, {
- "_id": "4dbafb8f6cc6af2018042ee8",
- "pos": 49152,
- "type": "check",
- "name": "Remove the boards from the member detail page"
- }, {
- "_id": "4dbafbad6cc6af2018045d3c",
- "pos": 65536,
- "type": "check",
- "name": "Make server-side data provider data/board/list/mine/updates"
- }, {
- "_id": "4dbafc706cc6af2018049bd2",
- "pos": 98304,
- "type": "check",
- "name": "BoardStatsListView"
- }, {
- "_id": "4dbafc766cc6af2018049c3f",
- "pos": 114688,
- "type": "check",
- "name": "MemberBoardsView"
- }, {
- "_id": "4dbee92360f4e2b21d01a2ed",
- "pos": 131072,
- "type": "check",
- "name": "Merge views and templates for profile and board pages"
- }],
- "_id": "4dbafb686cc6af20180422b0",
- "name": "Checklist"
- }, {
- "checkItems": [{
- "_id": "4dc7e84ee92755263100e7d1",
- "pos": 16384,
- "type": "check",
- "name": "actions.js [brett]"
- }, {
- "_id": "4dc7e858e92755263100e87b",
- "pos": 32768,
- "type": "check",
- "name": "notifications.js [brett]"
- }],
- "_id": "4dc7e841e92755263100e045",
- "name": "Checklist"
- }, {
- "checkItems": [{
- "_id": "4dc7e8dfe927552631012742",
- "pos": 16384,
- "type": "check",
- "name": "Patch controller"
- }, {
- "_id": "4dc7e8ece9275526310127f7",
- "pos": 32768,
- "type": "check",
- "name": "Server side support for member URLs"
- }, {
- "_id": "4dc7e8f6e92755263101290f",
- "pos": 49152,
- "type": "check",
- "name": "Server side support for board URLs"
- }, {
- "_id": "4dc9379612f8cee93501af53",
- "pos": 65536,
- "type": "check",
- "name": "Backwards compatible hash version"
- }, {
- "_id": "4dc937bb12f8cee93501b18b",
- "pos": 98304,
- "type": "check",
- "name": "quickload for members"
- }, {
- "_id": "4dc93f5b12f8cee9350270af",
- "pos": 114688,
- "type": "check",
- "name": "backfill username"
- }, {
- "_id": "4dc951466a4b5c603601f461",
- "pos": 131072,
- "type": "check",
- "name": "enforce username select on account create"
- }, {
- "_id": "4dc9929ee236cc1e370c0cd3",
- "pos": 147456,
- "type": "check",
- "name": "card URLs"
- }],
- "_id": "4dc7e8cde927552631011f32",
- "name": "Checklist"
- }, {
- "checkItems": [{
- "_id": "4dc849a6ba75abdf3205d35a",
- "pos": 16384,
- "type": "check",
- "name": "Verify use of tokens in ajaxRpc"
- }],
- "_id": "4dc84984ba75abdf3205c95e",
- "name": "Checklist"
- }, {
- "checkItems": [{
- "_id": "4dcaa2125c09b74e3e03bbf7",
- "pos": 16384,
- "type": "check",
- "name": "Give another user co-ownership of a board"
- }, {
- "_id": "4dcaa2295c09b74e3e03bd96",
- "pos": 32768,
- "type": "check",
- "name": "Remove your own ownership if there is a remaining owner"
- }],
- "_id": "4dcaa2085c09b74e3e03b00f",
- "name": "Checklist"
- }, {
- "checkItems": [{
- "_id": "4dcc42565dcb84024615584d",
- "pos": 16384,
- "type": "check",
- "name": "schema"
- }, {
- "_id": "4dcc425b5dcb8402461558e8",
- "pos": 32768,
- "type": "check",
- "name": "action wiring"
- }, {
- "_id": "4dcc42695dcb840246155a72",
- "pos": 65536,
- "type": "check",
- "name": "immediate vote feedback"
- }, {
- "_id": "4dcc42875dcb840246156d68",
- "pos": 81920,
- "type": "check",
- "name": "action display"
- }, {
- "_id": "4dcd3c585dcb840246221dee",
- "pos": 98304,
- "type": "check",
- "name": "badges"
- }],
- "_id": "4dcc41e25dcb84024614df30",
- "name": "Checklist"
- }, {
- "_id": "4dcd6da85dcb84024629626c",
- "checkItems": [{
- "_id": "4dcd6dd45dcb8402462973a1",
- "pos": 49152,
- "type": "check",
- "name": "Readable URLs"
- }, {
- "_id": "4dcd6de25dcb84024629750d",
- "pos": 65536,
- "type": "check",
- "name": "PushState instead of /#"
- }, {
- "_id": "4dcd6df95dcb8402462976db",
- "pos": 81920,
- "type": "check",
- "name": "Semantic Markup (good use of p tags, link text, img alt attributes)"
- }, {
- "_id": "4dcd6dfc5dcb840246297783",
- "pos": 98304,
- "type": "check",
- "name": "Meta description"
- }, {
- "_id": "4dcd6e005dcb84024629783c",
- "pos": 114688,
- "type": "check",
- "name": "Descriptive Page Titles"
- }],
- "name": "Checklist"
- }, {
- "checkItems": [{
- "_id": "4dcd77c05dcb8402462d836d",
- "pos": 16384,
- "type": "check",
- "name": "Skinny badge"
- }, {
- "_id": "4dcd77c95dcb8402462d845d",
- "pos": 32768,
- "type": "check",
- "name": "Vote button state"
- }],
- "_id": "4dcd77a75dcb8402462d69ee",
- "name": "Design Touch Ups"
- }, {
- "checkItems": [{
- "_id": "4dcd96575dcb84024634b11e",
- "pos": 16384,
- "type": "check",
- "name": "Propagate new commenting members (the member, not the idMembersRemoved) as soon as they comment"
- }, {
- "_id": "4dcd96655dcb84024634b29a",
- "pos": 32768,
- "type": "check",
- "name": "Vote actions not showing up on cards"
- }],
- "_id": "4dcd96405dcb84024634a0f8",
- "name": "Checklist"
- }, {
- "checkItems": [{
- "_id": "4dd11ecd5dcb840246509361",
- "pos": 16384,
- "type": "check",
- "name": "delete attachments"
- }, {
- "_id": "4dd11ee45dcb84024650d68a",
- "pos": 32768,
- "type": "check",
- "name": "badge for attachments"
- }, {
- "_id": "4dd122bf5dcb84024651be40",
- "pos": 49152,
- "type": "check",
- "name": "icon for attachments badge"
- }, {
- "_id": "4dd1239b5dcb8402465202a6",
- "pos": 65536,
- "type": "check",
- "name": "view attachments inside trellis?"
- }, {
- "_id": "4dd12d995dcb840246560562",
- "pos": 81920,
- "type": "check",
- "name": "attach multiple files at once"
- }, {
- "_id": "4dd137315dcb84024659ea3c",
- "pos": 98304,
- "type": "check",
- "name": "instant update of board action list when attachment is added"
- }],
- "_id": "4dd11ec95dcb840246508972",
- "name": "Checklist"
- }, {
- "checkItems": [{
- "_id": "4dd138a85dcb8402465aad37",
- "pos": 16384,
- "type": "check",
- "name": "IE 9 choke on console.log"
- }, {
- "_id": "4dd138e65dcb8402465ad86d",
- "pos": 32768,
- "type": "check",
- "name": "Can add a due date from a public comment [brett]"
- }, {
- "_id": "4dd139025dcb8402465b4c99",
- "pos": 49152,
- "type": "check",
- "name": "public viewers can see \"add checklist item\""
- }, {
- "_id": "4dd1391a5dcb8402465b4eac",
- "pos": 65536,
- "type": "check",
- "name": "public viewers get JS error when trying to open member menu"
- }, {
- "_id": "4dd13d5e5dcb8402465efad0",
- "pos": 81920,
- "type": "check",
- "name": "don't use cursor:pointer on checklist items for public viewers"
- }, {
- "_id": "4dd144e35dcb840246630e4e",
- "pos": 98304,
- "type": "check",
- "name": "color vote badge when it includes a vote from you [daniel]"
- }, {
- "_id": "4dd146dd5dcb8402466410ae",
- "pos": 114688,
- "type": "check",
- "name": "Adding an attachment doesn't instantly give a card an attachment badge [brett]"
- }],
- "_id": "4dd1389c5dcb8402465a7bc9",
- "name": "Checklist"
- }, {
- "_id": "4dd139ce5dcb8402465bcbf9",
- "checkItems": [{
- "_id": "4dd139dd5dcb8402465c3cd5",
- "pos": 32768,
- "type": "check",
- "name": "Can't quick enter checklist items on all browsers"
- }, {
- "_id": "4dd139ed5dcb8402465c65e8",
- "pos": 49152,
- "type": "check",
- "name": "hide attachments upload button from iOS devices"
- }, {
- "_id": "4dd13a3c5dcb8402465d3cdc",
- "pos": 65536,
- "type": "check",
- "name": "Button active state \"sticks\" on iOS"
- }],
- "name": "Checklist"
- }, {
- "_id": "4dd163620bd1b83a5b0e7c00",
- "checkItems": [{
- "_id": "4dd1638b0bd1b83a5b0e8abe",
- "name": "Labels button and pop-over in card detail [bobby]",
- "pos": 32768,
- "type": "check"
- }, {
- "_id": "4dd163b10bd1b83a5b0e991f",
- "name": "Palette [bobby]",
- "pos": 49152,
- "type": "check"
- }, {
- "_id": "4dd28be8728b921a5d1fbb42",
- "pos": 65536,
- "type": "check",
- "name": "HTML/CSS"
- }],
- "name": "Design Checklist"
- }, {
- "_id": "4dd177f40bd1b83a5b1c57a9",
- "checkItems": [{
- "_id": "4dd178390bd1b83a5b1c6760",
- "pos": 16384,
- "type": "check",
- "name": "Spawn multiple processes: Cluster?"
- }, {
- "_id": "4dd178d60bd1b83a5b1cc066",
- "name": "PubSub: Redis",
- "pos": 32768,
- "type": "check"
- }, {
- "_id": "4dd69b0d7165bb5c7243f933",
- "name": "Heartbeat / Sessions",
- "pos": 49152,
- "type": "check"
- }, {
- "_id": "4dda546b19e692e0053d9be5",
- "pos": 65536,
- "type": "check",
- "name": "Go to a single minify step across all processes (maybe on release)"
- }, {
- "_id": "4ddac01e566cceb30f14e1c2",
- "pos": 81920,
- "type": "check",
- "name": "Redis on trellisny1"
- }, {
- "_id": "4ddd1ef993234c0814377408",
- "pos": 114688,
- "type": "check",
- "name": "ajax RPC consistency"
- }],
- "name": "Checklist"
- }, {
- "_id": "4dd28ae9728b921a5d1e1249",
- "checkItems": [{
- "_id": "4dd28af0728b921a5d1e2295",
- "pos": 16384,
- "type": "check",
- "name": "Way to clear due date"
- }, {
- "_id": "4dd28afb728b921a5d1e246b",
- "pos": 32768,
- "type": "check",
- "name": "Add due dates from card actions (pop over menu)"
- }, {
- "_id": "4dd28b15728b921a5d1e267a",
- "pos": 49152,
- "type": "check",
- "name": "Fuzzy dates"
- }],
- "name": "Checklist"
- }, {
- "_id": "4dd2da8d728b921a5d30c42b",
- "checkItems": [{
- "_id": "4dd2da93728b921a5d30d50e",
- "pos": 16384,
- "type": "check",
- "name": "Get a real cert"
- }, {
- "_id": "4dd2dab1728b921a5d30d6e3",
- "pos": 32768,
- "type": "check",
- "name": "Use an options parser instead of the current crazy long positional opts"
- }, {
- "_id": "4dd2dab6728b921a5d30d7d9",
- "pos": 49152,
- "type": "check",
- "name": "Test like crazy"
- }, {
- "_id": "4dd2dac4728b921a5d30d8fb",
- "pos": 65536,
- "type": "check",
- "name": "modify start.sh to use ssl"
- }, {
- "_id": "4dd2ddf6728b921a5d316f84",
- "pos": 81920,
- "type": "check",
- "name": "node 0.4.7 running on trellisny1"
- }, {
- "_id": "4dd50b2bb9a905d36627f88b",
- "pos": 98304,
- "type": "check",
- "name": "key needs to be readable by trellis user."
- }],
- "name": "Checklist"
- }, {
- "_id": "4dd44cdeb9a905d3661bc337",
- "checkItems": [{
- "_id": "4dd44ce3b9a905d3661bd52e",
- "pos": 16384,
- "type": "check",
- "name": "public/private"
- }, {
- "_id": "4dd44ce5b9a905d3661bd620",
- "pos": 32768,
- "type": "check",
- "name": "voting"
- }, {
- "_id": "4dd44ce7b9a905d3661bd714",
- "pos": 49152,
- "type": "check",
- "name": "public comments"
- }, {
- "_id": "4dd44d01b9a905d3661bd9d8",
- "pos": 81920,
- "type": "check",
- "name": "sidebar - members"
- }, {
- "_id": "4dd44d11b9a905d3661bdb11",
- "pos": 98304,
- "type": "check",
- "name": "sidebar - board actions"
- }, {
- "_id": "4dd44d17b9a905d3661bdc8f",
- "pos": 114688,
- "type": "check",
- "name": "sidebar - activity"
- }, {
- "_id": "4dd44d1bb9a905d3661bdd96",
- "pos": 131072,
- "type": "check",
- "name": "list guide"
- }],
- "name": "Checklist"
- }, {
- "_id": "4dd67bda7165bb5c723c2d49",
- "checkItems": [{
- "_id": "4dd67bf47165bb5c723c402c",
- "pos": 16384,
- "type": "check",
- "name": "Fix flashsockets in the SSL case - they get flaky, esp. with multiple browser windows open"
- }, {
- "_id": "4dd67c207165bb5c723c42d9",
- "pos": 32768,
- "type": "check",
- "name": "The flash policy server is turned off for multi-process trellis, because when every trellis tries to start it up there's a port conflict"
- }],
- "name": "Checklist"
- }, {
- "_id": "4ddeb510a9a2bd055c1337e0",
- "checkItems": [{
- "_id": "4ddeb525a9a2bd055c134a2e",
- "pos": 16384,
- "type": "check",
- "name": "cards"
- }, {
- "_id": "4ddeb528a9a2bd055c134b56",
- "pos": 32768,
- "type": "check",
- "name": "checklists"
- }, {
- "_id": "4ddeb529a9a2bd055c134c64",
- "pos": 49152,
- "type": "check",
- "name": "lists"
- }, {
- "_id": "4ddeb52ba9a2bd055c134d82",
- "pos": 65536,
- "type": "check",
- "name": "boards"
- }, {
- "_id": "4ddeb52da9a2bd055c134ea2",
- "pos": 81920,
- "type": "check",
- "name": "members"
- }, {
- "_id": "4ddeb53ba9a2bd055c13501e",
- "pos": 98304,
- "type": "check",
- "name": "board invites"
- }],
- "name": "Checklist"
- }, {
- "_id": "4ddeb83fa9a2bd055c14cfe5",
- "checkItems": [{
- "_id": "4ddeb853a9a2bd055c14e131",
- "name": "Refactor sessions.js",
- "pos": 16384,
- "type": "check"
- }, {
- "_id": "4ddf9ce8478463e761026e64",
- "pos": 49152,
- "type": "check",
- "name": "Fix notification race condition"
- }, {
- "_id": "4ddfa076478463e76103790e",
- "name": "Get (correct) board watcher count back",
- "pos": 65536,
- "type": "check"
- }, {
- "_id": "4de289955c292e601a00d8a2",
- "pos": 98304,
- "type": "check",
- "name": "node-openid stores associations per process - we need to fix that."
- }, {
- "_id": "4dffa580079f66be4b04396f",
- "pos": 114688,
- "type": "check",
- "name": "over-publishing redis notifications"
- }, {
- "_id": "4e01e5214a2956844303cbc3",
- "pos": 131072,
- "type": "check",
- "name": "/enabled.html to activate/deactivate server"
- }, {
- "_id": "4e0232408d64cbf783026e0a",
- "pos": 147456,
- "type": "check",
- "name": "restore us to multiprocess, then to multiserver"
- }],
- "name": "Checklist"
- }, {
- "_id": "4ddfbbcf478463e7610a3621",
- "checkItems": [{
- "_id": "4ddfbbd7478463e7610a41bd",
- "pos": 16384,
- "type": "check",
- "name": "schema.js: 790"
- }, {
- "_id": "4ddfbc40478463e7610a586e",
- "pos": 32768,
- "type": "check",
- "name": "account.js: 108"
- }],
- "name": "Checklist"
- }, {
- "_id": "4de009d99669af6c6902b730",
- "checkItems": [{
- "_id": "4de009fb9669af6c6902bd81",
- "pos": 16384,
- "type": "check",
- "name": "IE \"Socket not writable\" when trying to get ie.css on fresh browser login in IE9 in test"
- }, {
- "_id": "4de00a089669af6c6902bf49",
- "pos": 32768,
- "type": "check",
- "name": "IE login laps in production"
- }],
- "name": "Checklist"
- }, {
- "_id": "4de3f6ae5c292e601a12e70f",
- "checkItems": [{
- "_id": "4de3f6bc5c292e601a12f0d8",
- "pos": 16384,
- "type": "check",
- "name": "patch openid for async assoc storage"
- }, {
- "_id": "4de3f6cb5c292e601a12f21e",
- "pos": 32768,
- "type": "check",
- "name": "trellis change to use Redis to store assocs"
- }],
- "name": "Checklist"
- }, {
- "checkItems": [],
- "_id": "4de4e54df38ea2ed1f02d946",
- "name": "Checklist"
- }, {
- "_id": "4de53731c1955d252209ffc8",
- "checkItems": [{
- "_id": "4de5375cc1955d25220a17c7",
- "name": "account.js [Aaron]",
- "pos": 16384,
- "type": "check"
- }, {
- "_id": "4de5375ec1955d25220a1908",
- "name": "ajaxRpc.js [Aaron]",
- "pos": 32768,
- "type": "check"
- }, {
- "_id": "4de5376bc1955d25220a1ba9",
- "name": "packager.js (Ian)",
- "pos": 49152,
- "type": "check"
- }, {
- "_id": "4de53770c1955d25220a1cee",
- "name": "permissions.js (Ian)",
- "pos": 65536,
- "type": "check"
- }, {
- "_id": "4de53775c1955d25220a1e38",
- "name": "rpc.js (daniel)",
- "pos": 81920,
- "type": "check"
- }, {
- "_id": "4de5377ac1955d25220a1f84",
- "name": "schema.js (daniel)",
- "pos": 98304,
- "type": "check"
- }, {
- "_id": "4de5377fc1955d25220a20cf",
- "name": "sessions.js [Brett]",
- "pos": 114688,
- "type": "check"
- }, {
- "_id": "4de53783c1955d25220a22b4",
- "name": "sockets.js (Ian)",
- "pos": 131072,
- "type": "check"
- }],
- "name": "/server"
- }, {
- "_id": "4de5373ec1955d25220a0b89",
- "checkItems": [{
- "_id": "4de538f2c1955d25220a5985",
- "name": "actionfilter.js - Not used right now, don't worry about it.",
- "pos": 16384,
- "type": "check"
- }, {
- "_id": "4de538f5c1955d25220a5ad6",
- "name": "browser.js [Aaron]",
- "pos": 32768,
- "type": "check"
- }, {
- "_id": "4de538f7c1955d25220a5c26",
- "name": "dates.js - see daniel first [Aaron]",
- "pos": 49152,
- "type": "check"
- }, {
- "_id": "4de538fdc1955d25220a5f8b",
- "name": "search.js - Not used right now, don't worry about it.",
- "pos": 81920,
- "type": "check"
- }],
- "name": "/static/js/"
- }, {
- "_id": "4de53c77c1955d25220b2bad",
- "checkItems": [{
- "_id": "4de53c7ec1955d25220b3770",
- "name": "trellis.js [Aaron]",
- "pos": 16384,
- "type": "check"
- }],
- "name": "/"
- }, {
- "_id": "4de695be3ecfb79a26086cfa",
- "checkItems": [{
- "_id": "4de695d23ecfb79a260881e2",
- "name": "closureCompiler.js (Ian)",
- "pos": 16384,
- "type": "check"
- }, {
- "_id": "4de695da3ecfb79a26088348",
- "name": "bundle.js [brett]",
- "pos": 32768,
- "type": "check"
- }, {
- "_id": "4de695de3ecfb79a26088546",
- "name": "gzipResponse.js (brett)",
- "pos": 49152,
- "type": "check"
- }, {
- "_id": "4de695f53ecfb79a26088a5a",
- "name": "auth.js (Ian)",
- "pos": 65536,
- "type": "check"
- }, {
- "_id": "4de90b954cd033ab2d196a21",
- "pos": 81920,
- "type": "check",
- "name": "emailusers/emailusers.js (brett)"
- }],
- "name": "/modules"
- }, {
- "_id": "4de8f7474cd033ab2d1431cd",
- "checkItems": [{
- "_id": "4de8f7524cd033ab2d146f97",
- "pos": 16384,
- "type": "check",
- "name": "Org profile"
- }, {
- "_id": "4de8f7594cd033ab2d1475e4",
- "pos": 32768,
- "type": "check",
- "name": "Create org"
- }, {
- "_id": "4de8f7704cd033ab2d147d18",
- "pos": 49152,
- "type": "check",
- "name": "List of orgs in member profile"
- }, {
- "_id": "4de8f77c4cd033ab2d147f5b",
- "pos": 65536,
- "type": "check",
- "name": "List of org boards in member profile"
- }, {
- "_id": "4de8f7824cd033ab2d1480dd",
- "name": "Preferences: perms for features, visibility",
- "pos": 81920,
- "type": "check"
- }, {
- "_id": "4de8f7c44cd033ab2d14982f",
- "pos": 98304,
- "type": "check",
- "name": "New board: option to make part of org"
- }, {
- "_id": "4de8f8244cd033ab2d149f0c",
- "name": "Move/add board to org (in board preferences)",
- "pos": 114688,
- "type": "check"
- }, {
- "_id": "4de8f82a4cd033ab2d14a096",
- "name": "Manage org members page",
- "pos": 131072,
- "type": "check"
- }, {
- "_id": "4de94253d7b3339b2f11cf9b",
- "pos": 147456,
- "type": "check",
- "name": "Org account page"
- }, {
- "_id": "4df8ed9adc894c716d1ccc78",
- "pos": 163840,
- "type": "check",
- "name": "Enforce that orgs and members can't have the same name: first claimer wins"
- }, {
- "_id": "4e035aba8d64cbf7834194d3",
- "pos": 180224,
- "type": "check",
- "name": "'Add Board' from the org page"
- }, {
- "_id": "4e035aeb8d64cbf78341c4ff",
- "pos": 196608,
- "type": "check",
- "name": "Email template changes for org invite"
- }, {
- "_id": "4e035afa8d64cbf78341d835",
- "pos": 212992,
- "type": "check",
- "name": "Allow you to join Trellis if you're invited to an org"
- }, {
- "_id": "4e035b978d64cbf783420ab1",
- "pos": 229376,
- "type": "check",
- "name": "Make org boards show up in 'Boards' menu"
- }, {
- "_id": "4e035c888d64cbf783424ea0",
- "pos": 245760,
- "type": "check",
- "name": "Feels like an organization header on the boards page should take me to the page for the org"
- }, {
- "_id": "4e035d088d64cbf7834269e4",
- "pos": 262144,
- "type": "check",
- "name": "Consolidate joinBoard / joinOrganization templates"
- }, {
- "_id": "4e035f718d64cbf78342d8f3",
- "pos": 278528,
- "type": "check",
- "name": "With Ian, figure out how to merge board and org invitation mgmt more"
- }],
- "name": "UI"
- }, {
- "_id": "4df21c86f07016d15a01b030",
- "checkItems": [{
- "_id": "4df21c91f07016d15a01c8eb",
- "pos": 16384,
- "type": "check",
- "name": "Get trellis running on one FreeBSD server"
- }, {
- "_id": "4df21cadf07016d15a01d6c2",
- "pos": 32768,
- "type": "check",
- "name": "Get both servers running pointing to the same mongo and redis"
- }, {
- "_id": "4df21ccaf07016d15a01e471",
- "pos": 49152,
- "type": "check",
- "name": "Load balance"
- }, {
- "_id": "4df21cd7f07016d15a01f13c",
- "pos": 65536,
- "type": "check",
- "name": "Test new setup"
- }, {
- "_id": "4df21ce4f07016d15a020b6d",
- "name": "Flip DNS and adjust NAT [shawn] Tomorrow first thing.",
- "pos": 98304,
- "type": "check"
- }, {
- "_id": "4df25601839542cf5a106682",
- "pos": 114688,
- "type": "check",
- "name": "Build node_modules for BSD 8.2"
- }, {
- "_id": "4df60eeac204e79d5d2a3713",
- "pos": 131072,
- "type": "check",
- "name": "FreeBSD build machine"
- }, {
- "_id": "4df6332b8676f5a05d30cc80",
- "name": "Fix node.js FreeBSD link issue to get hiredis, forever, and native bson parser working again. http://www.freebsd.org/cgi/query-pr.cgi?pr=157875",
- "pos": 147456,
- "type": "check"
- }, {
- "_id": "4df7bd69e5c8fa736d0b4323",
- "pos": 163840,
- "type": "check",
- "name": "Build with kqueue"
- }, {
- "_id": "4df7c062e5c8fa736d0b8169",
- "pos": 180224,
- "type": "check",
- "name": "re-enable backup-on-push in the bsd branch once we have perms"
- }, {
- "_id": "4df8be3edc894c716d12102f",
- "name": "Issue pull request to ry for kqueue patch. https://github.com/joyent/node/pull/1186",
- "pos": 196608,
- "type": "check"
- }, {
- "_id": "4df8e529dc894c716d1a308c",
- "pos": 212992,
- "type": "check",
- "name": "sockets are disconnected after a few seconds when going through HAProxy[shawn]"
- }, {
- "_id": "4df90c7fdc894c716d21a98d",
- "name": "Allow trellisny3 and ny2 to connect to trellisny1's mongo and redis so that we can move servers without burning the ships. [shawn] http://our.fogbugz.com/default.asp?2076170",
- "pos": 229376,
- "type": "check"
- }, {
- "_id": "4df91169dc894c716d21eea7",
- "name": "Send keepalives over open sockets every 30 minutes, b/c HAProxy will time out idle connections after an hour.[brett]",
- "pos": 245760,
- "type": "check"
- }, {
- "_id": "4dfa3772ee196e34f901c9e3",
- "pos": 262144,
- "type": "check",
- "name": "When connected to trellis on the new servers: you do something and don't see it refected in the activity feed"
- }, {
- "_id": "4dfa676b959e5c5cfc055192",
- "pos": 278528,
- "type": "check",
- "name": "Cut over to redis on trellisny2"
- }],
- "name": "Checklist"
- }, {
- "_id": "4df69f0e8676f5a05d39da78",
- "checkItems": [{
- "_id": "4df6a02d8676f5a05d3a0f4f",
- "pos": 16384,
- "type": "check",
- "name": "Clicking any input on /login redirects to /authenticate, expected /confirmedNewMember for create account"
- }, {
- "_id": "4df6a0e08676f5a05d3a2169",
- "pos": 32768,
- "type": "check",
- "name": "Whitelisted, non-google email address (I tried yahoo) redirect to '/login?returnUrl=/authenticate' on create account."
- }, {
- "_id": "4df6a4508676f5a05d3a4b8e",
- "name": "Attempting to sign in with google goes to /verify, then immediately redirects to /login?returnUrl=/verify (broken on fresh db, worked on old db)",
- "pos": 49152,
- "type": "check"
- }],
- "name": "Bugz"
- }, {
- "_id": "4df8bc8ddc894c716d110817",
- "checkItems": [{
- "_id": "4df8bc99dc894c716d111ec9",
- "pos": 16384,
- "type": "check",
- "name": "Test env on trellis-build"
- }, {
- "_id": "4df8bcafdc894c716d112b66",
- "name": "scripts to start server and blank DB",
- "pos": 32768,
- "type": "check"
- }, {
- "_id": "4df8bcc2dc894c716d11373c",
- "pos": 49152,
- "type": "check",
- "name": "VPN"
- }, {
- "_id": "4df8bccedc894c716d1143ce",
- "pos": 65536,
- "type": "check",
- "name": "account on trellis-build"
- }, {
- "_id": "4df8bcd2dc894c716d114f8d",
- "pos": 81920,
- "type": "check",
- "name": "instructions"
- }, {
- "_id": "4df8c75adc894c716d143849",
- "pos": 98304,
- "type": "check",
- "name": "ourdot access"
- }, {
- "_id": "4dfb8ccae01b09000006e102",
- "pos": 114688,
- "type": "check",
- "name": "script to hg pull/up"
- }],
- "name": "Checklist"
- }, {
- "_id": "4dfb6db01c2ab8000003041f",
- "checkItems": [{
- "_id": "4dfb6dbb1c2ab80000031d46",
- "pos": 16384,
- "type": "check",
- "name": "Close board"
- }, {
- "_id": "4dfb6dbf1c2ab80000032b7b",
- "pos": 32768,
- "type": "check",
- "name": "change name"
- }, {
- "_id": "4dfb6dc31c2ab800000338e3",
- "pos": 49152,
- "type": "check",
- "name": "change preferences"
- }],
- "name": "Checklist"
- }, {
- "_id": "4dff859729bde2224901cc9e",
- "checkItems": [{
- "_id": "4dff85b229bde2224901eaaa",
- "pos": 16384,
- "type": "check",
- "name": "People are listed as 'active' who haven't logged in for days"
- }, {
- "_id": "4dff85c629bde2224901fa21",
- "pos": 32768,
- "type": "check",
- "name": "only 4 procs are showing up in liveprocs"
- }, {
- "_id": "4dff85f029bde22249020b47",
- "pos": 49152,
- "type": "check",
- "name": "status entries without a liveprocs entry are still not being deleted"
- }, {
- "_id": "4dff864b29bde222490226f5",
- "pos": 65536,
- "type": "check",
- "name": "however, we do not see any status entries with an incorrect status in Redis"
- }],
- "name": "Checklist"
- }, {
- "_id": "4dffa67d079f66be4b04bb5a",
- "checkItems": [{
- "_id": "4dffa688079f66be4b04da43",
- "pos": 16384,
- "type": "check",
- "name": "Better behavior when setting password for existing account"
- }, {
- "_id": "4dffa68f079f66be4b04ea2f",
- "pos": 32768,
- "type": "check",
- "name": "No duplicate-email accounts"
- }, {
- "_id": "4dffa69f079f66be4b04fa20",
- "pos": 49152,
- "type": "check",
- "name": "Feedback that password change occurred"
- }, {
- "_id": "4dffa6ad079f66be4b050ae8",
- "pos": 65536,
- "type": "check",
- "name": "Feedback that the username or password was incorrect on login"
- }, {
- "_id": "4e00c20c079f66bb4b0f1148",
- "pos": 81920,
- "type": "check",
- "name": "Fix styles for incorrect login message"
- }, {
- "_id": "4e00d4ef079f66bc4b154c82",
- "pos": 98304,
- "type": "check",
- "name": "email account confirmation"
- }, {
- "_id": "4e00dffb079f66be4b14d9a0",
- "pos": 114688,
- "type": "check",
- "name": "allow you to create a non-openid user with a password"
- }, {
- "_id": "4e00f93117baf6ee6603162f",
- "pos": 131072,
- "type": "check",
- "name": "forgot password page"
- }, {
- "_id": "4e09f16685ad212f9adcc3c3",
- "pos": 147456,
- "type": "check",
- "name": "Allow account creation for any email with invite code"
- }, {
- "_id": "4e0cc02737796f0000214c74",
- "pos": 163840,
- "type": "check",
- "name": "Dial with CR comment from http://our.fogbugz.com/kiln/Review/2075267"
- }, {
- "_id": "4e14beb29df22bbd48037a16",
- "name": "No account confirmation [deferred for now]",
- "pos": 180224,
- "type": "check"
- }],
- "name": "Checklist"
- }, {
- "_id": "4e0dd9d6ca68f50000034cc7",
- "checkItems": [{
- "_id": "4e0dd9dcca68f50000036f68",
- "pos": 16384,
- "type": "check",
- "name": "Can't reopen closed boards"
- }, {
- "_id": "4e0dd9f4ca68f50000039479",
- "pos": 49152,
- "type": "check",
- "name": "When you DO reopen a closed board from a different page, it's blank until you reload"
- }, {
- "_id": "4e0dd9fcca68f5000003a6cd",
- "pos": 65536,
- "type": "check",
- "name": "Reopening a board should just take you to the board"
- }],
- "name": "Checklist"
- }, {
- "checkItems": [],
- "_id": "4e1457780855bfafa8020b9d",
- "name": "Checklist"
- }, {
- "_id": "4e1457790855bfafa8022037",
- "checkItems": [{
- "_id": "4e1457ad0855bfafa802c424",
- "pos": 16384,
- "type": "check",
- "name": "Board Preferences -> Better alignment/spacing on add to org"
- }, {
- "_id": "4e14582e0855bfafa8033196",
- "name": "Some way to get to the organization's page from the board (for org members)",
- "pos": 32768,
- "type": "check"
- }, {
- "_id": "4e146ba79b22b2b94800e2cb",
- "name": "Make org name show up in 'Boards' menu",
- "pos": 49152,
- "type": "check"
- }, {
- "_id": "4e146bbc8e237d0eaa00fa9c",
- "pos": 65536,
- "type": "check",
- "name": "With Ian, figure out how to merge board and org invitation mgmt more"
- }, {
- "_id": "4e146bc6c3dce0b84801a9cd",
- "pos": 81920,
- "type": "check",
- "name": "Member status changes update immediately"
- }, {
- "_id": "4e147dda9b22b2ba480955f2",
- "pos": 98304,
- "type": "check",
- "name": "Admin page: org stats"
- }, {
- "_id": "4e14a160018bc307aa072caa",
- "pos": 114688,
- "type": "check",
- "name": "Only allow org admins to see :org/members and :org/account"
- }, {
- "_id": "4e14b508b809c7dbab083e73",
- "pos": 131072,
- "type": "check",
- "name": "Public view of org (org and private boards hidden to non-orgers, members are shown)"
- }],
- "name": "Checklist"
- }, {
- "checkItems": [],
- "_id": "4e17350dec9833696c04aa79",
- "name": "Checklist"
- }, {
- "_id": "4e17350fec9833696c04c94a",
- "checkItems": [{
- "_id": "4e173527ec9833696c0524f7",
- "pos": 16384,
- "type": "check",
- "name": "Make sure non-socket-connected browsers catch the upgrade"
- }, {
- "_id": "4e17352dec9833696c053a3b",
- "pos": 32768,
- "type": "check",
- "name": "reconnect gracefully"
- }, {
- "_id": "4e17353bec9833696c054f8a",
- "pos": 49152,
- "type": "check",
- "name": "detect version bump and gently ask the user to click to upgrade"
- }, {
- "_id": "4e28af81ce64d1930a014150",
- "pos": 83360,
- "type": "check",
- "name": "Sync subscribed models on socket-connected browsers"
- }],
- "name": "Checklist"
- }, {
- "_id": "4e1b53e4274d60b9dd06b281",
- "checkItems": [{
- "_id": "4e1b53f5274d60b9dd071ada",
- "pos": 16384,
- "type": "check",
- "name": "Notifications"
- }, {
- "_id": "4e1b53fc274d60b9dd072d6b",
- "pos": 32768,
- "type": "check",
- "name": "Invitation"
- }, {
- "_id": "4e1b5404274d60b9dd073ff1",
- "pos": 49152,
- "type": "check",
- "name": "Org Permission Level"
- }, {
- "_id": "4e1b5407274d60b9dd07526c",
- "pos": 65536,
- "type": "check",
- "name": "Archive"
- }, {
- "_id": "4e1bce84a5a5a4000003c761",
- "pos": 81920,
- "type": "check",
- "name": "Search"
- }, {
- "_id": "4e1bd069a5a5a4000003fcab",
- "pos": 98304,
- "type": "check",
- "name": "Label (large)"
- }],
- "name": "Icons"
- }, {
- "checkItems": [],
- "_id": "4e1b53e5274d60b9dd06cd0d",
- "name": "Checklist"
- }, {
- "_id": "4e1c52928d677cfe8300697d",
- "checkItems": [{
- "_id": "4e1c52ad8d677cfe83008e73",
- "pos": 16384,
- "type": "check",
- "name": "Still have to keep dragging the card to keep it scrolling -- holding it over the bottom or top of the list doesn't scroll"
- }, {
- "_id": "4e1c52bf8d677cfe8300a1d4",
- "pos": 32768,
- "type": "check",
- "name": "Single-col still doesn't scroll"
- }, {
- "_id": "4e1dcb277b2050df84026134",
- "pos": 50033,
- "type": "check",
- "name": "ie doesn't scroll"
- }],
- "name": "Checklist"
- }, {
- "_id": "4e1c959c9a872d028402dbb8",
- "checkItems": [{
- "_id": "4e1c95ac9a872d0284030358",
- "pos": 16384,
- "type": "check",
- "name": "the \"Archive\" button in the sidebar looks like it might archive the board"
- }, {
- "_id": "4e1c95b49a872d028403183e",
- "pos": 32768,
- "type": "check",
- "name": "closed cards say \"reopen\""
- }, {
- "_id": "4e1c96489a872d0284046c6c",
- "pos": 65536,
- "type": "check",
- "name": "list menu says \"close list\""
- }, {
- "_id": "4e1c96566adf53fa83160ba6",
- "pos": 81920,
- "type": "check",
- "name": "hover over archive button on card says \"Close card\""
- }, {
- "_id": "4e1c967b9a872d0284048163",
- "pos": 98304,
- "type": "check",
- "name": "The long-hold menu uses the old close icon"
- }],
- "name": "Checklist"
- }, {
- "_id": "4e1d7fb662429ee21c02f483",
- "checkItems": [{
- "_id": "4e1d7fbe62429ee21c031e63",
- "pos": 17314,
- "type": "check",
- "name": "Show date uploaded"
- }, {
- "_id": "4e1d7fc362429ee21c03346f",
- "pos": 34198,
- "type": "check",
- "name": "Most recent file at top"
- }, {
- "_id": "4e1d7fd162429ee21c034a7d",
- "pos": 51067,
- "type": "check",
- "name": "Delete attachments"
- }, {
- "_id": "4e1d7fdf62429ee21c03608d",
- "pos": 67484,
- "type": "check",
- "name": "Show previews of attachment in action?"
- }, {
- "_id": "4e1dddbd802ec6e81c057d93",
- "pos": 84038,
- "type": "check",
- "name": "Fix previews for deleted attachments"
- }],
- "name": "Checklist"
- }, {
- "_id": "4e1f0b467c741f95300199a6",
- "checkItems": [{
- "_id": "4e1f0b787c741f953001ca06",
- "name": "Add helper text to org section of 'new board' page, include link to create org page.",
- "pos": 16415,
- "type": "check"
- }, {
- "_id": "4e1f0bdf7c741f9530024340",
- "name": "Add create org link on member boards page",
- "pos": 33562,
- "type": "check"
- }],
- "name": "Checklist"
- }, {
- "_id": "4e20882da43ec975a00594c0",
- "checkItems": [{
- "_id": "4e208831a43ec975a005c1c2",
- "pos": 16520,
- "type": "check",
- "name": "Profile of others has incomplete actions list"
- }, {
- "_id": "4e208836a43ec975a005d968",
- "pos": 33602,
- "type": "check",
- "name": "Initial board load prefs"
- }, {
- "_id": "4e208851a43ec975a005f115",
- "pos": 50465,
- "type": "check",
- "name": "No document may grow without bound or we will eventually bump MongoDB's 16MB limit"
- }],
- "name": "Checklist"
- }, {
- "checkItems": [],
- "_id": "4e31abbd44e04eb4f00c7627",
- "name": "Checklist"
- }, {
- "_id": "4e31abbe44e04eb4f00c96fe",
- "checkItems": [{
- "_id": "4e31abd544e04eb4f00cc734",
- "pos": 16928,
- "type": "check",
- "name": "Client takes a long time to figure out it's not connected"
- }],
- "name": "Checklist"
- }, {
- "checkItems": [],
- "_id": "4e36b73a115cc20000019c3a",
- "name": "Checklist"
- }, {
- "_id": "4e723f7dec7bba0000151903",
- "checkItems": [{
- "_id": "4e723f8aec7bba0000155231",
- "pos": 17392,
- "type": "check",
- "name": "Become invisible to people who don't know my email address"
- }, {
- "_id": "4e723f9dec7bba0000159cb6",
- "pos": 34459,
- "type": "check",
- "name": "Become invisible to people who I don't share a board/org with."
- }, {
- "_id": "4e723fb1eb752c26d615d10b",
- "pos": 51840,
- "type": "check",
- "name": "Make it so outsiders can't see what orgs I belong to."
- }],
- "name": "Checklist"
- }, {
- "_id": "4e7a20cb07fb4fc4c2019fc9",
- "checkItems": [{
- "_id": "4e7a20cf4003bbbbc200af97",
- "pos": 33965,
- "type": "check",
- "name": "2"
- }, {
- "_id": "4e7a20d06573bfc3c200013d",
- "pos": 51219,
- "type": "check",
- "name": "3"
- }, {
- "_id": "4e7a20d207fb4fc4c201bf51",
- "pos": 16825,
- "type": "check",
- "name": "1"
- }, {
- "_id": "4e7b97db87f962e71d023066",
- "pos": 67767,
- "type": "check",
- "name": "4"
- }],
- "name": "Checklist"
- }, {
- "_id": "4e95e0160fa46da8a6001c46",
- "checkItems": [{
- "_id": "4e95e0170fa46da8a6002026",
- "pos": 17372,
- "type": "check",
- "name": "1"
- }, {
- "_id": "4e95e0180fa46da8a60022a7",
- "pos": 34778,
- "type": "check",
- "name": "2"
- }, {
- "_id": "4e95e0180fa46da8a600252a",
- "pos": 51336,
- "type": "check",
- "name": "3"
- }, {
- "_id": "4e95e0180fa46da8a60027af",
- "pos": 68195,
- "type": "check",
- "name": "4"
- }, {
- "_id": "4e95e0190fa46da8a6002a36",
- "pos": 85418,
- "type": "check",
- "name": "45"
- }],
- "name": "Checklist"
- }, {
- "_id": "4edf81719dfadf92ba00043f",
- "checkItems": [{
- "_id": "4edf81739dfadf92ba00086e",
- "pos": 16681,
- "type": "check",
- "name": "a"
- }, {
- "_id": "4edf81749dfadf92ba000afd",
- "pos": 33089,
- "type": "check",
- "name": "b"
- }, {
- "_id": "4edf81749dfadf92ba000d8e",
- "pos": 49707,
- "type": "check",
- "name": "c"
- }, {
- "_id": "4edf8194406a1093ba000a7c",
- "pos": 66874,
- "type": "check",
- "name": "d"
- }, {
- "_id": "4edf8194406a1093ba000d17",
- "pos": 83580,
- "type": "check",
- "name": "e"
- }, {
- "_id": "4edf8195406a1093ba000fb7",
- "pos": 100642,
- "type": "check",
- "name": "f"
- }, {
- "_id": "4edf8195406a1093ba001256",
- "pos": 117840,
- "type": "check",
- "name": "g"
- }],
- "name": "ok"
- }, {
- "_id": "4edf81a6406a1093ba0019d4",
- "checkItems": [{
- "_id": "4edf81b1406a1093ba00210b",
- "pos": 16855,
- "type": "check",
- "name": "neato!"
- }],
- "name": "oh yeah?"
- }, {
- "_id": "4ee78a30fd0f210000002a81",
- "checkItems": [{
- "_id": "4ee78a36fd0f210000002ea4",
- "pos": 16858,
- "type": "check",
- "name": "Hello"
- }],
- "name": "Checklist"
- }, {
- "_id": "4f3182e705a3340b64000c26",
- "checkItems": [{
- "_id": "4f3182ea05a3340b640010b0",
- "pos": 17025,
- "type": "check",
- "name": "a"
- }, {
- "_id": "4f3182eb05a3340b64001690",
- "pos": 92449,
- "type": "check",
- "name": "c"
- }, {
- "_id": "4f3182eb05a3340b64001983",
- "pos": 67076,
- "type": "check",
- "name": "d"
- }, {
- "_id": "4f3182ec05a3340b64001c78",
- "pos": 83917,
- "type": "check",
- "name": "e"
- }, {
- "_id": "4f3182ed05a3340b64001f6f",
- "pos": 100981,
- "type": "check",
- "name": "f"
- }, {
- "_id": "4f318ed87568566268000753",
- "pos": 117775,
- "type": "check",
- "name": "g"
- }],
- "name": "Checklist"
- }, {
- "_id": "4f3182ef05a3340b64002267",
- "checkItems": [{
- "_id": "4f3182f105a3340b64002706",
- "pos": 16481,
- "type": "check",
- "name": "1"
- }, {
- "_id": "4f3182f205a3340b64002a03",
- "pos": 33604,
- "type": "check",
- "name": "2"
- }, {
- "_id": "4f3182f205a3340b64002d02",
- "pos": 50212,
- "type": "check",
- "name": "3"
- }, {
- "_id": "4f3182f205a3340b64003003",
- "pos": 67495,
- "type": "check",
- "name": "4"
- }, {
- "_id": "4f3182f305a3340b64003306",
- "pos": 84844,
- "type": "check",
- "name": "5"
- }],
- "name": "Checklist"
- }, {
- "_id": "4f318f5f75685662680020ea",
- "checkItems": [{
- "_id": "4f318f6275685662680025a2",
- "pos": 17401,
- "type": "check",
- "name": "a"
- }, {
- "_id": "4f318f6275685662680028ad",
- "pos": 34370,
- "type": "check",
- "name": "b"
- }, {
- "_id": "4f318f627568566268002bba",
- "pos": 51592,
- "type": "check",
- "name": "c"
- }, {
- "_id": "4f318f637568566268002ec9",
- "pos": 68597,
- "type": "check",
- "name": "d"
- }, {
- "_id": "4f318f6475685662680031da",
- "pos": 85739,
- "type": "check",
- "name": "e"
- }, {
- "_id": "4f318f6475685662680034ed",
- "pos": 102588,
- "type": "check",
- "name": "f"
- }],
- "name": "Checklist"
- }, {
- "_id": "4f357bc7f712566521002a28",
- "checkItems": [],
- "name": "ije"
- }, {
- "checkItems": [],
- "_id": "4f357bd1f71256652100320f",
- "name": "lolz"
- }, {
- "checkItems": [],
- "_id": "4f3a7141e1d0033072000257",
- "name": "Checklist"
- }, {
- "_id": "4f3a7142e1d00330720008d6",
- "checkItems": [{
- "_id": "4f3a7146e1d0033072000da8",
- "pos": 65536,
- "type": "check",
- "name": "1"
- }, {
- "_id": "4f3a7149e1d0033072001225",
- "pos": 33102,
- "type": "check",
- "name": "2"
- }, {
- "_id": "4f3a714ae1d00330720016a5",
- "pos": 49702,
- "type": "check",
- "name": "3"
- }],
- "name": "Checklist"
- }, {
- "_id": "4f3a75b94a2915db72000c7f",
- "checkItems": [{
- "_id": "4f3a75c94a2915db72001315",
- "pos": 16815,
- "type": "check",
- "name": "1"
- }, {
- "_id": "4f3a75cb4a2915db7200179e",
- "pos": 33925,
- "type": "check",
- "name": "2"
- }, {
- "_id": "4f3a75cc4a2915db72001c2a",
- "pos": 50337,
- "type": "check",
- "name": "3"
- }, {
- "_id": "4f3a75e54991753c730001d0",
- "pos": 67169,
- "type": "check",
- "name": "4"
- }, {
- "_id": "4f3a75f64991753c73000d69",
- "pos": 84551,
- "type": "check",
- "name": "5"
- }, {
- "_id": "4f3a7621814abb4d73001252",
- "pos": 101594,
- "type": "check",
- "name": "6"
- }, {
- "_id": "4f3a763e814abb4d7300239c",
- "pos": 118958,
- "type": "check",
- "name": "7"
- }, {
- "_id": "4f3a79b71f2bbfba730001f0",
- "pos": 136130,
- "type": "check",
- "name": "8"
- }, {
- "_id": "4f3a79d01f2bbfba73000a88",
- "pos": 152632,
- "type": "check",
- "name": "9"
- }, {
- "_id": "4f3a7a3e1797fc09740005cc",
- "pos": 169985,
- "type": "check",
- "name": "10"
- }, {
- "_id": "4f3a7ba03893916574004908",
- "pos": 187334,
- "type": "check",
- "name": "11"
- }, {
- "_id": "4f3a7d296f873c9f74002ce5",
- "pos": 204306,
- "type": "check",
- "name": "12"
- }, {
- "_id": "4f3a7d296f873c9f7400318f",
- "pos": 221278,
- "type": "check",
- "name": "13"
- }, {
- "_id": "4f3a7d2a6f873c9f7400363c",
- "pos": 238543,
- "type": "check",
- "name": "14"
- }, {
- "_id": "4f3a882edf73c6a4410009e9",
- "pos": 254942,
- "type": "check",
- "name": "15"
- }],
- "name": "Checklist"
- }, {
- "_id": "4f3a885ddf73c6a441002ce7",
- "checkItems": [{
- "_id": "4f3a8860df73c6a441003222",
- "pos": 17221,
- "type": "check",
- "name": "a"
- }, {
- "_id": "4f3a8861df73c6a441003bd3",
- "pos": 50893,
- "type": "check",
- "name": "c"
- }, {
- "_id": "4f3a8862df73c6a4410040b0",
- "pos": 67457,
- "type": "check",
- "name": "d"
- }, {
- "_id": "4f3a8862df73c6a441004590",
- "pos": 84067,
- "type": "check",
- "name": "e"
- }, {
- "_id": "4f3a8863df73c6a441004a73",
- "pos": 100547,
- "type": "check",
- "name": "f"
- }, {
- "_id": "4f3a8863df73c6a441004f59",
- "name": "grrrrrrrrrrrr",
- "pos": 117713.5,
- "type": "check"
- }, {
- "_id": "4f3d35c8e5f5419d10001e50",
- "name": "h",
- "pos": 66496.5,
- "type": "check"
- }, {
- "_id": "4f425bd3ada835670e02256b",
- "pos": 65536,
- "type": "check",
- "name": "1"
- }, {
- "name": "h",
- "type": "check",
- "pos": 134880,
- "_id": "4f79f18711b5b71a58000a26"
- }],
- "name": "Checklister"
- }, {
- "name": "Checklist",
- "_id": "4f711679661dd5ebc5004c86",
- "checkItems": []
- }, {
- "_id": "4f95b7d74043f7b179006025",
- "checkItems": [{
- "name": "30% packet loss",
- "type": "check",
- "pos": 17398,
- "_id": "4f95b7ec4043f7b17900637a"
- }, {
- "name": "just totally cuts out sometimes",
- "type": "check",
- "pos": 50968,
- "_id": "4f95b7ed4043f7b179006849"
- }, {
- "name": "REALLY lots of latency",
- "type": "check",
- "pos": 33898,
- "_id": "4f95b7edea9ef48b7c001c8a"
- }, {
- "name": "6",
- "type": "check",
- "pos": 152755,
- "_id": "4f95b857ea9ef48b7c0021a0"
- }, {
- "name": "7",
- "type": "check",
- "pos": 169709,
- "_id": "4f95b857ea9ef48b7c00267e"
- }, {
- "name": "8",
- "type": "check",
- "pos": 186988,
- "_id": "4f95b858ea9ef48b7c002b5f"
- }, {
- "name": "9",
- "type": "check",
- "pos": 203481,
- "_id": "4f95b858ea9ef48b7c003043"
- }, {
- "name": "1",
- "type": "check",
- "pos": 67700,
- "_id": "4f95b858ea9ef48b7c00352a"
- }, {
- "name": "4",
- "type": "check",
- "pos": 118187,
- "_id": "4f95b859ea9ef48b7c003a14"
- }, {
- "name": "5",
- "type": "check",
- "pos": 135566,
- "_id": "4f95b859ea9ef48b7c003f01"
- }, {
- "name": "2",
- "type": "check",
- "pos": 84641,
- "_id": "4f95b859ea9ef48b7c0043f1"
- }, {
- "name": "3",
- "type": "check",
- "pos": 101678,
- "_id": "4f95b85aea9ef48b7c0048e4"
- }],
- "name": "Checklisterize"
- }, {
- "_id": "4fa03d6d023c0b2a6b00437d",
- "checkItems": [{
- "name": "One",
- "type": "check",
- "pos": 16384,
- "_id": "4fa03d6f023c0b2a6b0048c1"
- }, {
- "name": "Two",
- "type": "check",
- "pos": 32768,
- "_id": "4fa03d6f023c0b2a6b004db9"
- }, {
- "name": "Three",
- "type": "check",
- "pos": 49152,
- "_id": "4fa03d70023c0b2a6b0052b4"
- }],
- "name": "Checklist"
- }, {
- "_id": "4fa03d79023c0b2a6b0061fc",
- "checkItems": [{
- "_id": "4fa03d7d023c0b2a6b007170",
- "name": "Six Seven Eight Nine",
- "pos": 49152,
- "type": "check"
- }],
- "name": "Checklist"
- }]
-}
diff --git a/benchmarks/clone.js b/benchmarks/clone.js
deleted file mode 100644
index 217f1153419..00000000000
--- a/benchmarks/clone.js
+++ /dev/null
@@ -1,79 +0,0 @@
-'use strict';
-
-const mongoose = require('../');
-const Schema = mongoose.Schema;
-const Benchmark = require('benchmark');
-
-const DocSchema = new Schema({
- title: String
-});
-
-const SimpleSchema = new Schema({
- string: { type: String, required: true },
- number: { type: Number, min: 10 }
-});
-
-const AllSchema = new Schema({
- string: { type: String, required: true },
- number: { type: Number, min: 10 },
- date: Date,
- bool: Boolean,
- buffer: Buffer,
- objectid: Schema.ObjectId,
- array: Array,
- strings: [String],
- numbers: [Number],
- dates: [Date],
- bools: [Boolean],
- buffers: [Buffer],
- objectids: [Schema.ObjectId],
- docs: {
- type: [DocSchema], validate: function() {
- return true;
- }
- },
- s: { nest: String }
-});
-
-const A = mongoose.model('A', AllSchema);
-const a = new A({
- string: 'hello world',
- number: 444848484,
- date: new Date(),
- bool: true,
- buffer: Buffer.alloc(0),
- objectid: new mongoose.Types.ObjectId(),
- array: [4, {}, [], 'asdfa'],
- strings: ['one', 'two', 'three', 'four'],
- numbers: [72, 6493, 83984643, 348282.55],
- dates: [new Date(), new Date(), new Date()],
- bools: [true, false, false, true, true],
- buffers: [Buffer.from([33]), Buffer.from([12])],
-
- objectids: [new mongoose.Types.ObjectId()],
- docs: [{ title: 'yo' }, { title: 'nowafasdi0fas asjkdfla fa' }],
- s: { nest: 'hello there everyone!' }
-});
-
-const Simple = mongoose.model('Simple', SimpleSchema);
-const simple = new Simple({
- string: 'hello world',
- number: 444848484
-});
-
-new Benchmark.Suite()
- .add('Simple', function() {
- simple.toObject({ depopulate: true });
- })
- .add('AllSchema', function() {
- a.toObject({ depopulate: true });
- })
- .on('cycle', function(evt) {
- if (process.env.MONGOOSE_DEV || process.env.PULL_REQUEST) {
- console.log(String(evt.target));
- }
- })
- .run();
-process.memoryUsage();
-
-// --trace-opt --trace-deopt --trace-bailout
\ No newline at end of file
diff --git a/benchmarks/create.js b/benchmarks/create.js
deleted file mode 100644
index 5a1bcb533b7..00000000000
--- a/benchmarks/create.js
+++ /dev/null
@@ -1,46 +0,0 @@
-// require('nodetime').profile();
-
-'use strict';
-
-const mongoose = require('../../mongoose');
-const Benchmark = require('benchmark');
-
-const Schema = mongoose.Schema;
-
-const CheckItem = new Schema({
- name: { type: String },
- type: { type: String },
- pos: { type: Number }
-});
-
-const Checklist = new Schema({
- name: { type: String },
- checkItems: { type: [CheckItem] }
-});
-
-const Board = new Schema({
- checklists: { type: [Checklist] }
-});
-
-const BoardModel = mongoose.model('Board', Board);
-const CheckItemModel = mongoose.model('CheckItem', CheckItem);
-const doc = require('./bigboard.json');
-
-new Benchmark.Suite()
- .add('CheckItem', function() {
- new CheckItemModel({
- _id: '4daee8a2aae47fe55305eabf',
- pos: 32768,
- type: 'check',
- name: 'delete checklists'
- });
- })
- .add('Board', function() {
- new BoardModel(doc);
- })
- .on('cycle', function(evt) {
- if (process.env.MONGOOSE_DEV || process.env.PULL_REQUEST) {
- console.log(String(evt.target));
- }
- })
- .run();
diff --git a/benchmarks/createDeepNestedDocArray.js b/benchmarks/createDeepNestedDocArray.js
new file mode 100644
index 00000000000..0f3ac6d4a7b
--- /dev/null
+++ b/benchmarks/createDeepNestedDocArray.js
@@ -0,0 +1,37 @@
+'use strict';
+
+const mongoose = require('../');
+
+run().catch(err => {
+ console.error(err);
+ process.exit(-1);
+});
+
+async function run() {
+ await mongoose.connect('mongodb://127.0.0.1:27017/mongoose_benchmark');
+
+ const levels = 12;
+
+ let schema = new mongoose.Schema({ test: { type: String, required: true } });
+ let doc = { test: 'gh-14897' };
+ for (let i = 0; i < levels; ++i) {
+ schema = new mongoose.Schema({ level: Number, subdocs: [schema] });
+ doc = { level: (levels - i), subdocs: [{ ...doc }, { ...doc }] };
+ }
+ const Test = mongoose.model('Test', schema);
+
+ if (!process.env.MONGOOSE_BENCHMARK_SKIP_SETUP) {
+ await Test.deleteMany({});
+ }
+
+ const insertStart = Date.now();
+ await Test.create(doc);
+ const insertEnd = Date.now();
+
+ const results = {
+ 'create() time ms': +(insertEnd - insertStart).toFixed(2)
+ };
+
+ console.log(JSON.stringify(results, null, ' '));
+ process.exit(0);
+}
\ No newline at end of file
diff --git a/benchmarks/index.js b/benchmarks/index.js
deleted file mode 100644
index 90521a52bda..00000000000
--- a/benchmarks/index.js
+++ /dev/null
@@ -1,151 +0,0 @@
-'use strict';
-
-Error.stackTraceLimit = Infinity;
-const out = process.argv.length < 3;
-function log() {
- if (out) {
- console.error.apply(console, arguments);
- }
-}
-
-const mongoose = require('../');
-const Schema = mongoose.Schema;
-
-const DocSchema = new Schema({
- title: String
-});
-
-const AllSchema = new Schema({
- string: String,
- number: Number,
- date: Date,
- bool: Boolean,
- buffer: Buffer,
- objectid: Schema.ObjectId,
- array: Array,
- strings: [String],
- numbers: [Number],
- dates: [Date],
- bools: [Boolean],
- buffers: [Buffer],
- objectids: [Schema.ObjectId],
- docs: [DocSchema]
-});
-
-const A = mongoose.model('A', AllSchema);
-
-let numdocs = 0;
-let totaltime = 0;
-
-// bench the normal way
-// the try building the doc into the document prototype
-// and using inheritance and bench that
-//
-// also, bench using listeners for each subdoc vs one
-// listener that knows about all subdocs and notifies
-// them.
-
-function run(label, fn) {
- log('running %s', label);
- let started = process.memoryUsage();
- let start = new Date;
- let total = 10000;
- let i = total;
- let a;
- while (i--) {
- a = fn();
- if (i % 2) {
- a.toObject({ depopulate: true });
- } else {
- if (a._delta) {
- a._delta();
- } else {
- a.$__delta();
- }
- }
- }
- let time = (new Date - start) / 1000;
- totaltime += time;
- numdocs += total;
- log(label + ' took %d seconds for %d docs (%d dps)', time, total, total / time);
- let used = process.memoryUsage();
- let res = {};
- res.rss = used.rss - started.rss;
- res.heapTotal = used.heapTotal - started.heapTotal;
- res.heapUsed = used.heapUsed - started.heapUsed;
- log('change: ', res);
- a = res = used = time = started = start = total = i = null;
-}
-
-run('string', function() {
- return new A({
- string: 'hello world'
- });
-});
-run('number', function() {
- return new A({
- number: 444848484
- });
-});
-run('date', function() {
- return new A({
- date: new Date
- });
-});
-run('bool', function() {
- return new A({
- bool: true
- });
-});
-run('buffer', function() {
- return new A({
- buffer: Buffer.alloc(0)
- });
-});
-run('objectid', function() {
- return new A({
- objectid: new mongoose.Types.ObjectId()
- });
-});
-run('array of mixed', function() {
- return new A({
- array: [4, {}, [], 'asdfa']
- });
-});
-run('array of strings', function() {
- return new A({
- strings: ['one', 'two', 'three', 'four']
- });
-});
-run('array of numbers', function() {
- return new A({
- numbers: [72, 6493, 83984643, 348282.55]
- });
-});
-run('array of dates', function() {
- return new A({
- dates: [new Date, new Date, new Date]
- });
-});
-run('array of bools', function() {
- return new A({
- bools: [true, false, false, true, true]
- });
-});
-run('array of buffers', function() {
- return new A({
- buffers: [Buffer.from([33]), Buffer.from([12])]
- });
-});
-run('array of objectids', function() {
- return new A({
- objectids: [new mongoose.Types.ObjectId]
- });
-});
-run('array of docs', function() {
- return new A({
- docs: [{ title: 'yo' }, { title: 'nowafasdi0fas asjkdfla fa' }]
- });
-});
-
-console.error('completed %d docs in %d seconds (%d dps)', numdocs, totaltime, numdocs / totaltime);
\ No newline at end of file
diff --git a/benchmarks/insertManySimple.js b/benchmarks/insertManySimple.js
new file mode 100644
index 00000000000..9300559294e
--- /dev/null
+++ b/benchmarks/insertManySimple.js
@@ -0,0 +1,38 @@
+'use strict';
+
+const mongoose = require('../');
+
+run().catch(err => {
+ console.error(err);
+ process.exit(-1);
+});
+
+async function run() {
+ await mongoose.connect('mongodb://127.0.0.1:27017/mongoose_benchmark');
+ const FooSchema = new mongoose.Schema({ foo: String });
+ const FooModel = mongoose.model('Foo', FooSchema);
+
+ if (!process.env.MONGOOSE_BENCHMARK_SKIP_SETUP) {
+ await FooModel.deleteMany({});
+ }
+
+ const numDocs = 1500;
+ const docs = [];
+ for (let i = 0; i < numDocs; ++i) {
+ docs.push({ foo: 'test foo ' + i });
+ }
+
+ const numIterations = 200;
+ const insertStart = Date.now();
+ for (let i = 0; i < numIterations; ++i) {
+ await FooModel.insertMany(docs);
+ }
+ const insertEnd = Date.now();
+
+ const results = {
+ 'Average insertMany time ms': +((insertEnd - insertStart) / numIterations).toFixed(2)
+ };
+
+ console.log(JSON.stringify(results, null, ' '));
+ process.exit(0);
+}
diff --git a/benchmarks/mapOfSubdocs.js b/benchmarks/mapOfSubdocs.js
index 9fc6e401919..dc450017448 100644
--- a/benchmarks/mapOfSubdocs.js
+++ b/benchmarks/mapOfSubdocs.js
@@ -8,13 +8,12 @@ run().catch(err => {
});
async function run() {
- await mongoose.connect('mongodb://127.0.0.1:27017/mongoose_test', {
+ await mongoose.connect('mongodb://127.0.0.1:27017/mongoose_benchmark', {
serverSelectionTimeoutMS: 5000
});
const minisMap = new mongoose.Schema(
{
- //? Mini reference
mini: {
type: Map,
of: new mongoose.Schema({
@@ -25,8 +24,6 @@ async function run() {
}),
},
},
- //? Automatic creation of timestamps for creation and updating.
- //? This will be created on the background by the package
{ timestamps: true }
);
const MinisMap = mongoose.model('MinisMap', minisMap);
diff --git a/benchmarks/mem.js b/benchmarks/mem.js
deleted file mode 100644
index c73acac33e4..00000000000
--- a/benchmarks/mem.js
+++ /dev/null
@@ -1,165 +0,0 @@
-'use strict';
-
-const mongoose = require('../');
-
-const Schema = mongoose.Schema;
-
-mongoose.connect('mongodb://127.0.0.1/mongoose-bench');
-
-const DocSchema = new Schema({
- title: String
-});
-
-const AllSchema = new Schema({
- string: { type: String, required: true },
- number: { type: Number, min: 10 },
- date: Date,
- bool: Boolean,
- buffer: Buffer,
- objectid: Schema.ObjectId,
- array: Array,
- strings: [String],
- numbers: [Number],
- dates: [Date],
- bools: [Boolean],
- buffers: [Buffer],
- objectids: [Schema.ObjectId],
- docs: {
- type: [DocSchema], validate: function() {
- return true;
- }
- },
- s: { nest: String }
-});
-
-const A = mongoose.model('A', AllSchema);
-
-const methods = [];
-methods.push(function(a, cb) {
- A.findOne({ _id: a._id }, cb);
-}); // 2 MB
-methods.push(function(a, cb) {
- A.find({ _id: a._id, bool: a.bool }, cb);
-}); // 3.8 MB
-methods.push(function(a, cb) {
- A.findById(a._id, cb);
-}); // 4.6 MB
-methods.push(function(a, cb) {
- A.where('number', a.number).limit(10).exec(cb);
-}); // 4.8 MB
-methods.push(function(a, cb) {
- A.where('date', a.date).select('string').limit(10).exec(cb);
-}); // 3.5 mb
-methods.push(function(a, cb) {
- A.where('date', a.date).select('string bool').limit(10).exec(cb);
-}); // 3.5 MB
-methods.push(function(a, cb) {
- A.where('date', a.date).where('array').in(3).limit(10).exec(cb);
-}); // 1.82 MB
-methods.push(function(a, cb) {
- A.updateOne({ _id: a._id }, { $addToset: { array: 'heeeeello' } }, cb);
-}); // 3.32 MB
-methods.push(function(a, cb) {
- A.deleteOne({ _id: a._id }, cb);
-}); // 3.32 MB
-methods.push(function(a, cb) {
- A.find().where('objectids').exists().select('dates').limit(10).exec(cb);
-}); // 3.32 MB
-methods.push(function(a, cb) {
- A.count({ strings: a.strings[2], number: a.number }, cb);
-}); // 3.32 MB
-methods.push(function(a, cb) {
- a.string = 'asdfaf';
- a.number = 38383838;
- a.date = new Date;
- a.bool = false;
- a.array.push(3);
- a.dates.push(new Date);
- // a.bools.push([true, false]);
- a.docs.addToSet({ title: 'woot' });
- a.strings.remove('three');
- a.numbers.pull(72);
- a.objectids.$pop();
- a.docs.pull.apply(a.docs, a.docs);
- a.s.nest = 'aooooooga';
-
- if (i % 2) {
- a.toObject({ depopulate: true });
- } else {
- if (a._delta) {
- a._delta();
- } else {
- a.$__delta();
- }
- }
-
- cb();
-});
-
-const started = process.memoryUsage();
-const start = new Date;
-const total = 10000;
-let i = total;
-
-function done() {
- const time = (new Date - start);
- const used = process.memoryUsage();
-
- const res = {};
- res.rss = used.rss - started.rss;
- res.heapTotal = used.heapTotal - started.heapTotal;
- res.heapUsed = used.heapUsed - started.heapUsed;
-
- console.error('took %d ms for %d docs (%d dps)', time, total, total / (time / 1000), 'change: ', res);
-
- mongoose.connection.db.dropDatabase(function() {
- mongoose.connection.close();
- });
-}
-
-function cycle() {
- if (i-- === 0) {
- return done();
- }
- let a = new A({
- string: 'hello world',
- number: 444848484,
- date: new Date,
- bool: true,
- buffer: Buffer.alloc(0),
- objectid: new mongoose.Types.ObjectId(),
- array: [4, {}, [], 'asdfa'],
- strings: ['one', 'two', 'three', 'four'],
- numbers: [72, 6493, 83984643, 348282.55],
- dates: [new Date, new Date, new Date],
- bools: [true, false, false, true, true],
- buffers: [Buffer.from([33]), Buffer.from([12])],
- objectids: [new mongoose.Types.ObjectId],
- docs: [{ title: 'yo' }, { title: 'nowafasdi0fas asjkdfla fa' }]
- });
-
- a.save(function() {
- methods[Math.random() * methods.length | 0](a, function() {
- a = null;
- process.nextTick(cycle);
- });
- });
-
- /* if (i%2)
- a.toObject({ depopulate: true });
- else
- a._delta();
-
- if (!(i%50)) {
- var u = process.memoryUsage();
- console.error('rss: %d, vsize: %d, heapTotal: %d, heapUsed: %d',
- u.rss, u.vsize, u.heapTotal, u.heapUsed);
- } */
-}
-
-mongoose.connection.on('open', function() {
- mongoose.connection.db.dropDatabase(function() {
- cycle();
- // --trace-opt --trace-deopt --trace-bailout
- });
-});
diff --git a/benchmarks/populate.js b/benchmarks/populate.js
deleted file mode 100644
index 2be29e72e0e..00000000000
--- a/benchmarks/populate.js
+++ /dev/null
@@ -1,84 +0,0 @@
-'use strict';
-const mongoose = require('../');
-const Schema = mongoose.Schema;
-const docs = process.argv[2] ? process.argv[2] | 0 : 100;
-
-const A = mongoose.model('A', Schema({ name: 'string' }));
-
-const nested = Schema({
- a: { type: Schema.ObjectId, ref: 'A' }
-});
-
-const B = mongoose.model('B', Schema({
- as: [{ type: Schema.ObjectId, ref: 'A' }],
- a: { type: Schema.ObjectId, ref: 'A' },
- nested: [nested]
-}));
-
-let start;
-let count = 0;
-
-mongoose.connect('mongodb://127.0.0.1/mongoose-bench', function(err) {
- if (err) {
- return done(err);
- }
-
- A.create({ name: 'wooooooooooooooooooooooooooooooooooooooooot' }, function(err, a) {
- if (err) {
- return done(err);
- }
-
- let pending = docs;
- for (let i = 0; i < pending; ++i) {
- new B({
- as: [a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a],
- a: a,
- nested: [{ a: a }, { a: a }, { a: a }, { a: a }, { a: a }, { a: a }]
- }).save(function(err) {
- if (err) {
- return done(err);
- }
- --pending;
- if (pending === 0) {
- // console.log('inserted %d docs. beginning test ...', docs);
- start = Date.now();
- test();
- }
- });
- }
- });
-});
-
-function test() {
- let pending = 2;
-
- B.find().populate('as').populate('a').populate('nested.a').exec(handle);
- B.findOne().populate('as').populate('a').populate('nested.a').exec(handle);
-
- function handle(err) {
- if (err) {
- throw err;
- }
- count++;
-
- if (Date.now() - start > 1000 * 20) {
- return done();
- }
-
- if (--pending === 0) {
- return test();
- }
- }
-}
-
-
-function done(err) {
- if (err) {
- console.error(err.stack);
- }
-
- mongoose.connection.db.dropDatabase(function() {
- mongoose.disconnect();
- console.log('%d completed queries on mongoose version %s', count, mongoose.version);
- });
-}
diff --git a/benchmarks/recursiveToObject.js b/benchmarks/recursiveToObject.js
new file mode 100644
index 00000000000..2760df44da0
--- /dev/null
+++ b/benchmarks/recursiveToObject.js
@@ -0,0 +1,60 @@
+'use strict';
+
+const mongoose = require('../');
+
+run().catch(err => {
+ console.error(err);
+ process.exit(-1);
+});
+
+async function run() {
+ await mongoose.connect('mongodb://127.0.0.1:27017/mongoose_benchmark', {
+ serverSelectionTimeoutMS: 5000
+ });
+
+ const ChildSchema = new mongoose.Schema({ name: String, parentId: 'ObjectId' });
+ ChildSchema.virtual('answer').get(function() { return 42; });
+ const ChildModel = mongoose.model('Child', ChildSchema);
+
+ const ParentSchema = new mongoose.Schema({
+ name: String
+ });
+ ParentSchema.virtual('child1', { ref: 'Child', localField: '_id', foreignField: 'parentId' });
+ ParentSchema.virtual('child2', { ref: 'Child', localField: '_id', foreignField: 'parentId' });
+ ParentSchema.virtual('child3', { ref: 'Child', localField: '_id', foreignField: 'parentId' });
+ ParentSchema.virtual('child4', { ref: 'Child', localField: '_id', foreignField: 'parentId' });
+ const ParentModel = mongoose.model('Parent', ParentSchema);
+
+ if (!process.env.MONGOOSE_BENCHMARK_SKIP_SETUP) {
+ await ParentModel.deleteMany({});
+ await ChildModel.deleteMany({});
+
+ const numDocs = 200;
+ const parents = [];
+ for (let i = 0; i < numDocs; ++i) {
+ const parent = await ParentModel.create({ name: 'test parent ' + i });
+ const children = [];
+ console.log(`${i} / ${numDocs}`);
+ for (let j = 0; j < numDocs; ++j) {
+ children.push({ name: 'test child ' + i + '_' + j, parentId: parent._id });
+ }
+ await ChildModel.create(children);
+ parents.push(parent);
+ }
+ }
+
+ const docs = await ParentModel.find().populate(['child1', 'child2', 'child3', 'child4']);
+ for (const doc of docs) {
+ doc.name = 'test parent';
+ }
+ const loopStart = Date.now();
+
+ docs.forEach(doc => doc.toObject({ virtuals: true }));
+
+ const results = {
+ 'Total toObject time ms': Date.now() - loopStart
+ };
+
+ console.log(JSON.stringify(results, null, ' '));
+ process.exit(0);
+}
\ No newline at end of file
diff --git a/benchmarks/saveSimple.js b/benchmarks/saveSimple.js
new file mode 100644
index 00000000000..0029559cdb9
--- /dev/null
+++ b/benchmarks/saveSimple.js
@@ -0,0 +1,57 @@
+'use strict';
+
+const mongoose = require('../');
+
+run().catch(err => {
+ console.error(err);
+ process.exit(-1);
+});
+
+async function run() {
+ await mongoose.connect('mongodb://127.0.0.1:27017/mongoose_benchmark');
+ const FooSchema = new mongoose.Schema({
+ prop1: String,
+ prop2: String,
+ prop3: String,
+ prop4: String,
+ prop5: String,
+ prop6: String,
+ prop7: String,
+ prop8: String,
+ prop9: String,
+ prop10: String
+ });
+ const FooModel = mongoose.model('Foo', FooSchema);
+
+ if (!process.env.MONGOOSE_BENCHMARK_SKIP_SETUP) {
+ await FooModel.deleteMany({});
+ }
+
+ const numIterations = 500;
+ const saveStart = Date.now();
+ for (let i = 0; i < numIterations; ++i) {
+ for (let j = 0; j < 10; ++j) {
+ const doc = new FooModel({
+ prop1: `test ${i}`,
+ prop2: `test ${i}`,
+ prop3: `test ${i}`,
+ prop4: `test ${i}`,
+ prop5: `test ${i}`,
+ prop6: `test ${i}`,
+ prop7: `test ${i}`,
+ prop8: `test ${i}`,
+ prop9: `test ${i}`,
+ prop10: `test ${i}`
+ });
+ await doc.save();
+ }
+ }
+ const saveEnd = Date.now();
+
+ const results = {
+ 'Average save time ms': +((saveEnd - saveStart) / numIterations).toFixed(2)
+ };
+
+ console.log(JSON.stringify(results, null, ' '));
+ process.exit(0);
+}
diff --git a/benchmarks/typescript/simple/package.json b/benchmarks/typescript/simple/package.json
index ee74cdf4d70..6571d866d06 100644
--- a/benchmarks/typescript/simple/package.json
+++ b/benchmarks/typescript/simple/package.json
@@ -1,7 +1,7 @@
{
"dependencies": {
"mongoose": "file:../../../mongoose.tgz",
- "typescript": "4.9.x"
+ "typescript": "5.5.x"
},
"scripts": {
"benchmark": "tsc --extendedDiagnostics"
diff --git a/benchmarks/validate.js b/benchmarks/validate.js
deleted file mode 100644
index 97e41051991..00000000000
--- a/benchmarks/validate.js
+++ /dev/null
@@ -1,68 +0,0 @@
-'use strict';
-
-const mongoose = require('../../mongoose');
-const Benchmark = require('benchmark');
-
-const Schema = mongoose.Schema;
-const breakfastSchema = new Schema({
- eggs: {
- type: Number,
- min: [6, 'Too few eggs'],
- max: 12
- },
- bacon: {
- type: Number,
- required: [true, 'Why no bacon?']
- },
- drink: {
- type: String,
- enum: ['Coffee', 'Tea'],
- required: function() {
- return this.bacon > 3;
- }
- }
-});
-const Breakfast = mongoose.model('Breakfast', breakfastSchema);
-
-const badBreakfast = new Breakfast({
- eggs: 2,
- bacon: 0,
- drink: 'Milk'
-});
-
-const goodBreakfast = new Breakfast({
- eggs: 6,
- bacon: 1,
- drink: 'Tea'
-});
-
-const suite = new Benchmark.Suite();
-suite
- .add('invalid async', {
- defer: true,
- fn: function(deferred) {
- // avoid test inlining
- suite.name;
- badBreakfast.validate().catch(() => deferred.resolve());
- }
- })
- .add('valid async', {
- defer: true,
- fn: function(deferred) {
- // avoid test inlining
- suite.name;
- goodBreakfast.validate().then(() => deferred.resolve());
- }
- })
- .add('invalid sync', function() {
- badBreakfast.validateSync();
- })
- .add('valid sync', function() {
- goodBreakfast.validateSync();
- })
- .on('cycle', function(evt) {
- if (process.env.MONGOOSE_DEV || process.env.PULL_REQUEST) {
- console.log(String(evt.target));
- }
- })
- .run();
\ No newline at end of file
diff --git a/docs/api.md b/docs/api.md
new file mode 100644
index 00000000000..a70251c729f
--- /dev/null
+++ b/docs/api.md
@@ -0,0 +1,7 @@
+# Redirecting
+
+
+
+Redirecting to proper API page, please wait
+
+This Page requires JavaScript to Redirect old links properly
diff --git a/docs/api.pug b/docs/api.pug
deleted file mode 100644
index 992b639f57c..00000000000
--- a/docs/api.pug
+++ /dev/null
@@ -1,67 +0,0 @@
-extends layout
-
-append style
- link(rel="stylesheet", href="/docs/css/api.css")
- script(src="/docs/js/api-bold-current-nav.js")
-
-block content
- h1 API Docs
-
- include includes/native
-
- div.api-nav
- div.api-nav-content
- each item in docs
- - if (!item.hideFromNav)
- div.nav-item(id='nav-' + item.title)
- div.nav-item-title
- a(href='./api/' + item.title.toLowerCase() + '.html')
- | #{item.title}
- ul.nav-item-sub
- each prop in item.props
- li
- a(href='./api/' + item.title.toLowerCase() + '.html#' + prop.anchorId)
- | #{prop.string}
-
- each item in docs
- hr.separate-api
- div.item-header-wrap
-
-
-
- h2(id=item.title, class="item-header")
- a(href='#' + item.title)
- | #{item.title}
- ul
- each prop in item.props
- li
- a(href='#' + prop.anchorId)
- | #{prop.string}
- each prop in item.props
- hr.separate-api-elements
- h3(id=prop.anchorId)
- a(href='#' + prop.anchorId)
- | #{prop.string}
- if prop.param != null
- h5 Parameters
- ul.params
- each param in prop.param
- - if (param.nested)
- ul(style="margin-top: 0.5em")
- li
- | #{param.name}
- | «#{param.types}» !{param.description}
- - else
- li.param
- | #{param.name}
- | «#{param.types}» !{param.description}
- if prop.return != null
- h5 Returns:
- ul
- li «#{prop.return.types}» !{prop.return.description}
- if prop.type != null && prop.type !== 'method' && prop.type !== 'function'
- h5 Type:
- ul
- li «#{prop.type}»
- div
- | !{prop.description}
diff --git a/docs/api_split.pug b/docs/api_split.pug
index b76ef0fe8d5..2078d1b28ee 100644
--- a/docs/api_split.pug
+++ b/docs/api_split.pug
@@ -3,6 +3,7 @@ extends layout
append style
link(rel="stylesheet", href=`${versions.versionedPath}/docs/css/api.css`)
script(src=`${versions.versionedPath}/docs/js/api-bold-current-nav.js`)
+ script(src=`${versions.versionedPath}/docs/js/convert-old-anchorid.js`)
block content
diff --git a/docs/async-await.md b/docs/async-await.md
index b9d5f6cc837..eb2d98ea405 100644
--- a/docs/async-await.md
+++ b/docs/async-await.md
@@ -6,32 +6,12 @@
## Basic Use
-Async/await lets us write asynchronous code as if it were synchronous.
+Async/await lets us write asynchronous code as if it were synchronous.
This is especially helpful for avoiding callback hell when executing multiple async operations in sequence--a common scenario when working with Mongoose.
Each of the three functions below retrieves a record from the database, updates it, and prints the updated record to the console.
```javascript
-// Works.
-function callbackUpdate() {
- MyModel.findOne({ firstName: 'franklin', lastName: 'roosevelt' }, function(err, doc) {
- if (err) {
- handleError(err);
- }
-
- doc.middleName = 'delano';
-
- doc.save(function(err, updatedDoc) {
- if (err) {
- handleError(err);
- }
-
- // Final logic is 2 callbacks deep
- console.log(updatedDoc);
- });
- });
-}
-
-// Better.
+// Using promise chaining
function thenUpdate() {
MyModel.findOne({ firstName: 'franklin', lastName: 'roosevelt' })
.then(function(doc) {
@@ -44,7 +24,7 @@ function thenUpdate() {
});
}
-// Best?
+// Using async/await
async function awaitUpdate() {
try {
const doc = await MyModel.findOne({
@@ -67,7 +47,7 @@ Note that the specific fulfillment values of different Mongoose methods vary, an
## Async Functions
Adding the keyword *async* to a JavaScript function automatically causes it to return a native JavaScript promise.
-This is true [regardless of the return value we specify in the function body](http://thecodebarbarian.com/async-functions-in-javascript.html#an-async-function-always-returns-a-promise).
+This is true [regardless of the return value we specify in the function body](http://thecodebarbarian.com/async-functions-in-javascript.html#an-async-function-always-returns-a-promise).
```javascript
async function getUser() {
@@ -102,13 +82,13 @@ async function doStuffWithUser() {
}
```
-Async/Await with Mongoose Queries
+## Async/Await with Mongoose Queries {#queries}
Under the hood, [async/await is syntactic sugar](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Async_await) over the Promise API.
-Due to the surprisingly simple way promises are implemented in JavaScript, the keyword `await` will try to unwrap any object with a property whose key is the string ‘then’ and whose value is a function.
-Such objects belong to a broader class of objects called [thenables](https://masteringjs.io/tutorials/fundamentals/thenable).
-If the thenable being unwrapped is a genuine promise, e.g. an instance of the [Promise constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise), we enjoy several guarantees about how the object’s ‘then’ function will behave.
-However, Mongoose provides several static helper methods that return a different class of thenable object called a [Query](queries.html)--and [Queries are not promises](queries.html#queries-are-not-promises).
+Due to the surprisingly simple way promises are implemented in JavaScript, the keyword `await` will try to unwrap any object with a property whose key is the string ‘then’ and whose value is a function.
+Such objects belong to a broader class of objects called [thenables](https://masteringjs.io/tutorials/fundamentals/thenable).
+If the thenable being unwrapped is a genuine promise, e.g. an instance of the [Promise constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise), we enjoy several guarantees about how the object’s ‘then’ function will behave.
+However, Mongoose provides several static helper methods that return a different class of thenable object called a [Query](queries.html)--and [Queries are not promises](queries.html#queries-are-not-promises).
Because Queries are also *thenables*, we can interact with a Query using async/await just as we would interact with a genuine promise, with one key difference: observing the fulfillment value of a genuine promise cannot under any circumstances change that value, but trying to re-observe the value of a Query may cause the Query to be re-executed.
```javascript
diff --git a/docs/browser.md b/docs/browser.md
index 8fc68fa29d4..43bc487384a 100644
--- a/docs/browser.md
+++ b/docs/browser.md
@@ -19,7 +19,7 @@ const mongoose = require('mongoose/browser');
import mongoose from 'mongoose/browser';
```
-Using the Browser Library
+## Using the Browser Library {#usage}
Mongoose's browser library is very limited. The only use case it supports is validating documents as shown below.
@@ -34,4 +34,4 @@ const doc = new mongoose.Document({}, new mongoose.Schema({
}));
// Prints an error because `name` is required.
console.log(doc.validateSync());
-```
\ No newline at end of file
+```
diff --git a/docs/change-streams.md b/docs/change-streams.md
index cc6d3d36c94..885279050e2 100644
--- a/docs/change-streams.md
+++ b/docs/change-streams.md
@@ -21,7 +21,7 @@ await Person.create({ name: 'Axl Rose' });
The above script will print output that looks like:
-```no-highlight
+```javascript
{
_id: {
_data: '8262408DAC000000012B022C0100296E5A10042890851837DB4792BE6B235E8B85489F46645F6964006462408DAC6F5C42FF5EE087A20004'
diff --git a/docs/check-version.md b/docs/check-version.md
index 59c97b8c2bc..8088fac23a4 100644
--- a/docs/check-version.md
+++ b/docs/check-version.md
@@ -11,7 +11,7 @@ console.log(mongoose.version); // '7.x.x'
We recommend printing the Mongoose version from Node.js, because that better handles cases where you have multiple versions of Mongoose installed.
You can also execute the above logic from your terminal using Node.js' `-e` flag as follows.
-```
+```sh
# Prints current Mongoose version, e.g. 7.0.3
node -e "console.log(require('mongoose').version)"
```
@@ -20,7 +20,7 @@ node -e "console.log(require('mongoose').version)"
You can also [get the installed version of the Mongoose npm package](https://masteringjs.io/tutorials/npm/version) using `npm list`.
-```
+```sh
$ npm list mongoose
test@ /path/to/test
└── mongoose@7.0.3
@@ -30,5 +30,5 @@ test@ /path/to/test
Other package managers also support similar functions:
-- [`yarn list --pattern "mongoose"`](https://classic.yarnpkg.com/lang/en/docs/cli/list/)
-- [`pnpm list "mongoose"`](https://pnpm.io/cli/list)
+* [`yarn list --pattern "mongoose"`](https://classic.yarnpkg.com/lang/en/docs/cli/list/)
+* [`pnpm list "mongoose"`](https://pnpm.io/cli/list)
diff --git a/docs/compatibility.md b/docs/compatibility.md
index 01f6ebb3926..1c1c8d66ad4 100644
--- a/docs/compatibility.md
+++ b/docs/compatibility.md
@@ -16,18 +16,20 @@ You can refer to [this table](https://www.mongodb.com/docs/drivers/node/current/
Below are the [semver](http://semver.org/) ranges representing which versions of mongoose are compatible with the listed versions of MongoDB server.
-| MongoDB Server | Mongoose |
-| :------------: | :---------------------------: |
-| `6.x` | `^6.5.0` |
-| `5.x` | `^6.0.0` |
-| `4.4.x` | `^5.10.0 \| ^6.0.0` |
-| `4.2.x` | `^5.7.0 \| ^6.0.0` |
-| `4.0.x` | `^5.2.0 \| ^6.0.0` |
-| `3.6.x` | `^5.0.0` |
-| `3.4.x` | `^4.7.3 \| ^5.0.0` |
-| `3.2.x` | `^4.3.0 \| 5.0.0` |
-| `3.0.x` | `^3.8.22 \| ^4.0.0 \| ^5.0.0` |
-| `2.6.x` | `^3.8.8 \| ^4.0.0` |
-| `2.4.x` | `^3.8.0 \| ^4.0.0` |
+| MongoDB Server | Mongoose |
+| :------------: | :--------------------------------------------: |
+| `8.x` | `^8.7.0` |
+| `7.x` | `^7.4.0 \| ^8.0.0` |
+| `6.x` | `^6.5.0 \| ^7.0.0 \| ^8.0.0` |
+| `5.x` | `^5.13.0` \| `^6.0.0 \| ^7.0.0 \| ^8.0.0` |
+| `4.4.x` | `^5.10.0 \| ^6.0.0 \| ^7.0.0 \| ^8.0.0` |
+| `4.2.x` | `^5.7.0 \| ^6.0.0 \| ^7.0.0 \| ^8.0.0` |
+| `4.0.x` | `^5.2.0 \| ^6.0.0 \| ^7.0.0 \| ^8.0.0` |
+| `3.6.x` | `^5.0.0 \| ^6.0.0 \| ^7.0.0 \| ^8.0.0 <8.8.0` |
+| `3.4.x` | `^4.7.3 \| ^5.0.0` |
+| `3.2.x` | `^4.3.0 \| ^5.0.0` |
+| `3.0.x` | `^3.8.22 \| ^4.0.0 \| ^5.0.0` |
+| `2.6.x` | `^3.8.8 \| ^4.0.0 \| ^5.0.0` |
+| `2.4.x` | `^3.8.0 \| ^4.0.0` |
Note that Mongoose `5.x` dropped support for all versions of MongoDB before `3.0.0`. If you need to use MongoDB `2.6` or older, use Mongoose `4.x`.
diff --git a/docs/connections.md b/docs/connections.md
index 5f296c16a8c..24164c01b8b 100644
--- a/docs/connections.md
+++ b/docs/connections.md
@@ -6,9 +6,9 @@ You can connect to MongoDB with the `mongoose.connect()` method.
mongoose.connect('mongodb://127.0.0.1:27017/myapp');
```
-This is the minimum needed to connect the `myapp` database running locally
-on the default port (27017). If connecting fails on your machine, try using
-`127.0.0.1` instead of `localhost`.
+This is the minimum needed to connect the `myapp` database running locally on the default port (27017).
+For local MongoDB databases, we recommend using `127.0.0.1` instead of `localhost`.
+That is because Node.js 18 and up prefer IPv6 addresses, which means, on many machines, Node.js will resolve `localhost` to the IPv6 address `::1` and Mongoose will be unable to connect, unless the mongodb instance is running with ipv6 enabled.
You can also specify several more parameters in the `uri`:
@@ -22,6 +22,7 @@ See the [mongodb connection string spec](http://www.mongodb.com/docs/manual/refe
Buffering
Error Handling
Options
+ serverSelectionTimeoutMS
Connection String Options
Connection Events
A note about keepAlive
@@ -31,9 +32,10 @@ See the [mongodb connection string spec](http://www.mongodb.com/docs/manual/refe
Multi-mongos support
Multiple connections
Connection Pools
+ Multi Tenant Connections
-
+## Operation Buffering {#buffering}
Mongoose lets you start using your models immediately, without waiting for
mongoose to establish a connection to MongoDB.
@@ -42,7 +44,7 @@ mongoose to establish a connection to MongoDB.
mongoose.connect('mongodb://127.0.0.1:27017/myapp');
const MyModel = mongoose.model('Test', new Schema({ name: String }));
// Works
-MyModel.findOne(function(error, result) { /* ... */ });
+await MyModel.findOne();
```
That's because mongoose buffers model function calls internally. This
@@ -52,12 +54,14 @@ connecting.
```javascript
const MyModel = mongoose.model('Test', new Schema({ name: String }));
-// Will just hang until mongoose successfully connects
-MyModel.findOne(function(error, result) { /* ... */ });
+const promise = MyModel.findOne();
setTimeout(function() {
mongoose.connect('mongodb://127.0.0.1:27017/myapp');
}, 60000);
+
+// Will just hang until mongoose successfully connects
+await promise;
```
To disable buffering, turn off the [`bufferCommands` option on your schema](guide.html#bufferCommands).
@@ -91,12 +95,12 @@ const Model = mongoose.model('Test', schema);
await Model.createCollection();
```
-
+## Error Handling {#error-handling}
There are two classes of errors that can occur with a Mongoose connection.
-- Error on initial connection. If initial connection fails, Mongoose will emit an 'error' event and the promise `mongoose.connect()` returns will reject. However, Mongoose will **not** automatically try to reconnect.
-- Error after initial connection was established. Mongoose will attempt to reconnect, and it will emit an 'error' event.
+* **Error on initial connection**: If initial connection fails, Mongoose will emit an 'error' event and the promise `mongoose.connect()` returns will reject. However, Mongoose will **not** automatically try to reconnect.
+* **Error after initial connection was established**: Mongoose will attempt to reconnect, and it will emit an 'error' event.
To handle initial connection errors, you should use `.catch()` or `try/catch` with async/await.
@@ -125,7 +129,7 @@ mongoose.connection.on('error', err => {
Note that Mongoose does not necessarily emit an 'error' event if it loses connectivity to MongoDB. You should
listen to the `disconnected` event to report when Mongoose is disconnected from MongoDB.
-
+## Options {#options}
The `connect` method also accepts an `options` object which will be passed
on to the underlying MongoDB driver.
@@ -148,34 +152,63 @@ Below are some of the options that are important for tuning Mongoose.
* `promiseLibrary` - Sets the [underlying driver's promise library](http://mongodb.github.io/node-mongodb-native/3.1/api/MongoClient.html).
* `maxPoolSize` - The maximum number of sockets the MongoDB driver will keep open for this connection. By default, `maxPoolSize` is 100. Keep in mind that MongoDB only allows one operation per socket at a time, so you may want to increase this if you find you have a few slow queries that are blocking faster queries from proceeding. See [Slow Trains in MongoDB and Node.js](http://thecodebarbarian.com/slow-trains-in-mongodb-and-nodejs). You may want to decrease `maxPoolSize` if you are running into [connection limits](https://www.mongodb.com/docs/atlas/reference/atlas-limits/#connection-limits-and-cluster-tier).
* `minPoolSize` - The minimum number of sockets the MongoDB driver will keep open for this connection. The MongoDB driver may close sockets that have been inactive for some time. You may want to increase `minPoolSize` if you expect your app to go through long idle times and want to make sure your sockets stay open to avoid slow trains when activity picks up.
-* `socketTimeoutMS` - How long the MongoDB driver will wait before killing a socket due to inactivity _after initial connection_. A socket may be inactive because of either no activity or a long-running operation. This is set to `30000` by default, you should set this to 2-3x your longest running operation if you expect some of your database operations to run longer than 20 seconds. This option is passed to [Node.js `socket#setTimeout()` function](https://nodejs.org/api/net.html#net_socket_settimeout_timeout_callback) after the MongoDB driver successfully completes.
+* `socketTimeoutMS` - How long the MongoDB driver will wait before killing a socket due to inactivity *after initial connection*. A socket may be inactive because of either no activity or a long-running operation. `socketTimeoutMS` defaults to 0, which means Node.js will not time out the socket due to inactivity. This option is passed to [Node.js `socket#setTimeout()` function](https://nodejs.org/api/net.html#net_socket_settimeout_timeout_callback) after the MongoDB driver successfully completes.
* `family` - Whether to connect using IPv4 or IPv6. This option passed to [Node.js' `dns.lookup()`](https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback) function. If you don't specify this option, the MongoDB driver will try IPv6 first and then IPv4 if IPv6 fails. If your `mongoose.connect(uri)` call takes a long time, try `mongoose.connect(uri, { family: 4 })`
* `authSource` - The database to use when authenticating with `user` and `pass`. In MongoDB, [users are scoped to a database](https://www.mongodb.com/docs/manual/tutorial/manage-users-and-roles/). If you are getting an unexpected login failure, you may need to set this option.
* `serverSelectionTimeoutMS` - The MongoDB driver will try to find a server to send any given operation to, and keep retrying for `serverSelectionTimeoutMS` milliseconds. If not set, the MongoDB driver defaults to using `30000` (30 seconds).
* `heartbeatFrequencyMS` - The MongoDB driver sends a heartbeat every `heartbeatFrequencyMS` to check on the status of the connection. A heartbeat is subject to `serverSelectionTimeoutMS`, so the MongoDB driver will retry failed heartbeats for up to 30 seconds by default. Mongoose only emits a `'disconnected'` event after a heartbeat has failed, so you may want to decrease this setting to reduce the time between when your server goes down and when Mongoose emits `'disconnected'`. We recommend you do **not** set this setting below 1000, too many heartbeats can lead to performance degradation.
-The `serverSelectionTimeoutMS` option also handles how long `mongoose.connect()` will
-retry initial connection before erroring out. `mongoose.connect()`
-will retry for 30 seconds by default (default `serverSelectionTimeoutMS`) before
-erroring out. To get faster feedback on failed operations, you can reduce `serverSelectionTimeoutMS`
-to 5000 as shown below.
+## serverSelectionTimeoutMS {#serverselectiontimeoutms}
-Example:
+The `serverSelectionTimeoutMS` option is extremely important: it controls how long the MongoDB Node.js driver will attempt to retry any operation before erroring out.
+This includes initial connection, like `await mongoose.connect()`, as well as any operations that make requests to MongoDB, like `save()` or `find()`.
+
+By default, `serverSelectionTimeoutMS` is 30000 (30 seconds).
+This means that, for example, if you call `mongoose.connect()` when your standalone MongoDB server is down, your `mongoose.connect()` call will only throw an error after 30 seconds.
```javascript
-const options = {
- autoIndex: false, // Don't build indexes
- maxPoolSize: 10, // Maintain up to 10 socket connections
- serverSelectionTimeoutMS: 5000, // Keep trying to send operations for 5 seconds
- socketTimeoutMS: 45000, // Close sockets after 45 seconds of inactivity
- family: 4 // Use IPv4, skip trying IPv6
-};
-mongoose.connect(uri, options);
+// Throws an error "getaddrinfo ENOTFOUND doesnt.exist" after 30 seconds
+await mongoose.connect('mongodb://doesnt.exist:27017/test');
+```
+
+Similarly, if your standalone MongoDB server goes down after initial connection, any `find()` or `save()` calls will error out after 30 seconds, unless your MongoDB server is restarted.
+
+While 30 seconds seems like a long time, `serverSelectionTimeoutMS` means you're unlikely to see any interruptions during a [replica set failover](https://www.mongodb.com/docs/manual/replication/#automatic-failover).
+If you lose your replica set primary, the MongoDB Node driver will ensure that any operations you send during the replica set election will eventually execute, assuming that the replica set election takes less than `serverSelectionTimeoutMS`.
+
+To get faster feedback on failed connections, you can reduce `serverSelectionTimeoutMS` to 5000 as follows.
+We don't recommend reducing `serverSelectionTimeoutMS` unless you are running a standalone MongoDB server rather than a replica set, or unless you are using a serverless runtime like [AWS Lambda](lambda.html).
+
+```javascript
+mongoose.connect(uri, {
+ serverSelectionTimeoutMS: 5000
+});
```
-See [this page](http://mongodb.github.io/node-mongodb-native/3.1/reference/faq/) for more information about `connectTimeoutMS` and `socketTimeoutMS`
+There is no way to tune `serverSelectionTimeoutMS` independently for `mongoose.connect()` vs for queries.
+If you want to reduce `serverSelectionTimeoutMS` for queries and other operations, but still retry `mongoose.connect()` for longer, you are responsible for retrying the `connect()` calls yourself using a `for` loop or [a tool like p-retry](https://github.com/Automattic/mongoose/issues/12967#issuecomment-1411227968).
-
+```javascript
+const serverSelectionTimeoutMS = 5000;
+
+// Prints "Failed 0", "Failed 1", "Failed 2" and then throws an
+// error. Exits after approximately 15 seconds.
+for (let i = 0; i < 3; ++i) {
+ try {
+ await mongoose.connect('mongodb://doesnt.exist:27017/test', {
+ serverSelectionTimeoutMS
+ });
+ break;
+ } catch (err) {
+ console.log('Failed', i);
+ if (i >= 2) {
+ throw err;
+ }
+ }
+}
+```
+
+## Callback {#callback}
The `connect()` function also accepts a callback parameter and returns a
[promise](promises.html).
@@ -192,7 +225,7 @@ mongoose.connect(uri, options).then(
);
```
-
+## Connection String Options {#connection-string-options}
You can also specify driver options in your connection string as
[parameters in the query string](https://en.wikipedia.org/wiki/Query_string)
@@ -201,10 +234,10 @@ driver. You **can't** set Mongoose-specific options like `bufferCommands`
in the query string.
```javascript
-mongoose.connect('mongodb://127.0.0.1:27017/test?connectTimeoutMS=1000&bufferCommands=false&authSource=otherdb');
+mongoose.connect('mongodb://127.0.0.1:27017/test?socketTimeoutMS=1000&bufferCommands=false&authSource=otherdb');
// The above is equivalent to:
mongoose.connect('mongodb://127.0.0.1:27017/test', {
- connectTimeoutMS: 1000
+ socketTimeoutMS: 1000
// Note that mongoose will **not** pull `bufferCommands` from the query string
});
```
@@ -212,10 +245,10 @@ mongoose.connect('mongodb://127.0.0.1:27017/test', {
The disadvantage of putting options in the query string is that query
string options are harder to read. The advantage is that you only need a
single configuration option, the URI, rather than separate options for
-`socketTimeoutMS`, `connectTimeoutMS`, etc. Best practice is to put options
+`socketTimeoutMS`, etc. Best practice is to put options
that likely differ between development and production, like `replicaSet`
or `ssl`, in the connection string, and options that should remain constant,
-like `connectTimeoutMS` or `maxPoolSize`, in the options object.
+like `socketTimeoutMS` or `maxPoolSize`, in the options object.
The MongoDB docs have a full list of
[supported connection string options](https://www.mongodb.com/docs/manual/reference/connection-string/).
@@ -225,7 +258,7 @@ are closely associated with the hostname and authentication information.
* `authSource` - The database to use when authenticating with `user` and `pass`. In MongoDB, [users are scoped to a database](https://www.mongodb.com/docs/manual/tutorial/manage-users-and-roles/). If you are getting an unexpected login failure, you may need to set this option.
* `family` - Whether to connect using IPv4 or IPv6. This option passed to [Node.js' `dns.lookup()`](https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback) function. If you don't specify this option, the MongoDB driver will try IPv6 first and then IPv4 if IPv6 fails. If your `mongoose.connect(uri)` call takes a long time, try `mongoose.connect(uri, { family: 4 })`
-
+## Connection Events {#connection-events}
Connections inherit from [Node.js' `EventEmitter` class](https://nodejs.org/api/events.html#events_class_eventemitter),
and emit events when something happens to the connection, like losing
@@ -234,36 +267,50 @@ connection may emit.
* `connecting`: Emitted when Mongoose starts making its initial connection to the MongoDB server
* `connected`: Emitted when Mongoose successfully makes its initial connection to the MongoDB server, or when Mongoose reconnects after losing connectivity. May be emitted multiple times if Mongoose loses connectivity.
-* `open`: Emitted after `'connected'` and `onOpen` is executed on all of this connection's models.
-* `disconnecting`: Your app called [`Connection#close()`](api/connection.html#connection_Connection-close) to disconnect from MongoDB
+* `open`: Emitted after `'connected'` and `onOpen` is executed on all of this connection's models. May be emitted multiple times if Mongoose loses connectivity.
+* `disconnecting`: Your app called [`Connection#close()`](api/connection.html#connection_Connection-close) to disconnect from MongoDB. This includes calling `mongoose.disconnect()`, which calls `close()` on all connections.
* `disconnected`: Emitted when Mongoose lost connection to the MongoDB server. This event may be due to your code explicitly closing the connection, the database server crashing, or network connectivity issues.
* `close`: Emitted after [`Connection#close()`](api/connection.html#connection_Connection-close) successfully closes the connection. If you call `conn.close()`, you'll get both a 'disconnected' event and a 'close' event.
* `reconnected`: Emitted if Mongoose lost connectivity to MongoDB and successfully reconnected. Mongoose attempts to [automatically reconnect](https://thecodebarbarian.com/managing-connections-with-the-mongodb-node-driver.html) when it loses connection to the database.
* `error`: Emitted if an error occurs on a connection, like a `parseError` due to malformed data or a payload larger than [16MB](https://www.mongodb.com/docs/manual/reference/limits/#BSON-Document-Size).
-* `fullsetup`: Emitted when you're connecting to a replica set and Mongoose has successfully connected to the primary and at least one secondary.
-* `all`: Emitted when you're connecting to a replica set and Mongoose has successfully connected to all servers specified in your connection string.
-When you're connecting to a single MongoDB server (a "standalone"), Mongoose will emit 'disconnected' if it gets
-disconnected from the standalone server, and 'connected' if it successfully connects to the standalone. In a
-replica set, Mongoose will emit 'disconnected' if it loses connectivity to the replica set primary, and 'connected' if it manages to reconnect to the replica set primary.
+When you're connecting to a single MongoDB server (a ["standalone"](https://www.mongodb.com/docs/cloud-manager/tutorial/deploy-standalone/)), Mongoose will emit `disconnected` if it gets
+disconnected from the standalone server, and `connected` if it successfully connects to the standalone. In a
+[replica set](https://www.mongodb.com/docs/manual/replication/), Mongoose will emit `disconnected` if it loses connectivity to the replica set primary, and `connected` if it manages to reconnect to the replica set primary.
+
+If you are using `mongoose.connect()`, you can use the following to listen to the above events:
-
+```javascript
+mongoose.connection.on('connected', () => console.log('connected'));
+mongoose.connection.on('open', () => console.log('open'));
+mongoose.connection.on('disconnected', () => console.log('disconnected'));
+mongoose.connection.on('reconnected', () => console.log('reconnected'));
+mongoose.connection.on('disconnecting', () => console.log('disconnecting'));
+mongoose.connection.on('close', () => console.log('close'));
+
+mongoose.connect('mongodb://127.0.0.1:27017/mongoose_test');
+```
-For long running applications, it is often prudent to enable `keepAlive`
-with a number of milliseconds. Without it, after some period of time
-you may start to see `"connection closed"` errors for what seems like
-no reason. If so, after
-[reading this](https://tldp.org/HOWTO/TCP-Keepalive-HOWTO/overview.html),
-you may decide to enable `keepAlive`:
+With `mongoose.createConnection()`, use the following instead:
```javascript
-mongoose.connect(uri, { keepAlive: true, keepAliveInitialDelay: 300000 });
+const conn = mongoose.createConnection('mongodb://127.0.0.1:27017/mongoose_test');
+
+conn.on('connected', () => console.log('connected'));
+conn.on('open', () => console.log('open'));
+conn.on('disconnected', () => console.log('disconnected'));
+conn.on('reconnected', () => console.log('reconnected'));
+conn.on('disconnecting', () => console.log('disconnecting'));
+conn.on('close', () => console.log('close'));
```
-`keepAliveInitialDelay` is the number of milliseconds to wait before initiating `keepAlive` on the socket.
-`keepAlive` is true by default since mongoose 5.2.0.
+## A note about keepAlive {#keepAlive}
+
+Before Mongoose 5.2.0, you needed to enable the `keepAlive` option to initiate [TCP keepalive](https://tldp.org/HOWTO/TCP-Keepalive-HOWTO/overview.html) to prevent `"connection closed"` errors.
+However, `keepAlive` has been `true` by default since Mongoose 5.2.0, and the `keepAlive` is deprecated as of Mongoose 7.2.0.
+Please remove `keepAlive` and `keepAliveInitialDelay` options from your Mongoose connections.
-
+## Replica Set Connections {#replicaset_connections}
To connect to a replica set you pass a comma delimited list of hosts to
connect to rather than a single host.
@@ -284,7 +331,7 @@ To connect to a single node replica set, specify the `replicaSet` option.
mongoose.connect('mongodb://host1:port1/?replicaSet=rsName');
```
-
+## Server Selection {#server-selection}
The underlying MongoDB driver uses a process known as [server selection](https://github.com/mongodb/specifications/blob/master/source/server-selection/server-selection.rst) to connect to MongoDB and send operations to MongoDB.
If the MongoDB driver can't find a server to send an operation to after `serverSelectionTimeoutMS`,
@@ -319,13 +366,13 @@ mongoose.connect(uri, {
}).catch(err => console.log(err.reason));
```
-
+## Replica Set Host Names {#replicaset-hostnames}
-MongoDB replica sets rely on being able to reliably figure out the domain name for each member.
+MongoDB replica sets rely on being able to reliably figure out the domain name for each member.
On Linux and OSX, the MongoDB server uses the output of the [`hostname` command](https://linux.die.net/man/1/hostname) to figure out the domain name to report to the replica set.
This can cause confusing errors if you're connecting to a remote MongoDB replica set running on a machine that reports its `hostname` as `localhost`:
-```
+```txt
// Can get this error even if your connection string doesn't include
// `localhost` if `rs.conf()` reports that one replica set member has
// `localhost` as its host name.
@@ -338,7 +385,7 @@ Follow [this page's instructions to change a replica set member's host name](htt
You can also check the `reason.servers` property of `MongooseServerSelectionError` to see what the MongoDB Node driver thinks the state of your replica set is.
The `reason.servers` property contains a [map](https://masteringjs.io/tutorials/fundamentals/map) of server descriptions.
-```
+```js
if (err.name === 'MongooseServerSelectionError') {
// Contains a Map describing the state of your replica set. For example:
// Map(1) {
@@ -352,7 +399,7 @@ if (err.name === 'MongooseServerSelectionError') {
}
```
-
+## Multi-mongos support {#mongos_connections}
You can also connect to multiple [mongos](https://www.mongodb.com/docs/manual/reference/program/mongos/) instances
for high availability in a sharded cluster. You do
@@ -363,10 +410,10 @@ for high availability in a sharded cluster. You do
mongoose.connect('mongodb://mongosA:27501,mongosB:27501', cb);
```
-
+## Multiple connections {#multiple_connections}
So far we've seen how to connect to MongoDB using Mongoose's default
-connection. Mongoose creates a _default connection_ when you call `mongoose.connect()`.
+connection. Mongoose creates a *default connection* when you call `mongoose.connect()`.
You can access the default connection using `mongoose.connection`.
You may need multiple connections to MongoDB for several reasons.
@@ -379,16 +426,24 @@ The `mongoose.createConnection()` function takes the same arguments as
const conn = mongoose.createConnection('mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]]', options);
```
-This [connection](api/connection.html#connection_Connection) object is then used to
-create and retrieve [models](api/model.html#model_Model). Models are
-**always** scoped to a single connection.
+This [connection](api/connection.html#connection_Connection) object is then used to create and retrieve [models](api/model.html#model_Model).
+Models are **always** scoped to a single connection.
```javascript
const UserModel = conn.model('User', userSchema);
```
-If you use multiple connections, you should make sure you export schemas,
-**not** models. Exporting a model from a file is called the _export model pattern_.
+The `createConnection()` function returns a connection instance, not a promise.
+If you want to use `await` to make sure Mongoose successfully connects to MongoDB, use the [`asPromise()` function](api/connection.html#Connection.prototype.asPromise()):
+
+```javascript
+// `asPromise()` returns a promise that resolves to the connection
+// once the connection succeeds, or rejects if connection failed.
+const conn = await mongoose.createConnection(connectionString).asPromise();
+```
+
+If you use multiple connections, you should make sure you export schemas, **not** models.
+Exporting a model from a file is called the *export model pattern*.
The export model pattern is limited because you can only use one connection.
```javascript
@@ -402,30 +457,10 @@ module.exports = userSchema;
// module.exports = mongoose.model('User', userSchema);
```
-If you use the export schema pattern, you still need to create models
-somewhere. There are two common patterns. First is to export a connection
-and register the models on the connection in the file:
-
-```javascript
-// connections/fast.js
-const mongoose = require('mongoose');
-
-const conn = mongoose.createConnection(process.env.MONGODB_URI);
-conn.model('User', require('../schemas/user'));
-
-module.exports = conn;
-
-// connections/slow.js
-const mongoose = require('mongoose');
-
-const conn = mongoose.createConnection(process.env.MONGODB_URI);
-conn.model('User', require('../schemas/user'));
-conn.model('PageView', require('../schemas/pageView'));
-
-module.exports = conn;
-```
-
-Another alternative is to register connections with a dependency injector
+If you use the export schema pattern, you still need to create models somewhere.
+There are two common patterns.
+The first is to create a function that instantiates a new connection and registers all models on that connection.
+With this pattern, you may also register connections with a dependency injector
or another [inversion of control (IOC) pattern](https://thecodebarbarian.com/using-ramda-as-a-dependency-injector).
```javascript
@@ -441,7 +476,24 @@ module.exports = function connectionFactory() {
};
```
-
+Exporting a function that creates a new connection is the most flexible pattern.
+However, that pattern can make it tricky to get access to your connection from your route handlers or wherever your business logic is.
+An alternative pattern is to export a connection and register the models on the connection in the file's top-level scope as follows.
+
+```javascript
+// connections/index.js
+const mongoose = require('mongoose');
+
+const conn = mongoose.createConnection(process.env.MONGODB_URI);
+conn.model('User', require('../schemas/user'));
+
+module.exports = conn;
+```
+
+You can create separate files for each connection, like `connections/web.js` and `connections/mobile.js` if you want to create separate connections for your web API backend and your mobile API backend.
+Your business logic can then `require()` or `import` the connection it needs.
+
+## Connection Pools {#connection_pools}
Each `connection`, whether created with `mongoose.connect` or
`mongoose.createConnection` are all backed by an internal configurable
@@ -457,6 +509,89 @@ const uri = 'mongodb://127.0.0.1:27017/test?maxPoolSize=10';
mongoose.createConnection(uri);
```
-Next Up
+The connection pool size is important because [MongoDB currently can only process one operation per socket](https://thecodebarbarian.com/slow-trains-in-mongodb-and-nodejs).
+So `maxPoolSize` functions as a cap on the number of concurrent operations.
+
+## Multi Tenant Connections {#multi-tenant-connections}
+
+In the context of Mongoose, a multi-tenant architecture typically means a case where multiple different clients talk to MongoDB through a single Mongoose application.
+This typically means each client makes queries and executes updates through a single Mongoose application, but has a distinct MongoDB database within the same MongoDB cluster.
+
+We recommend reading [this article about multi-tenancy with Mongoose](https://medium.com/brightlab-techblog/multitenant-node-js-application-with-mongoose-mongodb-f8841a285b4f); it has a good description of how we define multi-tenancy and a more detailed overview of our recommended patterns.
+
+There are two patterns we recommend for multi-tenancy in Mongoose:
+
+1. Maintain one connection pool, switch between tenants using the [`Connection.prototype.useDb()` method](https://mongoosejs.com/docs/api/connection.html#Connection.prototype.useDb()).
+2. Maintain a separate connection pool per tenant, store connections in a map or [POJO](https://masteringjs.io/tutorials/fundamentals/pojo).
+
+The following is an example of pattern (1).
+We recommend pattern (1) for cases where you have a small number of tenants, or if each individual tenant's workload is light (approximately < 1 request per second, all requests take < 10ms of database processing time).
+Pattern (1) is simpler to implement and simpler to manage in production, because there is only 1 connection pool.
+But, under high load, you will likely run into issues where some tenants' operations slow down other tenants' operations due to [slow trains](https://thecodebarbarian.com/slow-trains-in-mongodb-and-nodejs).
+
+```javascript
+const express = require('express');
+const mongoose = require('mongoose');
+
+mongoose.connect('mongodb://127.0.0.1:27017/main');
+mongoose.set('debug', true);
+
+mongoose.model('User', mongoose.Schema({ name: String }));
+
+const app = express();
+
+app.get('/users/:tenantId', function(req, res) {
+ const db = mongoose.connection.useDb(`tenant_${req.params.tenantId}`, {
+ // `useCache` tells Mongoose to cache connections by database name, so
+ // `mongoose.connection.useDb('foo', { useCache: true })` returns the
+ // same reference each time.
+ useCache: true
+ });
+ // Need to register models every time a new connection is created
+ if (!db.models['User']) {
+ db.model('User', mongoose.Schema({ name: String }));
+ }
+ console.log('Find users from', db.name);
+ db.model('User').find().
+ then(users => res.json({ users })).
+ catch(err => res.status(500).json({ message: err.message }));
+});
+
+app.listen(3000);
+```
+
+The following is an example of pattern (2).
+Pattern (2) is more flexible and better for use cases with > 10k tenants and > 1 requests/second.
+Because each tenant has a separate connection pool, one tenants' slow operations will have minimal impact on other tenants.
+However, this pattern is harder to implement and manage in production.
+In particular, [MongoDB does have a limit on the number of open connections](https://www.mongodb.com/blog/post/tuning-mongodb--linux-to-allow-for-tens-of-thousands-connections), and [MongoDB Atlas has separate limits on the number of open connections](https://www.mongodb.com/docs/atlas/reference/atlas-limits), so you need to make sure the total number of sockets in your connection pools doesn't go over MongoDB's limits.
+
+```javascript
+const express = require('express');
+const mongoose = require('mongoose');
+
+const tenantIdToConnection = {};
+
+const app = express();
+
+app.get('/users/:tenantId', function(req, res) {
+ let initialConnection = Promise.resolve();
+ const { tenantId } = req.params;
+ if (!tenantIdToConnection[tenantId]) {
+ tenantIdToConnection[tenantId] = mongoose.createConnection(`mongodb://127.0.0.1:27017/tenant_${tenantId}`);
+ tenantIdToConnection[tenantId].model('User', mongoose.Schema({ name: String }));
+ initialConnection = tenantIdToConnection[tenantId].asPromise();
+ }
+ const db = tenantIdToConnection[tenantId];
+ initialConnection.
+ then(() => db.model('User').find()).
+ then(users => res.json({ users })).
+ catch(err => res.status(500).json({ message: err.message }));
+});
+
+app.listen(3000);
+```
+
+## Next Up {#next}
Now that we've covered connections, let's take a look at [models](models.html).
diff --git a/docs/css/carbonads.css b/docs/css/carbonads.css
new file mode 100644
index 00000000000..021b8ad93a5
--- /dev/null
+++ b/docs/css/carbonads.css
@@ -0,0 +1,32 @@
+/* CPM ads */
+
+.carbonad{
+ margin-top:0!important;
+ margin-bottom:-3rem!important
+}
+
+#carbonads {
+ position:fixed;
+ right: 0px;
+ bottom: 0px;
+ display:block;
+ width:160px;
+ padding:15px 15px 15px 150px;
+ overflow:hidden;
+ font-size:13px;
+ line-height:1.4;
+ text-align:left;
+ background-color: #fafafa;
+}
+
+@media (max-width: 1170px) {
+ #carbonads {
+ display: none !important;
+ }
+}
+
+#carbonads a{color:#333;text-decoration:none}
+
+.carbon-img{float:left;margin-left:-145px}
+
+.carbon-poweredby{display:block;color:#777!important}
diff --git a/docs/css/default.css b/docs/css/default.css
deleted file mode 100644
index ccb22728ed3..00000000000
--- a/docs/css/default.css
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
-
-Original style from softwaremaniacs.org (c) Ivan Sagalaev
-
-*/
-
-pre code {
- display: block; padding: 0.5em;
- background: #F0F0F0;
-}
-
-pre code,
-pre .ruby .subst,
-pre .tag .title,
-pre .lisp .title,
-pre .clojure .built_in,
-pre .nginx .title {
- color: black;
-}
-
-pre .string,
-pre .title,
-pre .constant,
-pre .parent,
-pre .tag .value,
-pre .rules .value,
-pre .rules .value .number,
-pre .preprocessor,
-pre .ruby .symbol,
-pre .ruby .symbol .string,
-pre .ruby .symbol .keyword,
-pre .ruby .symbol .keymethods,
-pre .instancevar,
-pre .aggregate,
-pre .template_tag,
-pre .django .variable,
-pre .smalltalk .class,
-pre .addition,
-pre .flow,
-pre .stream,
-pre .bash .variable,
-pre .apache .tag,
-pre .apache .cbracket,
-pre .tex .command,
-pre .tex .special,
-pre .erlang_repl .function_or_atom,
-pre .markdown .header {
- color: #800;
-}
-
-pre .comment,
-pre .annotation,
-pre .template_comment,
-pre .diff .header,
-pre .chunk,
-pre .markdown .blockquote {
- color: #888;
-}
-
-pre .number,
-pre .date,
-pre .regexp,
-pre .literal,
-pre .smalltalk .symbol,
-pre .smalltalk .char,
-pre .go .constant,
-pre .change,
-pre .markdown .bullet,
-pre .markdown .link_url {
- color: #080;
-}
-
-pre .label,
-pre .javadoc,
-pre .ruby .string,
-pre .decorator,
-pre .filter .argument,
-pre .localvars,
-pre .array,
-pre .attr_selector,
-pre .important,
-pre .pseudo,
-pre .pi,
-pre .doctype,
-pre .deletion,
-pre .envvar,
-pre .shebang,
-pre .apache .sqbracket,
-pre .nginx .built_in,
-pre .tex .formula,
-pre .erlang_repl .reserved,
-pre .input_number,
-pre .markdown .link_label,
-pre .vhdl .attribute,
-pre .clojure .attribute {
- color: #88F
-}
-
-pre .keyword,
-pre .id,
-pre .phpdoc,
-pre .title,
-pre .built_in,
-pre .aggregate,
-pre .css .tag,
-pre .javadoctag,
-pre .phpdoc,
-pre .yardoctag,
-pre .smalltalk .class,
-pre .winutils,
-pre .bash .variable,
-pre .apache .tag,
-pre .go .typename,
-pre .tex .command,
-pre .markdown .strong,
-pre .request,
-pre .status {
- font-weight: bold;
-}
-
-pre .markdown .emphasis {
- font-style: italic;
-}
-
-pre .nginx .built_in {
- font-weight: normal;
-}
-
-pre .coffeescript .javascript,
-pre .xml .css,
-pre .xml .javascript,
-pre .xml .vbscript,
-pre .tex .formula {
- opacity: 0.5;
-}
diff --git a/docs/css/flexcpc.css b/docs/css/flexcpc.css
deleted file mode 100644
index 4b0b8dbe92f..00000000000
--- a/docs/css/flexcpc.css
+++ /dev/null
@@ -1,124 +0,0 @@
-/* CPC ads */
-
-.native-js {
- visibility: hidden;
- transition: all .25s ease-in-out;
- opacity: 0;
- flex-flow: column nowrap;
- transform: translateY(calc(100% - 35px));
-}
-
-.native-show {
- visibility: visible;
- position: fixed;
- width: 100%;
- bottom: 0;
- box-shadow: 0 -1px 4px 1px hsla(0, 0%, 0%, .15);
- opacity: 1;
- z-index: 10;
-}
-
-.native-show:hover {
- transform: translateY(0);
-}
-
-.native-img {
- margin-right: 20px;
- max-height: 50px;
- border-radius: 3px;
- width: auto;
-}
-
-.native-sponsor {
- margin: 10px 20px;
- text-align: center;
- text-transform: uppercase;
- transform-origin: left;
- letter-spacing: .5px;
- transition: all .3s ease-in-out;
- font-size: 12px;
-}
-
-.native-show:hover .native-sponsor {
- transform: scaleY(0);
- margin: 0 20px;
- opacity: 0;
-}
-
-.native-flex {
- display: flex;
- padding: 20px 20px 40px;
- text-decoration: none;
-
- flex-flow: row nowrap;
- justify-content: space-between;
- align-items: center;
-}
-
-.native-main {
- display: flex;
-
- flex-flow: row nowrap;
- align-items: center;
-}
-
-.native-details {
- display: flex;
- margin-right: 30px;
-
- flex-flow: column nowrap;
-}
-
-.native-company {
- margin-bottom: 4px;
- text-transform: uppercase;
- letter-spacing: 2px;
- font-size: 10px;
-}
-
-.native-desc {
- letter-spacing: 1px;
- font-weight: 300;
- line-height: 1.4;
-}
-
-.native-cta {
- padding: 10px 14px;
- border-radius: 3px;
- box-shadow: 0 6px 13px 0 hsla(0, 0%, 0%, .15);
- text-transform: uppercase;
- white-space: nowrap;
- letter-spacing: 1px;
- font-weight: 400;
- font-size: 12px;
- transition: all .3s ease-in-out;
- transform: translateY(-1px);
-}
-
-.native-cta:hover {
- box-shadow: none;
- transform: translateY(1px);
-}
-
-@media only screen and (min-width: 320px) and (max-width: 759px) {
- .native-flex {
- flex-wrap: wrap;
- flex-direction: column;
- }
-
- .native-img {
- margin: 0;
- }
-
- .native-details {
- margin: 0;
- }
-
- .native-main {
- margin-bottom: 20px;
- flex-wrap: wrap;
- flex-direction: column;
- text-align: center;
- align-content: center;
- }
-}
diff --git a/docs/css/github.css b/docs/css/github.css
index 75f54f5bd29..f8cec1bdc3f 100644
--- a/docs/css/github.css
+++ b/docs/css/github.css
@@ -1,3 +1,33 @@
+code {
+ background-color: #eee;
+ padding: 2px 4px;
+ font-size: 0.9em;
+ color: #800;
+ border-radius: 4px;
+}
+
+pre code {
+ background-color: transparent;
+ padding: 0;
+ font-size: 1em;
+ color: #222;
+}
+
+pre {
+ display: block;
+ padding: 9.5px;
+ margin: 10px 0 10px;
+ font-size: 13px;
+ line-height: 1.42857143;
+ color: #333;
+ word-break: break-all;
+ word-wrap: break-word;
+ background-color: #f5f5f5;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ font-family: Menlo,Monaco,Consolas,"Courier New",monospace;
+}
+
/*
github.com style (c) Vasily Polovnyov
@@ -121,4 +151,4 @@ github.com style (c) Vasily Polovnyov
.hljs-chunk {
color: #aaa;
-}
\ No newline at end of file
+}
diff --git a/docs/css/guide.css b/docs/css/guide.css
deleted file mode 100644
index d18ea6af618..00000000000
--- a/docs/css/guide.css
+++ /dev/null
@@ -1,332 +0,0 @@
-html, body, #content {
- height: 100%;
-}
-:target::before {
- content: ">>> ";
- color: #1371C9;
- font-weight: bold;
- font-size: 20px;
-}
-.module {
- min-height: 100%;
- box-sizing: border-box;
- overflow-x: hidden;
-}
-body {
- background: #d8e2d8 url(/docs/images/square_bg.png) fixed;
- font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
- color: #333;
- -webkit-font-smoothing: antialiased;
- -webkit-text-size-adjust: 100%;
- padding: 0;
- margin: 0;
- font-size: 14px;
- line-height: 22px;
-}
-a {
- color: #800;
- -webkit-transition-property: opacity, -webkit-transform, color, background-color, padding, -webkit-box-shadow;
- -webkit-transition-duration: 0.15s;
- -webkit-transition-timing-function: ease-out;
-}
-a:hover {
- opacity: 0.8;
-}
-h1 {
- font-family: 'Helvetica Nueue', Helvetica, Arial, FreeSans, sans-serif;
- text-rendering: geometricPrecision;
-}
-pre {
- background: rgba(255,255,255,.8);
- border: 1px solid #bbb;
- padding:5px;
- border-radius: 3px;
- box-shadow: 1px 3px 6px #ddd;
-}
-code {
- background: rgba(255,255,255,.8);
- color: #333;
- border-radius: 3px;
- font-size: 13px;
- font-family: Consolas, "Liberation Mono", Courier, monospace;
- /*text-shadow: 1px 2px 2px #555;*/
-}
-pre code {
- border: 0 none;
- padding: 1.2em;
- overflow-x: auto;
-}
-h2 {
- margin-top: 0;
-}
-h2 a {
- font-size: 12px;
- position: relative;
- bottom: 3px;
- font-weight: normal;
-}
-h3 { padding-top: 35px; }
-h3 code {
- font-weight: normal;
-}
-hr {
- display: none;
- height: 1px;
- border: 0 none;
- padding: 0;
- margin: 90px 0;
- background: -webkit-gradient(linear, left top, right top, from(rgba(57, 172, 57, 0.0)), color-stop(0.5, rgba(57, 172, 57, 0.33)), to(rgba(57, 172, 57, 0.0)))
-}
-.doclinks hr {
- margin: 10px 0;
-}
-li {
- list-style: square;
-}
-#header {
- padding-top: 22px;
- padding-bottom: 25px;
- text-transform: lowercase;
-}
-#header h1 {
- margin-top: 0;
- margin-bottom: 0;
-}
-#header h1 a {
- text-decoration: none;
-}
-#header .mongoose {
- font-size: 48px;
- font-weight: 100;
- color: #fff;
- letter-spacing: -5px;
-}
-#links {
- position: fixed;
- top: 0;
- left: 0;
- bottom: 0;
- width: 210px;
- overflow-x: hidden;
- overflow-y: auto;
- padding: 15px 0 30px 20px;
- border-right: 1px solid #ddd;
- background: -webkit-gradient(linear, left top, right top, from(transparent), color-stop(0.92, transparent), color-stop(0.9201, rgba(172,172,172, 0.0)), to(rgba(172,172,172, 0.4))), transparent;
-}
-#links .schematypes span {
- display: none;
-}
-#content {
- padding: 0;
- margin: 0 0 0 230px;
-}
-#content .controls {
- padding: 5px 15px 5px 10px;
- position: fixed;
- background: #fff;
- border: 3px solid #eee;
- border-radius: 0 0 12px 0;
- border-width: 0 3px 3px 10px;
- width: 100%;
- bottom: 0;
- opacity: 0.75;
- -webkit-transition-property: opacity;
- -webkit-transition-duration: 0.15s;
- -webkit-transition-timing-function: ease-out;
-}
-#content .controls:hover {
- opacity: .9;
-}
-#content p {
- word-wrap: break-word;
-}
-#content > ul {
- margin: 0;
- padding: 0;
-}
-.private {
- display: none;
-}
-.doclinks li.private a:before,
-.doclinks .module.private a:before,
-.doclinks item.private a:before {
- content: "p";
- background: #333;
- color: #fff;
- font-size: 11px;
- line-height: 15px;
- font-weight: normal;
- padding: 0 2px;
- border-radius: 3px;
- border: 1px solid #333;
- display: inline-block;
- margin-right: 5px;
-}
-#content .private h3:after {
- content: "private";
- background: #333;
- color: #fff;
- font-size: 11px;
- line-height: 15px;
- font-weight: normal;
- padding: 0 2px;
- border-radius: 3px;
- border: 1px solid #333;
- display: inline-block;
- margin-left: 5px;
-}
-.module {
- list-style: none;
- padding: 30px 0 0 30px;
- border-color: #eee;
- border-width: 9px 10px;
- border-style: solid;
- background-color: #fff;
-}
-.module > * {
- max-width: 700px;
-}
-.item {
- margin-bottom: 175px;
-}
-.item h3 a {
- color: #333;
- text-decoration: none;
-}
-.property h3 span {
- color: #444;
-}
-.description {
- margin-top: 25px;
-}
-.sourcecode {
- display: none;
-}
-.showcode {
- font-size: 12px;
- cursor: pointer;
- display: none;
-}
-.load .showcode {
- display: block;
-}
-.types a {
- text-decoration: none;
-}
-li.guide ul {
- padding-left: 16px;
-}
-
-.important {
- background-color: #FBFF94;
- margin-left: 5px;
-}
-.important p {
- padding: 22px;
-}
-
-ul.inthewild {
- margin: 30px 0 0 -30px;
- padding: 0;
- width: 125%;
- max-width: 125%;
-}
-ul.inthewild li {
- display: inline-block;
- list-style: none;
-}
-ul.inthewild img {
- width: 200px;
-}
-
-@media only screen and (device-width: 768px) {
- ul.inthewild {
- margin-left: 0px;
- }
-}
-
-@media only screen and (max-width: 480px) {
- ul.inthewild {
- margin-left: 0px;
- }
- ul.inthewild li {
- margin: 5px;
- border-width: 2px 2px 0 2px;
- border-style: solid;
- border-color: #eee;
- }
- ul.inthewild li img {
- width: 140px;
- }
- h2 a {
- white-space: nowrap;
- }
- #forkbanner { display: none }
- #header .mongoose {
- font-size: 65px;
- text-align: center;
- }
- html, body, #content {
- height: auto;
- }
- #links {
- position: static;
- width: auto;
- border: 0 none;
- border-right: 0 none;
- border-bottom: 1px solid #ddd;
- background: -webkit-gradient(linear, left top, left bottom, from(transparent), color-stop(0.92, transparent), color-stop(0.9201, rgba(172,172,172, 0.0)), to(rgba(172,172,172, 0.4))), transparent;
- padding: 15px 0;
- }
- #links, #links ul, #links li { -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; }
- #links ul { padding: 0 10px 0 0; }
- #links li {
- list-style: none;
- display: inline-block;
- width: 25%;
- text-align: center;
- }
- #links .home, #links .support, #links .fork {
- display: none;
- }
- .doclinks {
- display: none;
- }
- #content { margin-left: 0; }
- .module {
- padding-left: 5px;
- border-width: 3px;
- }
- #links li.guide {
- display: block;
- width: 390px;
- margin-bottom: 15px;
- }
- #links li.guide > a {
- display: none;
- }
- #links li ul li {
- width: 44%;
- text-align: left;
- }
- #links li ul li ul li {
- width: 95%;
- }
- #links .plugins,
- #links .changelog {
- display: none;
- }
- #links .schematypes span {
- display: inline;
- }
- #links .double {
- width: 332px;
- }
- #links .double > ul {
- display: inline;
- float: right;
- }
- #links .double > ul li {
- width: 155px;
- }
-}
diff --git a/docs/css/mongoose5.css b/docs/css/mongoose5.css
index 036f1fc1765..4ae64f50ddf 100644
--- a/docs/css/mongoose5.css
+++ b/docs/css/mongoose5.css
@@ -15,6 +15,10 @@ a {
color: #0971B2;
}
+p {
+ line-height: 1.5em;
+}
+
h1 a::before,
h2 a::before,
h3 a::before,
@@ -50,21 +54,6 @@ h4 a {
color: #000;
}
-code {
- background-color: #eee;
- padding: 2px 4px;
- font-size: 0.9em;
- color: #800;
- border-radius: 4px;
-}
-
-pre code {
- background-color: transparent;
- padding: 0;
- font-size: 1em;
- color: #222;
-}
-
#logo {
width: 62px;
height: 30px;
@@ -82,25 +71,28 @@ pre code {
}
.pure-menu-item {
- height: 28px;
font-size: 12pt;
padding-top: 0px;
}
.pure-menu-link {
padding-top: 2px;
+ padding-bottom: 2px;
}
-li.sub-item {
- height: 23px;
- font-size: 11pt;
- margin-left: 15px;
+/* change sub-item lists to be more dense */
+.sub-item > .pure-menu-link {
+ padding-top: 0px;
+ padding-bottom: 0px;
}
-li.tertiary-item {
- height: 23px;
+.pure-menu-link:hover, .pure-menu-link.selected {
+ background-color: rgba(0,0,0, 0.1);
+}
+
+li.sub-item {
font-size: 11pt;
- margin-left: 30px;
+ margin-left: 15px;
}
li.version {
@@ -113,12 +105,15 @@ li.version ul.pure-menu-children {
}
#logo-container {
- position: relative;
- top: -4px;
+ padding-top: 0;
padding-bottom: 6px;
border-bottom: 1px solid #ddd;
}
+#logo-container > a {
+ display: block;
+}
+
#menu {
z-index: 1;
position: fixed;
@@ -136,37 +131,27 @@ li.version ul.pure-menu-children {
left: 250px;
padding-left: 50px;
width: 850px;
- margin-bottom: 250px;
-}
-
-pre {
- display: block;
- padding: 9.5px;
- margin: 10px 0 10px;
- font-size: 13px;
- line-height: 1.42857143;
- color: #333;
- word-break: break-all;
- word-wrap: break-word;
- background-color: #f5f5f5;
- border: 1px solid #ccc;
- border-radius: 4px;
- font-family: Menlo,Monaco,Consolas,"Courier New",monospace;
+ margin-bottom: 30lvh;
}
/* Search */
.search {
- margin-top: 0.25em;
+ margin-top: 0.25em !important; /* important, because regardless of order, the ":not(:first-child)" rule takes precedence */
margin-bottom: 0.75em;
+ display: flex;
}
.search input {
border: 1px solid #ddd;
- margin-left: 1em;
padding: 0.25em;
width: 170px;
border-radius: 3px;
+ flex: 1;
+}
+
+#search-input-nav {
+ margin-left: 1em;
}
.search button {
@@ -177,9 +162,11 @@ pre {
height: 30px;
width: 30px;
padding: 0px;
- position: relative;
- top: 5px;
- left: 1px;
+ flex: 0 0 auto;
+}
+
+#search-button-nav {
+ margin-right: 1em;
}
.search button img {
@@ -207,6 +194,10 @@ pre {
font-weight: bold;
}
+.pure-menu-item:not(:first-child), .pure-menu-item.sub-item {
+ margin-top: 2px;
+}
+
/* Mobile */
#mobile-menu {
@@ -233,7 +224,7 @@ pre {
#menu {
display: none;
- position: absolute;
+ position: fixed;
top: 45px;
border-top: 1px solid #ddd;
border-right: 1px solid #ddd;
@@ -251,6 +242,9 @@ pre {
height: 45px;
background-color: #eee;
border-bottom: 1px solid #ddd;
+ position: sticky;
+ top: 0;
+ z-index: 1;
}
#logo {
@@ -277,7 +271,6 @@ pre {
top: 0px;
left: 0;
background-color: #eee;
- font-size: 10px; /* change this value to increase/decrease button size */
z-index: 10;
width: 2em;
height: 3px;
@@ -289,28 +282,11 @@ pre {
background: #ddd;
}
- .menu-link span {
- position: relative;
- display: block;
- }
-
- .menu-link span,
- .menu-link span:before,
- .menu-link span:after {
- background-color: #333;
- width: 100%;
- height: 0.2em;
- }
-
- .menu-link span:before,
- .menu-link span:after {
- position: absolute;
- margin-top: -0.6em;
- content: " ";
- }
-
- .menu-link span:after {
- margin-top: 0.6em;
+ #menuLink {
+ width: 40px;
+ height: 40px;
+ padding: 2.5px;
+ color: rgb(0,0,0);
}
.active {
@@ -323,48 +299,14 @@ pre {
}
.pure-menu-item:last-of-type {
- padding-bottom: 20px;
-}
-
-/* CPM ads */
-
-.carbonad{
- margin-top:0!important;
- margin-bottom:-3rem!important
-}
-
-#carbonads {
- position:fixed;
- right: 0px;
- bottom: 0px;
- display:block;
- width:160px;
- padding:15px 15px 15px 150px;
- overflow:hidden;
- font-size:13px;
- line-height:1.4;
- text-align:left;
- background-color: #fafafa;
-}
-
-@media (max-width: 1170px) {
- #carbonads {
- display: none !important;
- }
+ padding-bottom: 5px;
}
-@media (max-width: 1360px) {
- #jobs {
- display: none !important;
- }
+/* dont add padding if it also has other elements (like a sub-list) */
+.pure-menu-link:not(:last-child) {
+ padding-bottom: 0px;
}
-#carbonads a{color:#333;text-decoration:none}
-
-.carbon-img{float:left;margin-left:-145px}
-
-.carbon-poweredby{display:block;color:#777!important}
-
/* job board */
#jobs {
@@ -422,4 +364,10 @@ pre {
#jobs .jobs-view-more a {
color: black;
-}
\ No newline at end of file
+}
+
+@media (max-width: 1360px) {
+ #jobs {
+ display: none !important;
+ }
+}
diff --git a/docs/css/style.css b/docs/css/style.css
index f5ab8822aa8..32edaaea473 100644
--- a/docs/css/style.css
+++ b/docs/css/style.css
@@ -46,6 +46,7 @@ pre {
background: #eee;
padding: 5px;
border-radius: 3px;
+ overflow-x: auto;
}
code {
color: #333;
diff --git a/docs/customschematypes.md b/docs/customschematypes.md
index 95c202aed82..3093eab1e5e 100644
--- a/docs/customschematypes.md
+++ b/docs/customschematypes.md
@@ -2,7 +2,7 @@
## Creating a Basic Custom Schema Type
-_New in Mongoose 4.4.0:_ Mongoose supports custom types. Before you
+*New in Mongoose 4.4.0:* Mongoose supports custom types. Before you
reach for a custom type, however, know that a custom type is overkill
for most use cases. You can do most basic tasks with
[custom getters/setters](http://mongoosejs.com/docs/2.7.x/docs/getters-setters.html),
diff --git a/docs/deprecations.md b/docs/deprecations.md
index 54745af44cd..d7a7936e1fe 100644
--- a/docs/deprecations.md
+++ b/docs/deprecations.md
@@ -5,112 +5,34 @@ that Mongoose users should be aware of. Mongoose provides options to work
around these deprecation warnings, but you need to test whether these options
cause any problems for your application. Please [report any issues on GitHub](https://github.com/Automattic/mongoose/issues/new).
-
+## Summary {#summary}
To fix all deprecation warnings, follow the below steps:
-* Replace `update()` with `updateOne()`, `updateMany()`, or `replaceOne()`
-* Replace `remove()` with `deleteOne()` or `deleteMany()`.
-* Replace `count()` with `countDocuments()`, unless you want to count how many documents are in the whole collection (no filter). In the latter case, use `estimatedDocumentCount()`.
+* Replace `rawResult: true` with `includeResultMetadata: true` in `findOneAndUpdate()`, `findOneAndReplace()`, `findOneAndDelete()` calls.
Read below for more a more detailed description of each deprecation warning.
-
+## `rawResult` {#rawresult}
-The MongoDB driver's [`remove()` function](http://mongodb.github.io/node-mongodb-native/3.1/api/Collection.html#remove) is deprecated in favor of `deleteOne()` and `deleteMany()`. This is to comply with
-the [MongoDB CRUD specification](https://github.com/mongodb/specifications/blob/master/source/crud/crud.rst),
-which aims to provide a consistent API for CRUD operations across all MongoDB
-drivers.
-
-```
-DeprecationWarning: collection.remove is deprecated. Use deleteOne,
-deleteMany, or bulkWrite instead.
-```
-
-To remove this deprecation warning, replace any usage of `remove()` with
-`deleteMany()`, _unless_ you specify the [`single` option to `remove()`](api/model.html#model_Model-remove). The `single`
-option limited `remove()` to deleting at most one document, so you should
-replace `remove(filter, { single: true })` with `deleteOne(filter)`.
+As of Mongoose 7.4.0, the `rawResult` option to `findOneAndUpdate()` is deprecated.
+You should instead use the `includeResultMetadata` option, which the MongoDB Node.js driver's new option that replaces `rawResult`.
```javascript
// Replace this:
-MyModel.remove({ foo: 'bar' });
-// With this:
-MyModel.deleteMany({ foo: 'bar' });
-
-// Replace this:
-MyModel.remove({ answer: 42 }, { single: true });
-// With this:
-MyModel.deleteOne({ answer: 42 });
-```
+const doc = await Test.findOneAndUpdate(
+ { name: 'Test' },
+ { name: 'Test Testerson' },
+ { rawResult: true }
+);
-
-
-Like `remove()`, the [`update()` function](api/model.html#model_Model-update) is deprecated in favor
-of the more explicit [`updateOne()`](api/model.html#model_Model-updateOne), [`updateMany()`](api/model.html#model_Model-updateMany), and [`replaceOne()`](api/model.html#model_Model-replaceOne) functions. You should replace
-`update()` with `updateOne()`, unless you use the [`multi` or `overwrite` options](api/model.html#model_Model-update).
-
-```
-collection.update is deprecated. Use updateOne, updateMany, or bulkWrite
-instead.
-```
-
-```javascript
-// Replace this:
-MyModel.update({ foo: 'bar' }, { answer: 42 });
// With this:
-MyModel.updateOne({ foo: 'bar' }, { answer: 42 });
-
-// If you use `overwrite: true`, you should use `replaceOne()` instead:
-MyModel.update(filter, update, { overwrite: true });
-// Replace with this:
-MyModel.replaceOne(filter, update);
-
-// If you use `multi: true`, you should use `updateMany()` instead:
-MyModel.update(filter, update, { multi: true });
-// Replace with this:
-MyModel.updateMany(filter, update);
+const doc = await Test.findOneAndUpdate(
+ { name: 'Test' },
+ { name: 'Test Testerson' },
+ { includeResultMetadata: true }
+);
```
-
-
-The MongoDB server has deprecated the `count()` function in favor of two
-separate functions, [`countDocuments()`](#query_Query-countDocuments) and
-[`estimatedDocumentCount()`](#query_Query-estimatedDocumentCount).
-
-```
-DeprecationWarning: collection.count is deprecated, and will be removed in a future version. Use collection.countDocuments or collection.estimatedDocumentCount instead
-```
-
-The difference between the two is `countDocuments()` can accept a filter
-parameter like [`find()`](#query_Query-find). The `estimatedDocumentCount()`
-function is faster, but can only tell you the total number of documents in
-a collection. You cannot pass a `filter` to `estimatedDocumentCount()`.
-
-To migrate, replace `count()` with `countDocuments()` _unless_ you do not
-pass any arguments to `count()`. If you use `count()` to count all documents
-in a collection as opposed to counting documents that match a query, use
-`estimatedDocumentCount()` instead of `countDocuments()`.
-
-```javascript
-// Replace this:
-MyModel.count({ answer: 42 });
-// With this:
-MyModel.countDocuments({ answer: 42 });
-
-// If you're counting all documents in the collection, use
-// `estimatedDocumentCount()` instead.
-MyModel.count();
-// Replace with:
-MyModel.estimatedDocumentCount();
-
-// Replace this:
-MyModel.find({ answer: 42 }).count().exec();
-// With this:
-MyModel.find({ answer: 42 }).countDocuments().exec();
-
-// Replace this:
-MyModel.find().count().exec();
-// With this, since there's no filter
-MyModel.find().estimatedDocumentCount().exec();
-```
+The `rawResult` option only affects Mongoose; the MongoDB Node.js driver still returns the full result metadata, Mongoose just parses out the raw document.
+The `includeResultMetadata` option also tells the MongoDB Node.js driver to only return the document, not the full `ModifyResult` object.
diff --git a/docs/discriminators.md b/docs/discriminators.md
index 2507c0aa417..1d3abf56ca0 100644
--- a/docs/discriminators.md
+++ b/docs/discriminators.md
@@ -53,3 +53,27 @@ To update a document's discriminator key, use `findOneAndUpdate()` or `updateOne
```acquit
[require:use overwriteDiscriminatorKey to change discriminator key]
```
+
+## Embedded discriminators in arrays
+
+You can also define discriminators on embedded document arrays.
+Embedded discriminators are different because the different discriminator types are stored in the same document array (within a document) rather than the same collection.
+In other words, embedded discriminators let you store subdocuments matching different schemas in the same array.
+
+As a general best practice, make sure you declare any hooks on your schemas **before** you use them.
+You should **not** call `pre()` or `post()` after calling `discriminator()`.
+
+```acquit
+[require:Embedded discriminators in arrays]
+```
+
+## Single nested discriminators
+
+You can also define discriminators on single nested subdocuments, similar to how you can define discriminators on arrays of subdocuments.
+
+As a general best practice, make sure you declare any hooks on your schemas **before** you use them.
+You should **not** call `pre()` or `post()` after calling `discriminator()`.
+
+```acquit
+[require:Single nested discriminators]
+```
diff --git a/docs/documents.md b/docs/documents.md
index 216541982c9..6aca0814253 100644
--- a/docs/documents.md
+++ b/docs/documents.md
@@ -8,12 +8,13 @@ to documents as stored in MongoDB. Each document is an instance of its
Documents vs Models
Retrieving
Updating Using save()
+ Setting Nested Properties
Updating Using Queries
Validating
Overwriting
-
+## Documents vs Models {#documents-vs-models}
[Document](api/document.html#Document) and [Model](api/model.html#Model) are distinct
classes in Mongoose. The Model class is a subclass of the Document class.
@@ -33,7 +34,7 @@ In Mongoose, a "document" generally means an instance of a model.
You should not have to create an instance of the Document class without
going through a model.
-
+## Retrieving {#retrieving}
When you load documents from MongoDB using model functions like [`findOne()`](api/model.html#model_Model-findOne),
you get a Mongoose document back.
@@ -46,7 +47,7 @@ doc instanceof mongoose.Model; // true
doc instanceof mongoose.Document; // true
```
-
+## Updating Using `save()` {#updating-using-save}
Mongoose documents track changes. You can modify a document using vanilla
JavaScript assignments and Mongoose will convert it into [MongoDB update operators](https://www.mongodb.com/docs/manual/reference/operator/update/).
@@ -81,7 +82,55 @@ doc.name = 'foo';
await doc.save(); // Throws DocumentNotFoundError
```
-
+## Setting Nested Properties
+
+Mongoose documents have a `set()` function that you can use to safely set deeply nested properties.
+
+```javascript
+const schema = new Schema({
+ nested: {
+ subdoc: new Schema({
+ name: String
+ })
+ }
+});
+const TestModel = mongoose.model('Test', schema);
+
+const doc = new TestModel();
+doc.set('nested.subdoc.name', 'John Smith');
+doc.nested.subdoc.name; // 'John Smith'
+```
+
+Mongoose documents also have a `get()` function that lets you safely read deeply nested properties. `get()` lets you avoid having to explicitly check for nullish values, similar to JavaScript's [optional chaining operator `?.`](https://masteringjs.io/tutorials/fundamentals/optional-chaining-array).
+
+```javascript
+const doc2 = new TestModel();
+
+doc2.get('nested.subdoc.name'); // undefined
+doc2.nested?.subdoc?.name; // undefined
+
+doc2.set('nested.subdoc.name', 'Will Smith');
+doc2.get('nested.subdoc.name'); // 'Will Smith'
+```
+
+You can use optional chaining `?.` and nullish coalescing `??` with Mongoose documents.
+However, be careful when using [nullish coalescing assignments `??=`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_assignment) to create nested paths with Mongoose documents.
+
+```javascript
+// The following works fine
+const doc3 = new TestModel();
+doc3.nested.subdoc ??= {};
+doc3.nested.subdoc.name = 'John Smythe';
+
+// The following does **NOT** work.
+// Do not use the following pattern with Mongoose documents.
+const doc4 = new TestModel();
+(doc4.nested.subdoc ??= {}).name = 'Charlie Smith';
+doc.nested.subdoc; // Empty object
+doc.nested.subdoc.name; // undefined.
+```
+
+## Updating Using Queries {#updating-using-queries}
The [`save()`](api/model.html#model_Model-save) function is generally the right
way to update a document with Mongoose. With `save()`, you get full
@@ -96,11 +145,11 @@ with casting, [middleware](middleware.html#notes), and [limited validation](vali
await MyModel.updateMany({}, { $set: { name: 'foo' } });
```
-_Note that `update()`, `updateMany()`, `findOneAndUpdate()`, etc. do *not*
+*Note that `update()`, `updateMany()`, `findOneAndUpdate()`, etc. do **not**
execute `save()` middleware. If you need save middleware and full validation,
-first query for the document and then `save()` it._
+first query for the document and then `save()` it.*
-
+## Validating {#validating}
Documents are casted and validated before they are saved. Mongoose first casts
values to the specified type and then validates them. Internally, Mongoose
@@ -122,7 +171,7 @@ await p2.validate();
Mongoose also supports limited validation on updates using the [`runValidators` option](validation.html#update-validators).
Mongoose casts parameters to query functions like `findOne()`, `updateOne()`
-by default. However, Mongoose does _not_ run validation on query function
+by default. However, Mongoose does *not* run validation on query function
parameters by default. You need to set `runValidators: true` for Mongoose
to validate.
@@ -136,7 +185,7 @@ await Person.updateOne({}, { age: -1 }, { runValidators: true });
Read the [validation](validation.html) guide for more details.
-
+## Overwriting {#overwriting}
There are 2 different ways to overwrite a document (replacing all keys in the
document). One way is to use the
diff --git a/docs/enterprise.md b/docs/enterprise.md
index 795080a3e85..bad5366b5e3 100644
--- a/docs/enterprise.md
+++ b/docs/enterprise.md
@@ -23,37 +23,37 @@ Python, Java, PHP, Ruby, .NET, and more.
Your subscription includes:
-- Security updates
+* Security updates
Tidelift’s security response team coordinates patches for new breaking security
vulnerabilities and alerts immediately through a private channel, so your
software supply chain is always secure.
-- Licensing verification and indemnification
+* Licensing verification and indemnification
Tidelift verifies license information to enable easy policy enforcement and
adds intellectual property indemnification to cover creators and users in case
something goes wrong. You always have a 100% up-to-date bill of materials for
your dependencies to share with your legal team, customers, or partners.
-- Maintenance and code improvement
+* Maintenance and code improvement
Tidelift ensures the software you rely on keeps working as long as you need it
to work. Your managed dependencies are actively maintained and we recruit
additional maintainers where required.
-- Package selection and version guidance
+* Package selection and version guidance
We help you choose the best open source packages from the start—and then
guide you through updates to stay on the best releases as new issues arise.
-- Roadmap input
+* Roadmap input
Take a seat at the table with the creators behind the software you use.
Tidelift’s participating maintainers earn more income as their software is
used by more subscribers, so they’re interested in knowing what you need.
-- Tooling and cloud integration
+* Tooling and cloud integration
Tidelift works with GitHub, GitLab, BitBucket, and more. We support every
cloud platform (and other deployment targets, too).
diff --git a/docs/faq.md b/docs/faq.md
index d3b119c47e2..a0d493bc879 100644
--- a/docs/faq.md
+++ b/docs/faq.md
@@ -12,6 +12,19 @@ hr {
}
+
+
+**Q** . I get an error `connect ECONNREFUSED ::1:27017` when connecting to `localhost`. Why?
+
+The easy solution is to replace `localhost` with `127.0.0.1`.
+
+The reason why this error happens is that Node.js 18 and up prefer IPv6 addresses over IPv4 by default.
+And, most Linux and OSX machines have a `::1 localhost` entry in `/etc/hosts` by default.
+That means that Node.js 18 will assume that `localhost` means the IPv6 `::1` address.
+And MongoDB doesn't accept IPv6 connections by default.
+
+You can also fix this error by [enabling IPv6 support on your MongoDB server](https://www.mongodb.com/docs/manual/core/security-mongodb-configuration/).
+
**Q** . Operation `...` timed out after 10000 ms. What gives?
@@ -66,9 +79,8 @@ const schema = new mongoose.Schema({
});
const Model = db.model('Test', schema);
-Model.create([{ name: 'Val' }, { name: 'Val' }], function(err) {
- console.log(err); // No error, unless index was already built
-});
+// No error, unless index was already built
+await Model.create([{ name: 'Val' }, { name: 'Val' }]);
```
However, if you wait for the index to build using the `Model.on('index')` event, attempts to save duplicates will correctly error.
@@ -79,21 +91,12 @@ const schema = new mongoose.Schema({
});
const Model = db.model('Test', schema);
-Model.on('index', function(err) { // <-- Wait for model's indexes to finish
- assert.ifError(err);
- Model.create([{ name: 'Val' }, { name: 'Val' }], function(err) {
- console.log(err);
- });
-});
-
-// Promise based alternative. `init()` returns a promise that resolves
-// when the indexes have finished building successfully. The `init()`
+// Wait for model's indexes to finish. The `init()`
// function is idempotent, so don't worry about triggering an index rebuild.
-Model.init().then(function() {
- Model.create([{ name: 'Val' }, { name: 'Val' }], function(err) {
- console.log(err);
- });
-});
+await Model.init();
+
+// Throws a duplicate key error
+await Model.create([{ name: 'Val' }, { name: 'Val' }]);
```
MongoDB persists indexes, so you only need to rebuild indexes if you're starting
@@ -136,7 +139,7 @@ is undefined on the underlying [POJO](guide.html#minimize).
**Q** . I'm using an arrow function for a [virtual](guide.html#virtuals), [middleware](middleware.html), [getter](api/schematype.html#schematype_SchemaType-get)/[setter](api/schematype.html#schematype_SchemaType-set), or [method](guide.html#methods) and the value of `this` is wrong.
-**A**. Arrow functions [handle the `this` keyword much differently than conventional functions](https://masteringjs.io/tutorials/fundamentals/arrow#why-not-arrow-functions).
+**A**. Arrow functions [handle the `this` keyword differently than conventional functions](https://masteringjs.io/tutorials/fundamentals/arrow#why-not-arrow-functions).
Mongoose getters/setters depend on `this` to give you access to the document that you're writing to, but this functionality does not work with arrow functions. Do **not** use arrow functions for mongoose getters/setters unless do not intend to access the document in the getter/setter.
```javascript
@@ -290,7 +293,7 @@ compiled" when I use nodemon / a testing framework?
**A**. `mongoose.model('ModelName', schema)` requires 'ModelName' to be
unique, so you can access the model by using `mongoose.model('ModelName')`.
If you put `mongoose.model('ModelName', schema);` in a
-[mocha `beforeEach()` hook](https://mochajs.org/#hooks), this code will
+[mocha `beforeEach()` hook](https://masteringjs.io/tutorials/mocha/beforeeach), this code will
attempt to create a new model named 'ModelName' before **every** test,
and so you will get an error. Make sure you only create a new model with
a given name **once**. If you need to create multiple models with the
@@ -311,7 +314,7 @@ const Kitten = connection.model('Kitten', kittySchema);
**Q** . How can I change mongoose's default behavior of initializing an array path to an empty array so that I can require real data on document creation?
-**A**. You can set the default of the array to a function that returns `undefined`.
+**A**. You can set the default of the array to `undefined`.
```javascript
const CollectionSchema = new Schema({
@@ -323,16 +326,16 @@ const CollectionSchema = new Schema({
```
-
+
**Q** . How can I initialize an array path to `null`?
-**A**. You can set the default of the array to a function that returns `null`.
+**A**. You can set the default of the array to [`null`](https://masteringjs.io/tutorials/fundamentals/null).
```javascript
const CollectionSchema = new Schema({
field1: {
type: [String],
- default: () => { return null; }
+ default: null
}
});
```
diff --git a/docs/field-level-encryption.md b/docs/field-level-encryption.md
new file mode 100644
index 00000000000..3531fca0218
--- /dev/null
+++ b/docs/field-level-encryption.md
@@ -0,0 +1,114 @@
+# Integrating with MongoDB Client Side Field Level Encryption
+
+[Client Side Field Level Encryption](https://www.mongodb.com/docs/manual/core/csfle/), or CSFLE for short, is a tool for storing your data in an encrypted format in MongoDB.
+For example, instead of storing the `name` property as a plain-text string, CSFLE means MongoDB will store your document with `name` as an encrypted buffer.
+The resulting document will look similar to the following to a client that doesn't have access to decrypt the data.
+
+
+
+```js
+{
+ "_id" : ObjectId("647a3207661e3a3a1bc3e614"),
+ "name" : BinData(6,"ASrIv7XfokKwiCUJEjckOdgCG+u6IqavcOWX8hINz29MLvcKDZ4nnjCnPFZG+0ftVxMdWgzu6Vdh7ys1uIK1WiaPN0SqpmmtL2rPoqT9gfhADpGDmI60+vm0bJepXNY1Gv0="),
+ "__v" : 0
+}
+```
+
+You can read more about CSFLE on the [MongoDB CSFLE documentation](https://www.mongodb.com/docs/manual/core/csfle/) and [this blog post about CSFLE in Node.js](https://www.mongodb.com/developer/languages/javascript/client-side-field-level-encryption-csfle-mongodb-node/).
+
+Note that Mongoose does **not** currently have any Mongoose-specific APIs for CSFLE.
+Mongoose defers all CSFLE-related work to the MongoDB Node.js driver, so the [`autoEncryption` option](https://mongodb.github.io/node-mongodb-native/5.6/interfaces/AutoEncryptionOptions.html) for `mongoose.connect()` and `mongoose.createConnection()` is where you put all CSFLE-related configuration.
+Mongoose schemas currently don't support CSFLE configuration.
+
+## Setting Up Field Level Encryption with Mongoose
+
+First, you need to install the [mongodb-client-encryption npm package](https://www.npmjs.com/package/mongodb-client-encryption).
+This is MongoDB's official package for setting up encryption keys.
+
+```sh
+npm install mongodb-client-encryption
+```
+
+You also need to make sure you've installed [mongocryptd](https://www.mongodb.com/docs/manual/core/queryable-encryption/reference/mongocryptd/).
+mongocryptd is a separate process from the MongoDB server that you need to run to work with field level encryption.
+You can either run mongocryptd yourself, or make sure it is on the system PATH and the MongoDB Node.js driver will run it for you.
+[You can read more about mongocryptd here](https://www.mongodb.com/docs/v5.0/reference/security-client-side-encryption-appendix/#mongocryptd).
+
+Once you've set up and run mongocryptd, first you need to create a new encryption key as follows.
+Keep in mind that the following example is a simple example to help you get started.
+The encryption key in the following example is insecure; MongoDB recommends using a [KMS](https://www.mongodb.com/docs/v5.0/core/security-client-side-encryption-key-management/).
+
+```javascript
+const { ClientEncryption } = require('mongodb-client-encryption');
+const mongoose = require('mongoose');
+const { Binary } = require('mongodb');
+
+run().catch(err => console.log(err));
+
+async function run() {
+ /* Step 1: Connect to MongoDB and insert a key */
+
+ // Create a very basic key. You're responsible for making
+ // your key secure, don't use this in prod :)
+ const arr = [];
+ for (let i = 0; i < 96; ++i) {
+ arr.push(i);
+ }
+ const key = Buffer.from(arr);
+
+ const keyVaultNamespace = 'client.encryption';
+ const kmsProviders = { local: { key } };
+
+ const uri = 'mongodb://127.0.0.1:27017/mongoose_test';
+ const conn = await mongoose.createConnection(uri, {
+ autoEncryption: {
+ keyVaultNamespace,
+ kmsProviders
+ }
+ }).asPromise();
+ const encryption = new ClientEncryption(conn.client, {
+ keyVaultNamespace,
+ kmsProviders,
+ });
+
+ const _key = await encryption.createDataKey('local');
+}
+```
+
+Once you have an encryption key, you can create a separate Mongoose connection with a [`schemaMap`](https://mongodb.github.io/node-mongodb-native/5.6/interfaces/AutoEncryptionOptions.html#schemaMap) that defines which fields are encrypted using JSON schema syntax as follows.
+
+```javascript
+/* Step 2: connect using schema map and new key */
+await mongoose.connect('mongodb://127.0.0.1:27017/mongoose_test', {
+ // Configure auto encryption
+ autoEncryption: {
+ keyVaultNamespace,
+ kmsProviders,
+ schemaMap: {
+ 'mongoose_test.tests': {
+ bsonType: 'object',
+ encryptMetadata: {
+ keyId: [_key]
+ },
+ properties: {
+ name: {
+ encrypt: {
+ bsonType: 'string',
+ algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'
+ }
+ }
+ }
+ }
+ }
+ }
+});
+```
+
+With the above connection, if you create a model named 'Test' that uses the 'tests' collection, any documents will have their `name` property encrypted.
+
+```javascript
+// 'super secret' will be stored as 'BinData' in the database,
+// if you query using the `mongo` shell.
+const Model = mongoose.model('Test', mongoose.Schema({ name: String }));
+await Model.create({ name: 'super secret' });
+```
diff --git a/docs/further_reading.md b/docs/further_reading.md
index 68ebf0fcc0a..e20ac3fba1b 100644
--- a/docs/further_reading.md
+++ b/docs/further_reading.md
@@ -1,11 +1,6 @@
# Further Reading
-_First be sure you have [MongoDB](http://www.mongodb.org/downloads) and [Node.js](http://nodejs.org/) installed._
+*First be sure you have [MongoDB](https://www.mongodb.com/try/download/community) and [Node.js](http://nodejs.org/en/download) installed.*
Next install Mongoose from the command line using `npm`:
-```
-$ npm install mongoose --save
+```sh
+npm install mongoose --save
```
Now say we like fuzzy kittens and want to record every kitten we ever meet
diff --git a/docs/jest.md b/docs/jest.md
index 03c6aa9741c..aa1669ef329 100644
--- a/docs/jest.md
+++ b/docs/jest.md
@@ -6,15 +6,15 @@ We strongly recommend using a different testing framework, like [Mocha](https://
To suppress any Jest warnings from Mongoose, set the `SUPPRESS_JEST_WARNINGS` environment variable:
-```
+```sh
env SUPPRESS_JEST_WARNINGS=1 npm test
```
If you choose to delve into dangerous waters and test Mongoose apps with Jest, here's what you need to know:
-
+## Recommended `testEnvironment` {#recommended-testenvironment}
-If you are using Jest `<=26`, do **not** use Jest's default [`jsdom` test environment](https://jestjs.io/docs/en/configuration.html#testenvironment-string) when testing Mongoose apps, _unless_ you are explicitly testing an application that only uses [Mongoose's browser library](browser.html). In Jest `>=27`, ["node" is Jest's default `testEnvironment`](https://jestjs.io/ro/blog/2021/05/25/jest-27#flipping-defaults), so this is no longer an issue.
+If you are using Jest `<=26`, do **not** use Jest's default [`jsdom` test environment](https://jestjs.io/docs/en/configuration.html#testenvironment-string) when testing Mongoose apps, *unless* you are explicitly testing an application that only uses [Mongoose's browser library](browser.html). In Jest `>=27`, ["node" is Jest's default `testEnvironment`](https://jestjs.io/ro/blog/2021/05/25/jest-27#flipping-defaults), so this is no longer an issue.
The `jsdom` test environment attempts to create a browser-like test
environment in Node.js, and it comes with numerous nasty surprises like a
@@ -32,7 +32,7 @@ module.exports = {
};
```
-
+## Timer Mocks {#timer-mocks}
Absolutely do **not** use [timer mocks](https://jestjs.io/docs/en/timer-mocks.html) when testing Mongoose apps.
This is especially important if you're using Jest `>=25`, which stubs out `process.nextTick()`.
@@ -71,7 +71,7 @@ const sinon = require('sinon');
sinon.stub(time, 'setTimeout');
```
-
+## `globalSetup` and `globalTeardown` {#globalsetup-and-globalteardown}
Do **not** use `globalSetup` to call `mongoose.connect()` or
`mongoose.createConnection()`. Jest runs `globalSetup` in
@@ -86,5 +86,5 @@ Want to learn how to test Mongoose apps correctly? The
course on Pluralsight has a great section on testing Mongoose apps with [Mocha](http://npmjs.com/package/mocha).
-
+
diff --git a/docs/js/convert-old-anchorid.js b/docs/js/convert-old-anchorid.js
new file mode 100644
index 00000000000..5f651092f6e
--- /dev/null
+++ b/docs/js/convert-old-anchorid.js
@@ -0,0 +1,92 @@
+'use strict';
+
+window.addEventListener('DOMContentLoaded', () => {
+ const anchor = window.location.hash;
+
+ // only operate on the old id's
+ if (!/^#\w+_\w+(?:-\w+)?$/i.test(anchor)) {
+ return fixNoAsyncFn();
+ }
+
+ // in case there is no anchor, return without modifying the anchor
+ if (!anchor) {
+ return;
+ }
+
+ const splits = anchor.split('_');
+
+ // dont modify anything if the splits are not "2"
+ if (splits.length !== 2) {
+ return;
+ }
+
+ const secondSplits = splits[1].split('-');
+
+ let mainName;
+ let propName = '';
+
+ // there are 2 possibilities:
+ // "mongoose_Mongoose-property"
+ // "mongoose_property"
+ if (secondSplits.length === 2) {
+ mainName = secondSplits[0];
+ propName = secondSplits[1];
+ } else {
+ // use the part after the "_" directly, because the before is just a lower-cased version
+ mainName = splits[1];
+ propName = '';
+ }
+
+ // check to ensure those properties are not empty
+ if (!mainName) {
+ return;
+ }
+
+ let tests = [];
+
+ // have to use multiple tests, because the old version did not differentiate between functions and properties
+ if (mainName && propName) {
+ // have to double the tests here, because the old version did not differentiate between static and instance properties
+ tests = [
+ `${mainName}.${propName}`,
+ `${mainName}.${propName}()`,
+
+ `${mainName}.prototype.${propName}`,
+ `${mainName}.prototype.${propName}()`
+ ];
+ } else {
+ tests = [
+ mainName,
+ `${mainName}()`
+ ];
+ }
+
+ for (const test of tests) {
+ // have to use the "[id=]" selector because "#Something()" is not a valid selector (the "()" part)
+ const header = document.querySelector(`h3[id="${test}"]`);
+ if (header) {
+ window.location.hash = `#${test}`;
+ }
+ }
+
+ // function to fix dox not recognizing async functions and resulting in inproper anchors
+ function fixNoAsyncFn() {
+ const anchorSlice = anchor.slice(1);
+ // dont modify anchor if it already exists
+ if (document.querySelector(`h3[id="${anchorSlice}"`)) {
+ return;
+ }
+
+ const tests = [
+ `${anchorSlice}()`
+ ];
+
+ for (const test of tests) {
+ // have to use the "[id=]" selector because "#Something()" is not a valid selector (the "()" part)
+ const header = document.querySelector(`h3[id="${test}"]`);
+ if (header) {
+ window.location.hash = `#${test}`;
+ }
+ }
+ }
+}, { once: true });
diff --git a/docs/js/navbar-search.js b/docs/js/navbar-search.js
index a9ff5fb8d7a..c117663fea5 100644
--- a/docs/js/navbar-search.js
+++ b/docs/js/navbar-search.js
@@ -5,6 +5,11 @@
const searchPrefix = versionFromUrl ? '/docs/' + version + '/docs/' : '/docs/';
+ // dont use nav-bar search for search site, let the search site handle that
+ if (/\/search(:?\.html)?$/i.test(window.location.pathname)) {
+ return;
+ }
+
document.getElementById('search-button-nav').onclick = function() {
const q = document.getElementById('search-input-nav').value;
window.location.href = searchPrefix + 'search.html?q=' + encodeURIComponent(q);
diff --git a/docs/js/redirect-old-api.js b/docs/js/redirect-old-api.js
new file mode 100644
index 00000000000..970735dd778
--- /dev/null
+++ b/docs/js/redirect-old-api.js
@@ -0,0 +1,30 @@
+'use strict';
+window.addEventListener('DOMContentLoaded', () => {
+ const anchor = window.location.hash;
+
+ // in case there is not anchor
+ if (!anchor) {
+ return redirectToBase();
+ }
+
+ const firstName = anchor.split('_')[0];
+
+ // in case there is no split
+ if (!firstName) {
+ return redirectToBase();
+ }
+
+ const sliced = firstName.slice(1).toLowerCase(); // ignore first character, which will always be "#"
+
+ // in case everything after "#" is empty
+ if (!sliced) {
+ return redirectToBase();
+ }
+
+ window.location.replace('./api/' + sliced + '.html' + anchor);
+}, { once: true });
+
+// helper function to redirect in case no other redirect can be found
+function redirectToBase() {
+ window.location.replace('./api/mongoose.html');
+}
diff --git a/docs/js/search.js b/docs/js/search.js
index a00f4c046c6..b21a5eaf3cd 100644
--- a/docs/js/search.js
+++ b/docs/js/search.js
@@ -1,27 +1,68 @@
'use strict';
-const root = 'https://mongoosejs.azurewebsites.net/api';
-const pairs = window.location.search.replace(/^\?/, '').split('&');
-
-let q = null;
-for (let i = 0; i < pairs.length; ++i) {
- const _pair = pairs[i].split('=');
- if (_pair[0] === 'q') {
- q = _pair[1];
- }
-}
-const defaultVersion = '6.x';
+const root = 'https://mongoose-js.netlify.app/.netlify/functions';
+
+const defaultVersion = '8.x';
const versionFromUrl = window.location.pathname.match(/^\/docs\/(\d+\.x)/);
const version = versionFromUrl ? versionFromUrl[1] : defaultVersion;
-if (q != null) {
- document.getElementById('search-input').value = decodeURIComponent(q);
- fetch(root + '/search?search=' + q + '&version=' + version).
+search();
+
+document.getElementById('search-button').onclick = function() {
+ addHistory(document.getElementById('search-input').value);
+};
+
+document.getElementById('search-input').onkeyup = function(ev) {
+ if (ev.keyCode === 13) {
+ addHistory(document.getElementById('search-input').value);
+ }
+};
+
+document.getElementById('search-button-nav').onclick = function() {
+ addHistory(document.getElementById('search-input-nav').value);
+};
+
+document.getElementById('search-input-nav').onkeyup = function(ev) {
+ if (ev.keyCode === 13) {
+ addHistory(document.getElementById('search-input-nav').value);
+ }
+};
+
+/** Helper to consistently add history and reload results */
+function addHistory(value) {
+ const url = new URL(window.location.href);
+
+ // use this to only modify the param "q" and not overwrite any other existing params
+ url.searchParams.set('q', value);
+
+ window.history.pushState({}, '', url);
+ search();
+}
+
+/** (re)load results */
+function search() {
+ const resultsDiv = document.getElementById('results');
+
+ resultsDiv.innerHTML = 'Loading...
No Search Parameters
';
+ return;
+ }
+
+ const qSearch = url.searchParams.get('q');
+
+ document.getElementById('search-input').value = qSearch;
+ document.getElementById('search-input-nav').value = ''; // set navbar search empty, to encourage big input usage
+
+ fetch(root + '/search?search=' + encodeURIComponent(qSearch) + '&version=' + version).
then(function(res) { return res.json(); }).
then(
function(result) {
if (result.results.length === 0) {
- document.getElementById('results').innerHTML = 'No Results ';
+ resultsDiv.innerHTML = 'No Results ';
return;
}
let html = '';
@@ -37,23 +78,11 @@ if (q != null) {
'';
}
- document.getElementById('results').innerHTML = '';
+ resultsDiv.innerHTML = '';
},
function(error) {
- document.getElementById('results').innerHTML =
+ resultsDiv.innerHTML =
'An error occurred: ' + error.message + ' ';
}
);
}
-
-document.getElementById('search-button').onclick = function() {
- const q = document.getElementById('search-input').value;
- window.location.href = 'search.html?q=' + encodeURIComponent(q);
-};
-
-q = document.getElementById('search-input').onkeyup = function(ev) {
- if (ev.keyCode === 13) {
- const q = document.getElementById('search-input').value;
- window.location.href = 'search.html?q=' + encodeURIComponent(q);
- }
-};
diff --git a/docs/layout.pug b/docs/layout.pug
index 74538b02650..16dd2cf9257 100644
--- a/docs/layout.pug
+++ b/docs/layout.pug
@@ -12,21 +12,8 @@ html(lang='en')
link(rel="stylesheet", href="https://fonts.googleapis.com/css?family=Open+Sans")
link(rel="stylesheet", href=`${versions.versionedPath}/docs/css/github.css`)
link(rel="stylesheet", href=`${versions.versionedPath}/docs/css/mongoose5.css`)
+ link(rel="stylesheet", href=`${versions.versionedPath}/docs/css/carbonads.css`)
- link(rel='apple-touch-icon', sizes='57x57', href='images/favicon/apple-icon-57x57.png')
- link(rel='apple-touch-icon', sizes='60x60', href='images/favicon/apple-icon-60x60.png')
- link(rel='apple-touch-icon', sizes='72x72', href='images/favicon/apple-icon-72x72.png')
- link(rel='apple-touch-icon', sizes='76x76', href='images/favicon/apple-icon-76x76.png')
- link(rel='apple-touch-icon', sizes='114x114', href='images/favicon/apple-icon-114x114.png')
- link(rel='apple-touch-icon', sizes='120x120', href='images/favicon/apple-icon-120x120.png')
- link(rel='apple-touch-icon', sizes='144x144', href='images/favicon/apple-icon-144x144.png')
- link(rel='apple-touch-icon', sizes='152x152', href='images/favicon/apple-icon-152x152.png')
- link(rel='apple-touch-icon', sizes='180x180', href='images/favicon/apple-icon-180x180.png')
- link(rel='icon', type='image/png', sizes='192x192', href='images/favicon/android-icon-192x192.png')
- link(rel='icon', type='image/png', sizes='32x32', href='images/favicon/favicon-32x32.png')
- link(rel='icon', type='image/png', sizes='96x96', href='images/favicon/favicon-96x96.png')
- link(rel='icon', type='image/png', sizes='16x16', href='images/favicon/favicon-16x16.png')
- link(rel='manifest', href='images/favicon/manifest.json')
meta(name='msapplication-TileColor', content='#ffffff')
meta(name='msapplication-TileImage', content=`${versions.versionedPath}/docs/images/favicon/ms-icon-144x144.png`)
meta(name='theme-color', content='#ffffff')
@@ -36,13 +23,13 @@ html(lang='en')
#layout
#mobile-menu
a#menuLink.menu-link(href='#menu')
- span
+
#mobile-logo-container
a(href="/")
img#logo(src=`${versions.versionedPath}/docs/images/mongoose5_62x30_transparent.png`)
span.logo-text mongoose
#menu
- .pure-menu
+ nav.pure-menu
#logo-container.pure-menu-heading
a(href="/")
img#logo(src=`${versions.versionedPath}/docs/images/mongoose5_62x30_transparent.png`)
@@ -56,10 +43,7 @@ html(lang='en')
a.pure-menu-link(href=`${versions.latestVersion.path}/docs/index.html`) Version #{versions.latestVersion.listed}
each pastVersion in versions.pastVersions
li.pure-menu-item
- - if ((pastVersion.listed != versions.currentVersion.listed) || versions.versionedDeploy)
- a.pure-menu-link(href=`/docs/${pastVersion.path}/index.html`) Version #{pastVersion.listed}
- - else
- a.pure-menu-link(href=`/docs/index.html`) Version #{pastVersion.listed}
+ a.pure-menu-link(href=`/docs/${pastVersion.path}/index.html`) Version #{pastVersion.listed}
li.pure-menu-item.search
input#search-input-nav(type="text", placeholder="Search")
button#search-button-nav
@@ -68,88 +52,94 @@ html(lang='en')
a.pure-menu-link(href=`${versions.versionedPath}/docs/index.html`, class=outputUrl === `${versions.versionedPath}/docs/index.html` ? 'selected' : '') Quick Start
li.pure-menu-item
a.pure-menu-link(href=`${versions.versionedPath}/docs/guides.html`, class=outputUrl === `${versions.versionedPath}/docs/guides.html` ? 'selected' : '') Guides
- li.pure-menu-item.sub-item
- a.pure-menu-link(href=`${versions.versionedPath}/docs/guide.html`, class=outputUrl === `${versions.versionedPath}/docs/guide.html` ? 'selected' : '') Schemas
- li.pure-menu-item.sub-item
- a.pure-menu-link(href=`${versions.versionedPath}/docs/schematypes.html`, class=outputUrl === `${versions.versionedPath}/docs/schematypes.html` ? 'selected' : '') SchemaTypes
- li.pure-menu-item.sub-item
- a.pure-menu-link(href=`${versions.versionedPath}/docs/connections.html`, class=outputUrl === `${versions.versionedPath}/docs/connections.html` ? 'selected' : '') Connections
- - if ([`${versions.versionedPath}/docs/connections`, `${versions.versionedPath}/docs/tutorials/ssl`].some(path => outputUrl.startsWith(path)))
- li.pure-menu-item.tertiary-item
- a.pure-menu-link(href=`${versions.versionedPath}/docs/tutorials/ssl.html`, class=outputUrl === `${versions.versionedPath}/docs/tutorials/ssl.html` ? 'selected' : '') SSL Connections
- li.pure-menu-item.sub-item
- a.pure-menu-link(href=`${versions.versionedPath}/docs/models.html`, class=outputUrl === `${versions.versionedPath}/docs/models.html` ? 'selected' : '') Models
- - if ([`${versions.versionedPath}/docs/models`, `${versions.versionedPath}/docs/change-streams`].some(path => outputUrl.startsWith(path)))
- li.pure-menu-item.tertiary-item
- a.pure-menu-link(href=`${versions.versionedPath}/docs/change-streams.html`, class=outputUrl === `${versions.versionedPath}/docs/change-streams.html` ? 'selected' : '') Change Streams
- li.pure-menu-item.sub-item
- a.pure-menu-link(href=`${versions.versionedPath}/docs/documents.html`, class=outputUrl === `${versions.versionedPath}/docs/documents.html` ? 'selected' : '') Documents
- li.pure-menu-item.sub-item
- a.pure-menu-link(href=`${versions.versionedPath}/docs/subdocs.html`, class=outputUrl === `${versions.versionedPath}/docs/subdocs.html` ? 'selected' : '') Subdocuments
- li.pure-menu-item.sub-item
- a.pure-menu-link(href=`${versions.versionedPath}/docs/queries.html`, class=outputUrl === `${versions.versionedPath}/docs/queries.html` ? 'selected' : '') Queries
- - if ([`${versions.versionedPath}/docs/queries`, `${versions.versionedPath}/docs/tutorials/findoneandupdate`, `${versions.versionedPath}/docs/tutorials/lean`, `${versions.versionedPath}/docs/tutorials/query_casting`].some(path => outputUrl.startsWith(path)))
- li.pure-menu-item.tertiary-item
- a.pure-menu-link(href=`${versions.versionedPath}/docs/tutorials/query_casting.html`, class=outputUrl === `${versions.versionedPath}/docs/tutorials/query_casting.html` ? 'selected' : '') Query Casting
- li.pure-menu-item.tertiary-item
- a.pure-menu-link(href=`${versions.versionedPath}/docs/tutorials/findoneandupdate.html`, class=outputUrl === `${versions.versionedPath}/docs/tutorials/findoneandupdate.html` ? 'selected' : '') findOneAndUpdate
- li.pure-menu-item.tertiary-item
- a.pure-menu-link(href=`${versions.versionedPath}/docs/tutorials/lean.html`, class=outputUrl === `${versions.versionedPath}/docs/tutorials/lean.html` ? 'selected' : '') The Lean Option
- li.pure-menu-item.sub-item
- a.pure-menu-link(href=`${versions.versionedPath}/docs/validation.html`, class=outputUrl === `${versions.versionedPath}/docs/validation.html` ? 'selected' : '') Validation
- li.pure-menu-item.sub-item
- a.pure-menu-link(href=`${versions.versionedPath}/docs/middleware.html`, class=outputUrl === `${versions.versionedPath}/docs/middleware.html` ? 'selected' : '') Middleware
- li.pure-menu-item.sub-item
- a.pure-menu-link(href=`${versions.versionedPath}/docs/populate.html`, class=outputUrl === `${versions.versionedPath}/docs/populate.html` ? 'selected' : '') Populate
- li.pure-menu-item.sub-item
- a.pure-menu-link(href=`${versions.versionedPath}/docs/discriminators.html`, class=outputUrl === `${versions.versionedPath}/docs/discriminators.html` ? 'selected' : '') Discriminators
- li.pure-menu-item.sub-item
- a.pure-menu-link(href=`${versions.versionedPath}/docs/plugins.html`, class=outputUrl === `${versions.versionedPath}/docs/plugins.html` ? 'selected' : '') Plugins
- li.pure-menu-item.sub-item
- a.pure-menu-link(href=`${versions.versionedPath}/docs/timestamps.html`, class=outputUrl === `${versions.versionedPath}/docs/timestamps.html` ? 'selected' : '') Timestamps
- li.pure-menu-item.sub-item
- a.pure-menu-link(href=`${versions.versionedPath}/docs/transactions.html`, class=outputUrl === `${versions.versionedPath}/docs/transactions.html` ? 'selected' : '') Transactions
- li.pure-menu-item.sub-item
- a.pure-menu-link(href=`${versions.versionedPath}/docs/typescript.html`, class=outputUrl === `${versions.versionedPath}/docs/typescript.html` ? 'selected' : '') TypeScript
- - if (outputUrl.startsWith(`${versions.versionedPath}/docs/typescript`))
- li.pure-menu-item.tertiary-item
- a.pure-menu-link(href=`${versions.versionedPath}/docs/typescript/schemas.html`, class=outputUrl === `${versions.versionedPath}/docs/typescript/schemas.html` ? 'selected' : '') Schemas
- li.pure-menu-item.tertiary-item
- a.pure-menu-link(href=`${versions.versionedPath}/docs/typescript/statics-and-methods.html`, class=outputUrl === `${versions.versionedPath}/docs/typescript/statics-and-methods.html` ? 'selected' : '') Statics
- li.pure-menu-item.tertiary-item
- a.pure-menu-link(href=`${versions.versionedPath}/docs/typescript/query-helpers.html`, class=outputUrl === `${versions.versionedPath}/docs/typescript/query-helpers.html` ? 'selected' : '') Query Helpers
- li.pure-menu-item.tertiary-item
- a.pure-menu-link(href=`${versions.versionedPath}/docs/typescript/populate.html`, class=outputUrl === `${versions.versionedPath}/docs/typescript/populate.html` ? 'selected' : '') Populate
- li.pure-menu-item.tertiary-item
- a.pure-menu-link(href=`${versions.versionedPath}/docs/typescript/subdocuments.html`, class=outputUrl === `${versions.versionedPath}/docs/typescript/subdocuments.html` ? 'selected' : '') Subdocuments
+ ul.pure-menu-list
+ li.pure-menu-item.sub-item
+ a.pure-menu-link(href=`${versions.versionedPath}/docs/guide.html`, class=outputUrl === `${versions.versionedPath}/docs/guide.html` ? 'selected' : '') Schemas
+ li.pure-menu-item.sub-item
+ a.pure-menu-link(href=`${versions.versionedPath}/docs/schematypes.html`, class=outputUrl === `${versions.versionedPath}/docs/schematypes.html` ? 'selected' : '') SchemaTypes
+ li.pure-menu-item.sub-item
+ a.pure-menu-link(href=`${versions.versionedPath}/docs/connections.html`, class=outputUrl === `${versions.versionedPath}/docs/connections.html` ? 'selected' : '') Connections
+ - if ([`${versions.versionedPath}/docs/connections`, `${versions.versionedPath}/docs/tutorials/ssl`].some(path => outputUrl.startsWith(path)))
+ ul.pure-menu-list
+ li.pure-menu-item.sub-item
+ a.pure-menu-link(href=`${versions.versionedPath}/docs/tutorials/ssl.html`, class=outputUrl === `${versions.versionedPath}/docs/tutorials/ssl.html` ? 'selected' : '') TLS/SSL Connections
+ li.pure-menu-item.sub-item
+ a.pure-menu-link(href=`${versions.versionedPath}/docs/models.html`, class=outputUrl === `${versions.versionedPath}/docs/models.html` ? 'selected' : '') Models
+ - if ([`${versions.versionedPath}/docs/models`, `${versions.versionedPath}/docs/change-streams`].some(path => outputUrl.startsWith(path)))
+ ul.pure-menu-list
+ li.pure-menu-item.sub-item
+ a.pure-menu-link(href=`${versions.versionedPath}/docs/change-streams.html`, class=outputUrl === `${versions.versionedPath}/docs/change-streams.html` ? 'selected' : '') Change Streams
+ li.pure-menu-item.sub-item
+ a.pure-menu-link(href=`${versions.versionedPath}/docs/documents.html`, class=outputUrl === `${versions.versionedPath}/docs/documents.html` ? 'selected' : '') Documents
+ li.pure-menu-item.sub-item
+ a.pure-menu-link(href=`${versions.versionedPath}/docs/subdocs.html`, class=outputUrl === `${versions.versionedPath}/docs/subdocs.html` ? 'selected' : '') Subdocuments
+ li.pure-menu-item.sub-item
+ a.pure-menu-link(href=`${versions.versionedPath}/docs/queries.html`, class=outputUrl === `${versions.versionedPath}/docs/queries.html` ? 'selected' : '') Queries
+ - if ([`${versions.versionedPath}/docs/queries`, `${versions.versionedPath}/docs/tutorials/findoneandupdate`, `${versions.versionedPath}/docs/tutorials/lean`, `${versions.versionedPath}/docs/tutorials/query_casting`].some(path => outputUrl.startsWith(path)))
+ ul.pure-menu-list
+ li.pure-menu-item.sub-item
+ a.pure-menu-link(href=`${versions.versionedPath}/docs/tutorials/query_casting.html`, class=outputUrl === `${versions.versionedPath}/docs/tutorials/query_casting.html` ? 'selected' : '') Query Casting
+ li.pure-menu-item.sub-item
+ a.pure-menu-link(href=`${versions.versionedPath}/docs/tutorials/findoneandupdate.html`, class=outputUrl === `${versions.versionedPath}/docs/tutorials/findoneandupdate.html` ? 'selected' : '') findOneAndUpdate
+ li.pure-menu-item.sub-item
+ a.pure-menu-link(href=`${versions.versionedPath}/docs/tutorials/lean.html`, class=outputUrl === `${versions.versionedPath}/docs/tutorials/lean.html` ? 'selected' : '') The Lean Option
+ li.pure-menu-item.sub-item
+ a.pure-menu-link(href=`${versions.versionedPath}/docs/validation.html`, class=outputUrl === `${versions.versionedPath}/docs/validation.html` ? 'selected' : '') Validation
+ li.pure-menu-item.sub-item
+ a.pure-menu-link(href=`${versions.versionedPath}/docs/middleware.html`, class=outputUrl === `${versions.versionedPath}/docs/middleware.html` ? 'selected' : '') Middleware
+ li.pure-menu-item.sub-item
+ a.pure-menu-link(href=`${versions.versionedPath}/docs/populate.html`, class=outputUrl === `${versions.versionedPath}/docs/populate.html` ? 'selected' : '') Populate
+ li.pure-menu-item.sub-item
+ a.pure-menu-link(href=`${versions.versionedPath}/docs/discriminators.html`, class=outputUrl === `${versions.versionedPath}/docs/discriminators.html` ? 'selected' : '') Discriminators
+ li.pure-menu-item.sub-item
+ a.pure-menu-link(href=`${versions.versionedPath}/docs/plugins.html`, class=outputUrl === `${versions.versionedPath}/docs/plugins.html` ? 'selected' : '') Plugins
+ li.pure-menu-item.sub-item
+ a.pure-menu-link(href=`${versions.versionedPath}/docs/timestamps.html`, class=outputUrl === `${versions.versionedPath}/docs/timestamps.html` ? 'selected' : '') Timestamps
+ li.pure-menu-item.sub-item
+ a.pure-menu-link(href=`${versions.versionedPath}/docs/transactions.html`, class=outputUrl === `${versions.versionedPath}/docs/transactions.html` ? 'selected' : '') Transactions
+ li.pure-menu-item.sub-item
+ a.pure-menu-link(href=`${versions.versionedPath}/docs/typescript.html`, class=outputUrl === `${versions.versionedPath}/docs/typescript.html` ? 'selected' : '') TypeScript
+ - if (outputUrl.startsWith(`${versions.versionedPath}/docs/typescript`))
+ ul.pure-menu-list
+ li.pure-menu-item.sub-item
+ a.pure-menu-link(href=`${versions.versionedPath}/docs/typescript/schemas.html`, class=outputUrl === `${versions.versionedPath}/docs/typescript/schemas.html` ? 'selected' : '') Schemas
+ li.pure-menu-item.sub-item
+ a.pure-menu-link(href=`${versions.versionedPath}/docs/typescript/statics-and-methods.html`, class=outputUrl === `${versions.versionedPath}/docs/typescript/statics-and-methods.html` ? 'selected' : '') Statics and Methods
+ li.pure-menu-item.sub-item
+ a.pure-menu-link(href=`${versions.versionedPath}/docs/typescript/query-helpers.html`, class=outputUrl === `${versions.versionedPath}/docs/typescript/query-helpers.html` ? 'selected' : '') Query Helpers
+ li.pure-menu-item.sub-item
+ a.pure-menu-link(href=`${versions.versionedPath}/docs/typescript/populate.html`, class=outputUrl === `${versions.versionedPath}/docs/typescript/populate.html` ? 'selected' : '') Populate
+ li.pure-menu-item.sub-item
+ a.pure-menu-link(href=`${versions.versionedPath}/docs/typescript/subdocuments.html`, class=outputUrl === `${versions.versionedPath}/docs/typescript/subdocuments.html` ? 'selected' : '') Subdocuments
li.pure-menu-item
a.pure-menu-link(href=`${versions.versionedPath}/docs/api/mongoose.html`, class=outputUrl === `${versions.versionedPath}/docs/api/mongoose.html` ? 'selected' : '') API
- li.pure-menu-item.sub-item
- a.pure-menu-link(href=`${versions.versionedPath}/docs/api/mongoose.html`, class=outputUrl === `${versions.versionedPath}/docs/api/mongoose.html` ? 'selected' : '') Mongoose
- li.pure-menu-item.sub-item
- a.pure-menu-link(href=`${versions.versionedPath}/docs/api/schema.html`, class=outputUrl === `${versions.versionedPath}/docs/api/schema.html` ? 'selected' : '') Schema
- li.pure-menu-item.sub-item
- a.pure-menu-link(href=`${versions.versionedPath}/docs/api/connection.html`, class=outputUrl === `${versions.versionedPath}/docs/api/connection.html` ? 'selected' : '') Connection
- li.pure-menu-item.sub-item
- a.pure-menu-link(href=`${versions.versionedPath}/docs/api/document.html`, class=outputUrl === `${versions.versionedPath}/docs/api/document.html` ? 'selected' : '') Document
- li.pure-menu-item.sub-item
- a.pure-menu-link(href=`${versions.versionedPath}/docs/api/model.html`, class=outputUrl === `${versions.versionedPath}/docs/api/model.html` ? 'selected' : '') Model
- li.pure-menu-item.sub-item
- a.pure-menu-link(href=`${versions.versionedPath}/docs/api/query.html`, class=outputUrl === `${versions.versionedPath}/docs/api/query.html` ? 'selected' : '') Query
- li.pure-menu-item.sub-item
- a.pure-menu-link(href=`${versions.versionedPath}/docs/api/aggregate.html`, class=outputUrl === `${versions.versionedPath}/docs/api/aggregate.html` ? 'selected' : '') Aggregate
- li.pure-menu-item.sub-item
- a.pure-menu-link(href=`${versions.versionedPath}/docs/api/schematype.html`, class=outputUrl === `${versions.versionedPath}/docs/api/schematype.html` ? 'selected' : '') SchemaType
- li.pure-menu-item.sub-item
- a.pure-menu-link(href=`${versions.versionedPath}/docs/api/virtualtype.html`, class=outputUrl === `${versions.versionedPath}/docs/api/virtualtype.html` ? 'selected' : '') VirtualType
+ ul.pure-menu-list
+ li.pure-menu-item.sub-item
+ a.pure-menu-link(href=`${versions.versionedPath}/docs/api/mongoose.html`, class=outputUrl === `${versions.versionedPath}/docs/api/mongoose.html` ? 'selected' : '') Mongoose
+ li.pure-menu-item.sub-item
+ a.pure-menu-link(href=`${versions.versionedPath}/docs/api/schema.html`, class=outputUrl === `${versions.versionedPath}/docs/api/schema.html` ? 'selected' : '') Schema
+ li.pure-menu-item.sub-item
+ a.pure-menu-link(href=`${versions.versionedPath}/docs/api/connection.html`, class=outputUrl === `${versions.versionedPath}/docs/api/connection.html` ? 'selected' : '') Connection
+ li.pure-menu-item.sub-item
+ a.pure-menu-link(href=`${versions.versionedPath}/docs/api/document.html`, class=outputUrl === `${versions.versionedPath}/docs/api/document.html` ? 'selected' : '') Document
+ li.pure-menu-item.sub-item
+ a.pure-menu-link(href=`${versions.versionedPath}/docs/api/model.html`, class=outputUrl === `${versions.versionedPath}/docs/api/model.html` ? 'selected' : '') Model
+ li.pure-menu-item.sub-item
+ a.pure-menu-link(href=`${versions.versionedPath}/docs/api/query.html`, class=outputUrl === `${versions.versionedPath}/docs/api/query.html` ? 'selected' : '') Query
+ li.pure-menu-item.sub-item
+ a.pure-menu-link(href=`${versions.versionedPath}/docs/api/aggregate.html`, class=outputUrl === `${versions.versionedPath}/docs/api/aggregate.html` ? 'selected' : '') Aggregate
+ li.pure-menu-item.sub-item
+ a.pure-menu-link(href=`${versions.versionedPath}/docs/api/schematype.html`, class=outputUrl === `${versions.versionedPath}/docs/api/schematype.html` ? 'selected' : '') SchemaType
+ li.pure-menu-item.sub-item
+ a.pure-menu-link(href=`${versions.versionedPath}/docs/api/virtualtype.html`, class=outputUrl === `${versions.versionedPath}/docs/api/virtualtype.html` ? 'selected' : '') VirtualType
li.pure-menu-item
- a.pure-menu-link(href=`${versions.versionedPath}/docs/migrating_to_6.html`, class=outputUrl === `${versions.versionedPath}/docs/migrating_to_7.html` ? 'selected' : '') Migration Guide
+ a.pure-menu-link(href=`${versions.versionedPath}/docs/migrating_to_8.html`, class=outputUrl === `${versions.versionedPath}/docs/migrating_to_8.html` ? 'selected' : '') Migration Guide
li.pure-menu-item
a.pure-menu-link(href=`${versions.versionedPath}/docs/compatibility.html`, class=outputUrl === `${versions.versionedPath}/docs/compatibility.html` ? 'selected' : '') Version Compatibility
- li.pure-menu-item
- a.pure-menu-link(href=`${versions.versionedPath}/docs/faq.html`, class=outputUrl === `${versions.versionedPath}/docs/faq.html` ? 'selected' : '') FAQ
li.pure-menu-item
a.pure-menu-link(href=`${versions.versionedPath}/docs/version-support.html`, class=outputUrl === `${versions.versionedPath}/docs/version-support.html` ? 'selected' : '') Version Support
+ li.pure-menu-item
+ a.pure-menu-link(href=`${versions.versionedPath}/docs/faq.html`, class=outputUrl === `${versions.versionedPath}/docs/faq.html` ? 'selected' : '') FAQ
li.pure-menu-item
a.pure-menu-link(href=`${versions.versionedPath}/docs/further_reading.html`, class=outputUrl === `${versions.versionedPath}/docs/further_reading.html` ? 'selected' : '') Further Reading
li.pure-menu-item
diff --git a/docs/lodash.md b/docs/lodash.md
index 3e7837920e3..1856e81cea5 100644
--- a/docs/lodash.md
+++ b/docs/lodash.md
@@ -8,7 +8,7 @@ However, there are a few caveats that you should know about.
## `cloneDeep()`
You should not use [Lodash's `cloneDeep()` function](https://lodash.com/docs/4.17.15#cloneDeep) on any Mongoose objects.
-This includes [connections](connections.html), [model classes](models.html), and [queries](queries.html), but is _especially_ important for [documents](documents.html).
+This includes [connections](connections.html), [model classes](models.html), and [queries](queries.html), but is *especially* important for [documents](documents.html).
For example, you may be tempted to do the following:
```javascript
diff --git a/docs/middleware.md b/docs/middleware.md
index f7b37301233..87381fad2e0 100644
--- a/docs/middleware.md
+++ b/docs/middleware.md
@@ -12,6 +12,7 @@ on the schema level and is useful for writing [plugins](plugins.html).
Asynchronous Post Hooks
Define Middleware Before Compiling Models
Save/Validate Hooks
+ Accessing Parameters in Middleware
Naming Conflicts
Notes on findAndUpdate() and Query Middleware
Error Handling Middleware
@@ -30,7 +31,6 @@ In document middleware functions, `this` refers to the document. To access the m
* [validate](api/document.html#document_Document-validate)
* [save](api/model.html#model_Model-save)
-* [remove](api/model.html#model_Model-remove)
* [updateOne](api/document.html#document_Document-updateOne)
* [deleteOne](api/model.html#model_Model-deleteOne)
* [init](api/document.html#document_Document-init) (note: init hooks are [synchronous](#synchronous))
@@ -47,12 +47,9 @@ In query middleware functions, `this` refers to the query.
* [find](api/query.html#query_Query-find)
* [findOne](api/query.html#query_Query-findOne)
* [findOneAndDelete](api/query.html#query_Query-findOneAndDelete)
-* [findOneAndRemove](api/query.html#query_Query-findOneAndRemove)
* [findOneAndReplace](api/query.html#query_Query-findOneAndReplace)
* [findOneAndUpdate](api/query.html#query_Query-findOneAndUpdate)
-* [remove](api/model.html#model_Model-remove)
* [replaceOne](api/query.html#query_Query-replaceOne)
-* [update](api/query.html#query_Query-update)
* [updateOne](api/query.html#query_Query-updateOne)
* [updateMany](api/query.html#query_Query-updateMany)
* [validate](validation.html#update-validators)
@@ -64,28 +61,30 @@ In aggregate middleware, `this` refers to the [aggregation object](api/model.htm
* [aggregate](api/model.html#model_Model-aggregate)
Model middleware is supported for the following model functions.
-Don't confuse model middleware and document middleware: model middleware hooks into _static_ functions on a `Model` class, document middleware hooks into _methods_ on a `Model` class.
+Don't confuse model middleware and document middleware: model middleware hooks into *static* functions on a `Model` class, document middleware hooks into *methods* on a `Model` class.
In model middleware functions, `this` refers to the model.
+* [bulkWrite](api/model.html#model_Model-bulkWrite)
+* [createCollection](api/model.html#model_Model-createCollection)
* [insertMany](api/model.html#model_Model-insertMany)
Here are the possible strings that can be passed to `pre()`
* aggregate
+* bulkWrite
* count
* countDocuments
+* createCollection
* deleteOne
* deleteMany
* estimatedDocumentCount
* find
* findOne
* findOneAndDelete
-* findOneAndRemove
* findOneAndReplace
* findOneAndUpdate
* init
* insertMany
-* remove
* replaceOne
* save
* update
@@ -96,17 +95,14 @@ Here are the possible strings that can be passed to `pre()`
All middleware types support pre and post hooks.
How pre and post hooks work is described in more detail below.
-**Note:** If you specify `schema.pre('remove')`, Mongoose will register this
-middleware for [`doc.remove()`](api/model.html#model_Model-remove) by default. If you
-want your middleware to run on [`Query.remove()`](api/query.html#query_Query-remove)
-use [`schema.pre('remove', { query: true, document: false }, fn)`](api/schema.html#schema_Schema-pre).
+**Note:** Mongoose registers `updateOne` middleware on `Query.prototype.updateOne()` by default.
+This means that both `doc.updateOne()` and `Model.updateOne()` trigger `updateOne` hooks, but `this` refers to a query, not a document.
+To register `updateOne` middleware as document middleware, use `schema.pre('updateOne', { document: true, query: false })`.
-**Note:** Unlike `schema.pre('remove')`, Mongoose registers `updateOne` and
-`deleteOne` middleware on `Query#updateOne()` and `Query#deleteOne()` by default.
-This means that both `doc.updateOne()` and `Model.updateOne()` trigger
-`updateOne` hooks, but `this` refers to a query, not a document. To register
-`updateOne` or `deleteOne` middleware as document middleware, use
-`schema.pre('updateOne', { document: true, query: false })`.
+**Note:** Like `updateOne`, Mongoose registers `deleteOne` middleware on `Query.prototype.deleteOne` by default.
+That means that `Model.deleteOne()` will trigger `deleteOne` hooks, and `this` will refer to a query.
+However, `doc.deleteOne()` does **not** fire `deleteOne` query middleware for legacy reasons.
+To register `deleteOne` middleware as document middleware, use `schema.pre('deleteOne', { document: true, query: false })`.
**Note:** The [`create()`](./api/model.html#model_Model-create) function fires `save()` hooks.
@@ -130,7 +126,7 @@ childSchema.pre('findOneAndUpdate', function() {
});
```
-
+## Pre {#pre}
Pre middleware functions are executed one after another, when each
middleware calls `next`.
@@ -152,7 +148,7 @@ schema.pre('save', function() {
then(() => doMoreStuff());
});
-// Or, in Node.js >= 7.6.0:
+// Or, using async functions
schema.pre('save', async function() {
await doStuff();
await doMoreStuff();
@@ -176,7 +172,7 @@ schema.pre('save', function(next) {
});
```
-
+### Use Cases
Middleware are useful for atomizing model logic. Here are some other ideas:
@@ -185,7 +181,7 @@ Middleware are useful for atomizing model logic. Here are some other ideas:
* asynchronous defaults
* asynchronous tasks that a certain action triggers
-
+### Errors in Pre Hooks {#error-handling}
If any pre hook errors out, mongoose will not execute subsequent middleware
or the hooked function. Mongoose will instead pass an error to the callback
@@ -229,9 +225,9 @@ myDoc.save(function(err) {
Calling `next()` multiple times is a no-op. If you call `next()` with an
error `err1` and then throw an error `err2`, mongoose will report `err1`.
-
+## Post middleware {#post}
-[post](api.html#schema_Schema-post) middleware are executed _after_
+[post](api.html#schema_Schema-post) middleware are executed *after*
the hooked method and all of its `pre` middleware have completed.
```javascript
@@ -244,16 +240,14 @@ schema.post('validate', function(doc) {
schema.post('save', function(doc) {
console.log('%s has been saved', doc._id);
});
-schema.post('remove', function(doc) {
- console.log('%s has been removed', doc._id);
+schema.post('deleteOne', function(doc) {
+ console.log('%s has been deleted', doc._id);
});
```
-
+## Asynchronous Post Hooks {#post-async}
-If your post hook function takes at least 2 parameters, mongoose will
-assume the second parameter is a `next()` function that you will call to
-trigger the next middleware in the sequence.
+If your post hook function takes at least 2 parameters, mongoose will assume the second parameter is a `next()` function that you will call to trigger the next middleware in the sequence.
```javascript
// Takes 2 parameters: this is an asynchronous post hook
@@ -272,7 +266,26 @@ schema.post('save', function(doc, next) {
});
```
-
+You can also pass an async function to `post()`.
+If you pass an async function that takes at least 2 parameters, you are still responsible for calling `next()`.
+However, you can also pass in an async function that takes less than 2 parameters, and Mongoose will wait for the promise to resolve.
+
+```javascript
+schema.post('save', async function(doc) {
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ console.log('post1');
+ // If less than 2 parameters, no need to call `next()`
+});
+
+schema.post('save', async function(doc, next) {
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ console.log('post1');
+ // If there's a `next` parameter, you need to call `next()`.
+ next();
+});
+```
+
+## Define Middleware Before Compiling Models {#defining}
Calling `pre()` or `post()` after [compiling a model](models.html#compiling)
does **not** work in Mongoose in general. For example, the below `pre('save')`
@@ -322,7 +335,7 @@ const schema = new mongoose.Schema({ name: String });
module.exports = mongoose.model('User', schema);
```
-
+## Save/Validate Hooks {#order}
The `save()` function triggers `validate()` hooks, because mongoose
has a built-in `pre('save')` hook that calls `validate()`. This means
@@ -344,40 +357,94 @@ schema.post('save', function() {
});
```
-
+## Accessing Parameters in Middleware {#accessing-parameters-in-middleware}
-Mongoose has both query and document hooks for `remove()`.
+Mongoose provides 2 ways to get information about the function call that triggered the middleware.
+For query middleware, we recommend using `this`, which will be a [Mongoose Query instance](api/query.html).
```javascript
-schema.pre('remove', function() { console.log('Removing!'); });
+const userSchema = new Schema({ name: String, age: Number });
+userSchema.pre('findOneAndUpdate', function() {
+ console.log(this.getFilter()); // { name: 'John' }
+ console.log(this.getUpdate()); // { age: 30 }
+});
+const User = mongoose.model('User', userSchema);
-// Prints "Removing!"
-doc.remove();
+await User.findOneAndUpdate({ name: 'John' }, { $set: { age: 30 } });
+```
-// Does **not** print "Removing!". Query middleware for `remove` is not
-// executed by default.
-Model.remove();
+For document middleware, like `pre('save')`, Mongoose passes the 1st parameter to `save()` as the 2nd argument to your `pre('save')` callback.
+You should use the 2nd argument to get access to the `save()` call's `options`, because Mongoose documents don't store all the options you can pass to `save()`.
+
+```javascript
+const userSchema = new Schema({ name: String, age: Number });
+userSchema.pre('save', function(next, options) {
+ options.validateModifiedOnly; // true
+
+ // Remember to call `next()` unless you're using an async function or returning a promise
+ next();
+});
+const User = mongoose.model('User', userSchema);
+
+const doc = new User({ name: 'John', age: 30 });
+await doc.save({ validateModifiedOnly: true });
+```
+
+## Naming Conflicts {#naming}
+
+Mongoose has both query and document hooks for `deleteOne()`.
+
+```javascript
+schema.pre('deleteOne', function() { console.log('Removing!'); });
+
+// Does **not** print "Removing!". Document middleware for `deleteOne` is not executed by default
+await doc.deleteOne();
+
+// Prints "Removing!"
+await Model.deleteOne();
```
You can pass options to [`Schema.pre()`](api.html#schema_Schema-pre)
and [`Schema.post()`](api.html#schema_Schema-post) to switch whether
-Mongoose calls your `remove()` hook for [`Document.remove()`](api/model.html#model_Model-remove)
-or [`Model.remove()`](api/model.html#model_Model-remove). Note here that you need to set both `document` and `query` properties in the passed object:
+Mongoose calls your `deleteOne()` hook for [`Document.prototype.deleteOne()`](api/model.html#Model.prototype.deleteOne())
+or [`Query.prototype.deleteOne()`](api/query.html#Query.prototype.deleteOne()). Note here that you need to set both `document` and `query` properties in the passed object:
```javascript
// Only document middleware
-schema.pre('remove', { document: true, query: false }, function() {
- console.log('Removing doc!');
+schema.pre('deleteOne', { document: true, query: false }, function() {
+ console.log('Deleting doc!');
+});
+
+// Only query middleware. This will get called when you do `Model.deleteOne()`
+// but not `doc.deleteOne()`.
+schema.pre('deleteOne', { query: true, document: false }, function() {
+ console.log('Deleting!');
});
+```
+
+Mongoose also has both query and document hooks for `validate()`.
+Unlike `deleteOne` and `updateOne`, `validate` middleware applies to `Document.prototype.validate` by default.
-// Only query middleware. This will get called when you do `Model.remove()`
-// but not `doc.remove()`.
-schema.pre('remove', { query: true, document: false }, function() {
- console.log('Removing!');
+```javascript
+const schema = new mongoose.Schema({ name: String });
+schema.pre('validate', function() {
+ console.log('Document validate');
+});
+schema.pre('validate', { query: true, document: false }, function() {
+ console.log('Query validate');
});
+const Test = mongoose.model('Test', schema);
+
+const doc = new Test({ name: 'foo' });
+
+// Prints "Document validate"
+await doc.validate();
+
+// Prints "Query validate"
+await Test.find().validate();
```
-
+## Notes on `findAndUpdate()` and Query Middleware {#notes}
Pre and post `save()` hooks are **not** executed on `update()`,
`findOneAndUpdate()`, etc. You can see a more detailed discussion why in
@@ -444,9 +511,7 @@ await doc.updateOne({ $set: { name: 'test' } }); // Prints "Updating"
await Model.updateOne({}, { $set: { name: 'test' } });
```
-
-
-_New in 4.5.0_
+## Error Handling Middleware {#error-handling-middleware}
Middleware execution normally stops the first time a piece of middleware
calls `next()` with an error. However, there is a special kind of post
@@ -487,31 +552,29 @@ also define a post `update()` hook that will catch MongoDB duplicate key
errors.
```javascript
-// The same E11000 error can occur when you call `update()`
-// This function **must** take 3 parameters. If you use the
-// `passRawResult` function, this function **must** take 4
-// parameters
-schema.post('update', function(error, res, next) {
+// The same E11000 error can occur when you call `updateOne()`
+// This function **must** take 4 parameters.
+
+schema.post('updateOne', function(passRawResult, error, res, next) {
if (error.name === 'MongoServerError' && error.code === 11000) {
next(new Error('There was a duplicate key error'));
} else {
- next(); // The `update()` call will still error out.
+ next(); // The `updateOne()` call will still error out.
}
});
const people = [{ name: 'Axl Rose' }, { name: 'Slash' }];
-Person.create(people, function(error) {
- Person.update({ name: 'Slash' }, { $set: { name: 'Axl Rose' } }, function(error) {
- // `error.message` will be "There was a duplicate key error"
- });
-});
+await Person.create(people);
+
+// Throws "There was a duplicate key error"
+await Person.updateOne({ name: 'Slash' }, { $set: { name: 'Axl Rose' } });
```
Error handling middleware can transform an error, but it can't remove the
error. Even if you call `next()` with no error as shown above, the
function call will still error out.
-
+## Aggregation Hooks {#aggregate}
You can also define hooks for the [`Model.aggregate()` function](api/model.html#model_Model-aggregate).
In aggregation middleware functions, `this` refers to the [Mongoose `Aggregate` object](api/aggregate.html#Aggregate).
@@ -533,7 +596,7 @@ lets you access the MongoDB aggregation pipeline that Mongoose will send to
the MongoDB server. It is useful for adding stages to the beginning of the
pipeline from middleware.
-
+## Synchronous Hooks {#synchronous}
Certain Mongoose hooks are synchronous, which means they do **not** support
functions that return promises or receive a `next()` callback. Currently,
@@ -552,7 +615,7 @@ rejections.
[require:post init hooks.*error]
```
-Next Up
+## Next Up {#next}
Now that we've covered middleware, let's take a look at Mongoose's approach
to faking JOINs with its query [population](populate.html) helper.
diff --git a/docs/migrating_to_5.md b/docs/migrating_to_5.md
index aa9a3f60748..06d60e81fb3 100644
--- a/docs/migrating_to_5.md
+++ b/docs/migrating_to_5.md
@@ -41,13 +41,13 @@ If you're still on Mongoose 3.x, please read the [Mongoose 3.x to 4.x migration
* [`bulkWrite()` results](#bulkwrite-results)
* [Strict SSL validation](#strict-ssl-validation)
-
+## Version Requirements {#version-requirements}
Mongoose now requires Node.js >= 4.0.0 and MongoDB >= 3.0.0.
[MongoDB 2.6](https://www.mongodb.com/blog/post/mongodb-2-6-end-of-life) and
[Node.js < 4](https://github.com/nodejs/Release) where both EOL-ed in 2016.
-
+## Query Middleware {#query-middleware}
Query middleware is now compiled when you call `mongoose.model()` or `db.model()`. If you add query middleware after calling `mongoose.model()`, that middleware will **not** get called.
@@ -63,9 +63,7 @@ MyModel.find().exec(function() {
});
```
-
+## Promises and Callbacks for `mongoose.connect()` {#promises-and-callbacks}
`mongoose.connect()` and `mongoose.disconnect()` now return a promise if no callback specified, or `null` otherwise. It does **not** return the mongoose singleton.
@@ -80,9 +78,7 @@ mongoose.connect('mongodb://127.0.0.1:27017/test');
mongoose.model('Test', new Schema({}));
```
-
+## Connection Logic and `useMongoClient` {#connection-logic}
The [`useMongoClient` option](/docs/4.x/docs/connections.html#use-mongo-client) was
removed in Mongoose 5, it is now always `true`. As a consequence, Mongoose 5
@@ -97,9 +93,7 @@ examples of `mongoose.connect()` calls that do **not** work in Mongoose 5.x.
In Mongoose 5.x, the first parameter to `mongoose.connect()` and `mongoose.createConnection()`, if specified, **must** be a [MongoDB connection string](https://www.mongodb.com/docs/manual/reference/connection-string/). The
connection string and options are then passed down to [the MongoDB Node.js driver's `MongoClient.connect()` function](http://mongodb.github.io/node-mongodb-native/3.0/api/MongoClient.html#.connect). Mongoose does not modify the connection string, although `mongoose.connect()` and `mongoose.createConnection()` support a [few additional options in addition to the ones the MongoDB driver supports](http://mongoosejs.com/docs/connections.html#options).
-
+## Setter Order {#setter-order}
Setters run in reverse order in 4.x:
@@ -119,9 +113,7 @@ schema.path('name').
set(() => console.log('This will print 2nd'));
```
-
+## Checking if a path is populated {#id-getter}
Mongoose 5.1.0 introduced an `_id` getter to ObjectIds that lets you get an ObjectId regardless of whether a path
is populated.
@@ -151,9 +143,7 @@ As a consequence, checking whether `blogPost.author._id` is [no longer viable as
Note that you can call `mongoose.set('objectIdGetter', false)` to change this behavior.
-
+## Return Values for `remove()` and `deleteX()` {#return-value-for-delete}
`deleteOne()`, `deleteMany()`, and `remove()` now resolve to the result object
rather than the full [driver `WriteOpResult` object](http://mongodb.github.io/node-mongodb-native/2.2/api/Collection.html#~writeOpCallback).
@@ -165,9 +155,7 @@ MyModel.deleteMany().then(res => console.log(res.result.n));
MyModel.deleteMany().then(res => res.n);
```
-
+## Aggregation Cursors {#aggregation-cursors}
The `useMongooseAggCursor` option from 4.x is now always on. This is the new syntax for aggregation cursors in mongoose 5:
@@ -185,23 +173,17 @@ const cursorWithOptions = MyModel.
exec();
```
-
+## `geoNear` {#geonear}
`Model.geoNear()` has been removed because the [MongoDB driver no longer supports it](https://github.com/mongodb/node-mongodb-native/blob/4bac63ce7b9e9fff87c31c5a27d78bcdaca12669/etc/notes/CHANGES_3.0.0.md#geonear-command-helper)
-
+## Required URI encoding of connection strings {#uri-encoding}
Due to changes in the MongoDB driver, connection strings must be URI encoded.
If they are not, connections may fail with an illegal character message.
-
+## Passwords which contain certain characters {#password-characters}
See a [full list of affected characters](https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding).
@@ -230,9 +212,7 @@ mongoose.connect(encodeMongoURI(mongodbConnectString));
The function above is safe to use whether the existing string is already encoded or not.
-
+## Domain sockets {#domain-sockets}
Domain sockets must be URI encoded. For example:
@@ -247,9 +227,7 @@ const host = encodeURIComponent('/tmp/mongodb-27017.sock');
mongoose.createConnection(`mongodb://aaron:psw@${host}/fake`);
```
-
+## `toObject()` Options {#toobject-options}
The `options` parameter to `toObject()` and `toJSON()` merge defaults rather than overwriting them.
@@ -267,9 +245,7 @@ const doc = new MyModel({ name: 'test' });
console.log(doc.toJSON({ minimize: false }).answer);
```
-
+## Aggregate Parameters {#aggregate-parameters}
`aggregate()` no longer accepts a spread, you **must** pass your aggregation pipeline as an array. The below code worked in 4.x:
@@ -283,9 +259,7 @@ The above code does **not** work in 5.x, you **must** wrap the `$match` and `$sk
MyModel.aggregate([{ $match: { isDeleted: false } }, { $skip: 10 }]).exec(cb);
```
-
+## Boolean Casting {#boolean-casting}
By default, mongoose 4 would coerce any value to a boolean without error.
@@ -316,16 +290,14 @@ And the following values to `false`:
All other values will cause a `CastError`
-
+## Query Casting {#query-casting}
Casting for `update()`, `updateOne()`, `updateMany()`, `replaceOne()`,
`remove()`, `deleteOne()`, and `deleteMany()` doesn't happen until `exec()`.
This makes it easier for hooks and custom query helpers to modify data, because
mongoose won't restructure the data you passed in until after your hooks and
query helpers have ran. It also makes it possible to set the `overwrite` option
-_after_ passing in an update.
+*after* passing in an update.
```javascript
// In mongoose 4.x, this becomes `{ $set: { name: 'Baz' } }` despite the `overwrite`
@@ -334,9 +306,7 @@ _after_ passing in an update.
User.where({ name: 'Bar' }).update({ name: 'Baz' }).setOptions({ overwrite: true });
```
-
+## Post Save Hooks Get Flow Control {#post-save-flow-control}
Post hooks now get flow control, which means async post save hooks and child document post save hooks execute **before** your `save()` callback.
@@ -363,21 +333,15 @@ m.save(function() {
});
```
-
+## The `$pushAll` Operator {#pushall}
`$pushAll` is no longer supported and no longer used internally for `save()`, since it has been [deprecated since MongoDB 2.4](https://www.mongodb.com/docs/manual/reference/operator/update/pushAll/). Use `$push` with `$each` instead.
-
+## Always Use Forward Key Order {#retain-key-order}
The `retainKeyOrder` option was removed, mongoose will now always retain the same key position when cloning objects. If you have queries or indexes that rely on reverse key order, you will have to change them.
-
+## Run setters on queries {#run-setters-on-queries}
Setters now run on queries by default, and the old `runSettersOnQuery` option
has been removed.
@@ -390,69 +354,49 @@ const Model = mongoose.model('Test', schema);
Model.find({ email: 'FOO@BAR.BAZ' }); // Converted to `find({ email: 'foo@bar.baz' })`
```
-
+## Pre-compiled Browser Bundle {#browser-bundle}
We no longer have a pre-compiled version of mongoose for the browser. If you want to use mongoose schemas in the browser, you need to build your own bundle with browserify/webpack.
-
+## Save Errors {#save-errors}
The `saveErrorIfNotFound` option was removed, mongoose will now always error out from `save()` if the underlying document was not found
-
+## Init hook signatures {#init-hooks}
`init` hooks are now fully synchronous and do not receive `next()` as a parameter.
`Document.prototype.init()` no longer takes a callback as a parameter. It
was always synchronous, just had a callback for legacy reasons.
-
+## `numAffected` and `save()` {#save-num-affected}
`doc.save()` no longer passes `numAffected` as a 3rd param to its callback.
-
+## `remove()` and debouncing {#remove-debounce}
`doc.remove()` no longer debounces
-
+## `getPromiseConstructor()` {#get-promise-constructor}
`getPromiseConstructor()` is gone, just use `mongoose.Promise`.
-
+## Passing Parameters from Pre Hooks {#pre-hook-params}
You cannot pass parameters to the next pre middleware in the chain using `next()` in mongoose 5.x. In mongoose 4, `next('Test')` in pre middleware would call the
next middleware with 'Test' as a parameter. Mongoose 5.x has removed support for this.
-
+## `required` validator for arrays {#array-required}
In mongoose 5 the `required` validator only verifies if the value is an
-array. That is, it will **not** fail for _empty_ arrays as it would in
+array. That is, it will **not** fail for *empty* arrays as it would in
mongoose 4.
-
+## debug output defaults to stdout instead of stderr {#debug-output}
In mongoose 5 the default debug function uses `console.info()` to display messages instead of `console.error()`.
-
+## Overwriting filter properties {#overwrite-filter}
In Mongoose 4.x, overwriting a filter property that's a primitive with one that is an object would silently fail. For example, the below code would ignore the `where()` and be equivalent to `Sport.find({ name: 'baseball' })`
@@ -462,9 +406,7 @@ Sport.find({ name: 'baseball' }).where({ name: { $ne: 'softball' } });
In Mongoose 5.x, the above code will correctly overwrite `'baseball'` with `{ $ne: 'softball' }`
-
+## `bulkWrite()` results {#bulkwrite-results}
Mongoose 5.x uses version 3.x of the [MongoDB Node.js driver](http://npmjs.com/package/mongodb). MongoDB driver 3.x changed the format of
the result of [`bulkWrite()` calls](api/model.html#model_Model-bulkWrite) so there is no longer a top-level `nInserted`, `nModified`, etc. property. The new result object structure is [described here](http://mongodb.github.io/node-mongodb-native/3.1/api/Collection.html#~BulkWriteOpResult).
@@ -479,7 +421,7 @@ console.log(res);
In Mongoose 4.x, the above will print:
-```
+```txt
BulkWriteResult {
ok: [Getter],
nInserted: [Getter],
@@ -512,7 +454,7 @@ BulkWriteResult {
In Mongoose 5.x, the script will print:
-```
+```txt
BulkWriteResult {
result:
{ ok: 1,
@@ -536,9 +478,7 @@ BulkWriteResult {
n: 1 }
```
-
+## Strict SSL Validation {#strict-ssl-validation}
The most recent versions of the [MongoDB Node.js driver use strict SSL validation by default](http://mongodb.github.io/node-mongodb-native/3.5/tutorials/connect/tls/),
which may lead to errors if you're using [self-signed certificates](https://github.com/Automattic/mongoose/issues/9147).
diff --git a/docs/migrating_to_6.md b/docs/migrating_to_6.md
index 2aa3a3a8bd4..7f672bcad7d 100644
--- a/docs/migrating_to_6.md
+++ b/docs/migrating_to_6.md
@@ -50,14 +50,17 @@ If you're still on Mongoose 4.x, please read the [Mongoose 4.x to 5.x migration
* [SchemaType `set` parameters now use `priorValue` as the second parameter instead of `self`](#schematype-set-parameters)
* [No default model for `Query.prototype.populate()`](#no-default-model-for-query-prototype-populate)
* [`toObject()` and `toJSON()` Use Nested Schema `minimize`](#toobject-and-tojson-use-nested-schema-minimize)
-* [TypeScript changes](#typescript-changes)
* [Removed `reconnectTries` and `reconnectInterval` options](#removed-reconnecttries-and-reconnectinterval-options)
+* [MongoDB Driver's New URL Parser Incompatible with Some npm Packages](#mongodb-drivers-new-url-parser-incompatible-with-some-npm-packages)
+* [Lodash `.isEmpty()` returns false for ObjectIds](#lodash-object-id)
+* [mongoose.modelSchemas removed](#model-schemas)
+* [TypeScript changes](#typescript-changes)
-
+## Version Requirements {#version-requirements}
Mongoose now requires Node.js >= 12.0.0. Mongoose still supports MongoDB server versions back to 3.0.0.
-
+## MongoDB Driver 4.0 {#mongodb-driver-40}
Mongoose now uses v4.x of the [MongoDB Node driver](https://www.npmjs.com/package/mongodb).
See [the MongoDB Node drivers' migration guide](https://github.com/mongodb/node-mongodb-native/blob/4.0/docs/CHANGES_4.0.0.md) for detailed info.
@@ -86,7 +89,7 @@ res;
res.deletedCount; // Number of documents that were deleted. Replaces `res.n`
```
-
+## No More Deprecation Warning Options {#no-more-deprecation-warning-options}
`useNewUrlParser`, `useUnifiedTopology`, `useFindAndModify`, and `useCreateIndex` are no longer supported options. Mongoose 6 always behaves as if `useNewUrlParser`, `useUnifiedTopology`, and `useCreateIndex` are `true`, and `useFindAndModify` is `false`. Please remove these options from your code.
@@ -100,7 +103,7 @@ await mongoose.connect('mongodb://127.0.0.1:27017/test', {
});
```
-
+## The `asPromise()` Method for Connections {#the-aspromise-method-for-connections}
Mongoose connections are no longer [thenable](https://masteringjs.io/tutorials/fundamentals/thenable). This means that `await mongoose.createConnection(uri)` **no longer waits for Mongoose to connect**. Use `mongoose.createConnection(uri).asPromise()` instead. See [#8810](https://github.com/Automattic/mongoose/issues/8810).
@@ -112,11 +115,11 @@ await mongoose.createConnection(uri);
await mongoose.createConnection(uri).asPromise();
```
-
+## `mongoose.connect()` Returns a Promise {#mongoose-connect-returns-a-promise}
The `mongoose.connect()` function now always returns a promise, **not** a Mongoose instance.
-
+## Duplicate Query Execution {#duplicate-query-execution}
Mongoose no longer allows executing the same query object twice. If you do, you'll get a `Query was already executed` error. Executing the same query instance twice is typically indicative of mixing callbacks and promises, but if you need to execute the same query twice, you can call `Query#clone()` to clone the query and re-execute it. See [gh-7398](https://github.com/Automattic/mongoose/issues/7398)
@@ -129,7 +132,7 @@ await q;
await q.clone(); // Can `clone()` the query to allow executing the query again
```
-
+## `Model.exists(...)` now returns a lean document instead of boolean {#model-exists-returns-a-lean-document-instead-of-boolean}
```js
// in Mongoose 5.x, `existingUser` used to be a boolean
@@ -140,12 +143,20 @@ if (existingUser) {
}
```
-
+## `strictQuery` is now equal to `strict` by default {#strictquery-is-removed-and-replaced-by-strict}
~Mongoose no longer supports a `strictQuery` option. You must now use `strict`.~
-As of Mongoose 6.0.10, we brought back the `strictQuery` option.
-However, `strictQuery` is tied to `strict` by default.
-This means that, by default, Mongoose will filter out query filter properties that are not in the schema.
+As of Mongoose 6.0.10, we brought back the `strictQuery` option. In Mongoose 6, `strictQuery` is set to `strict` by default. This means that, by default, Mongoose will filter out query filter properties that are not in the schema.
+
+However, this behavior was a source of confusion in some cases, so in Mongoose 7, this default changes back to `false`. So if you want to retain the default behavior of Mongoose 5 as well as Mongoose 7 and later, you can also disable `strictQuery` globally to override:
+
+```javascript
+mongoose.set('strictQuery', false);
+```
+
+In a test suite, it may be useful to set `strictQuery` to `throw`, which will throw exceptions any time a query references schema that doesn't exist, which could help identify a bug in your tests or code.
+
+Here's an example of the effect of `strictQuery`:
```javascript
const userSchema = new Schema({ name: String });
@@ -166,11 +177,11 @@ You can also disable `strictQuery` globally to override:
mongoose.set('strictQuery', false);
```
-
+## MongoError is now MongoServerError {#mongoerror-is-now-mongoservererror}
In MongoDB Node.js Driver v4.x, 'MongoError' is now 'MongoServerError'. Please change any code that depends on the hardcoded string 'MongoError'.
-
+## Clone Discriminator Schemas By Default {#clone-discriminator-schemas-by-default}
Mongoose now clones discriminator schemas by default. This means you need to pass `{ clone: false }` to `discriminator()` if you're using recursive embedded discriminators.
@@ -183,7 +194,7 @@ User.discriminator('author', authorSchema.clone());
User.discriminator('author', authorSchema, { clone: false });
```
-
+## Simplified `isValidObjectId()` and separate `isObjectIdOrHexString()` {#simplified-isvalidobjectid-and-separate-isobjectidorhexstring}
In Mongoose 5, `mongoose.isValidObjectId()` returned `false` for values like numbers, which was inconsistent with the MongoDB driver's `ObjectId.isValid()` function.
Technically, any JavaScript number can be converted to a MongoDB ObjectId.
@@ -208,7 +219,7 @@ mongoose.isObjectIdOrHexString('0123456789ab'); // false
mongoose.isObjectIdOrHexString(6); // false
```
-
+## Schema Defined Document Key Order {#schema-defined-document-key-order}
Mongoose now saves objects with keys in the order the keys are specified in the schema, not in the user-defined object. So whether `Object.keys(new User({ name: String, email: String }).toObject()` is `['name', 'email']` or `['email', 'name']` depends on the order `name` and `email` are defined in your schema.
@@ -232,7 +243,7 @@ const doc = new Test({
assert.deepEqual(Object.keys(doc.toObject().profile.name), ['first', 'last']);
```
-
+## `sanitizeFilter` and `trusted()` {#sanitizefilter-and-trusted}
Mongoose 6 introduces a new `sanitizeFilter` option to globals and queries that defends against [query selector injection attacks](https://thecodebarbarian.com/2014/09/04/defending-against-query-selector-injection-attacks.html). If you enable `sanitizeFilter`, Mongoose will wrap any object in the query filter in a `$eq`:
@@ -249,7 +260,7 @@ To explicitly allow a query selector, use `mongoose.trusted()`:
await Test.find({ username: 'val', pwd: mongoose.trusted({ $ne: null }) }).setOptions({ sanitizeFilter: true });
```
-
+## Removed `omitUndefined`: Mongoose now removes `undefined` keys in updates instead of setting them to `null` {#removed-omitundefined}
In Mongoose 5.x, setting a key to `undefined` in an update operation was equivalent to setting it to `null`.
@@ -278,7 +289,7 @@ The only workaround is to explicitly set properties to `null` in your updates:
const res = await Test.findOneAndUpdate({}, { $set: { name: null } }, { new: true });
```
-
+## Document Parameter to Default Functions {#document-parameter-to-default-functions}
Mongoose now passes the document as the first parameter to `default` functions, which is helpful for using [arrow functions](https://masteringjs.io/tutorials/fundamentals/arrow) with defaults.
@@ -296,7 +307,7 @@ const schema = new Schema({
});
```
-
+## Arrays are Proxies {#arrays-are-proxies}
Mongoose arrays are now ES6 proxies. You no longer need to `markModified()` after setting an array index directly.
@@ -307,23 +318,23 @@ post.tags[0] = 'javascript';
await post.save(); // Works, no need for `markModified()`!
```
-
+## `typePojoToMixed` {#typepojotomixed}
Schema paths declared with `type: { name: String }` become single nested subdocs in Mongoose 6, as opposed to Mixed in Mongoose 5. This removes the need for the `typePojoToMixed` option. See [gh-7181](https://github.com/Automattic/mongoose/issues/7181).
```javascript
// In Mongoose 6, the below makes `foo` into a subdocument with a `name` property.
-// In Mongoose 5, the below would make `foo` a `Mixed` type, _unless_ you set `typePojoToMixed: true`.
+// In Mongoose 5, the below would make `foo` a `Mixed` type, _unless_ you set `typePojoToMixed: false`.
const schema = new Schema({
foo: { type: { name: String } }
});
```
-
+## `strictPopulate()` {#strictpopulate}
Mongoose now throws an error if you `populate()` a path that isn't defined in your schema. This is only for cases when we can infer the local schema, like when you use `Query#populate()`, **not** when you call `Model.populate()` on a POJO. See [gh-5124](https://github.com/Automattic/mongoose/issues/5124).
-
+## Subdocument `ref` Function Context {#subdocument-ref-function-context}
When populating a subdocument with a function `ref` or `refPath`, `this` is now the subdocument being populated, not the top-level document. See [#8469](https://github.com/Automattic/mongoose/issues/8469).
@@ -343,55 +354,56 @@ const schema = new Schema({
});
```
-
+## Schema Reserved Names Warning {#schema-reserved-names-warning}
-Using `save`, `isNew`, and other Mongoose reserved names as schema path names now triggers a warning, not an error. You can suppress the warning by setting the `supressReservedKeysWarning` in your schema options: `new Schema({ save: String }, { supressReservedKeysWarning: true })`. Keep in mind that this may break plugins that rely on these reserved names.
+Using `save`, `isNew`, and other Mongoose reserved names as schema path names now triggers a warning, not an error. You can suppress the warning by setting the `suppressReservedKeysWarning` in your schema options: `new Schema({ save: String }, { suppressReservedKeysWarning: true })`. Keep in mind that this may break plugins that rely on these reserved names.
-
+## Subdocument Paths {#subdocument-paths}
Single nested subdocs have been renamed to "subdocument paths". So `SchemaSingleNestedOptions` is now `SchemaSubdocumentOptions` and `mongoose.Schema.Types.Embedded` is now `mongoose.Schema.Types.Subdocument`. See [gh-10419](https://github.com/Automattic/mongoose/issues/10419)
-
+## Creating Aggregation Cursors {#creating-aggregation-cursors}
`Aggregate#cursor()` now returns an AggregationCursor instance to be consistent with `Query#cursor()`. You no longer need to do `Model.aggregate(pipeline).cursor().exec()` to get an aggregation cursor, just `Model.aggregate(pipeline).cursor()`.
-
+## `autoCreate` Defaults to `true` {#autocreate-defaults-to-true}
`autoCreate` is `true` by default **unless** readPreference is secondary or secondaryPreferred, which means Mongoose will attempt to create every model's underlying collection before creating indexes. If readPreference is secondary or secondaryPreferred, Mongoose will default to `false` for both `autoCreate` and `autoIndex` because both `createCollection()` and `createIndex()` will fail when connected to a secondary.
-
+## No More `context: 'query'` {#no-more-context-query}
The `context` option for queries has been removed. Now Mongoose always uses `context = 'query'`.
-
+## Custom Validators with Populated Paths {#custom-validators-with-populated-paths}
Mongoose 6 always calls validators with depopulated paths (that is, with the id rather than the document itself). In Mongoose 5, Mongoose would call validators with the populated doc if the path was populated. See [#8042](https://github.com/Automattic/mongoose/issues/8042)
-
+## Disconnected Event with Replica Sets {#disconnected-event-with-replica-sets}
When connected to a replica set, connections now emit 'disconnected' when connection to the primary is lost. In Mongoose 5, connections only emitted 'disconnected' when losing connection to all members of the replica set.
However, Mongoose 6 does **not** buffer commands while a connection is disconnected. So you can still successfully execute commands like queries with `readPreference = 'secondary'`, even if the Mongoose connection is in the disconnected state.
-
+## Removed `execPopulate()` {#removed-execpopulate}
`Document#populate()` now returns a promise and is now no longer chainable.
* Replace `await doc.populate('path1').populate('path2').execPopulate();` with `await doc.populate(['path1', 'path2']);`
* Replace `await doc.populate('path1', 'select1').populate('path2', 'select2').execPopulate();` with
- ```
+
+ ```js
await doc.populate([{path: 'path1', select: 'select1'}, {path: 'path2', select: 'select2'}]);
```
-
+## `create()` with Empty Array {#create-with-empty-array}
`await Model.create([])` in v6.0 returns an empty array when provided an empty array, in v5.0 it used to return `undefined`. If any of your code is checking whether the output is `undefined` or not, you need to modify it with the assumption that `await Model.create(...)` will always return an array if provided an array.
-
+## Removed Nested Path Merging {#removed-nested-path-merging}
`doc.set({ child: { age: 21 } })` now works the same whether `child` is a nested path or a subdocument: Mongoose will overwrite the value of `child`. In Mongoose 5, this operation would merge `child` if `child` was a nested path.
-
+## ObjectId `valueOf()` {#objectid-valueof}
Mongoose now adds a `valueOf()` function to ObjectIds. This means you can now use `==` to compare an ObjectId against a string.
@@ -401,23 +413,22 @@ const a = ObjectId('6143b55ac9a762738b15d4f0');
a == '6143b55ac9a762738b15d4f0'; // true
```
-
+## Immutable `createdAt` {#immutable-createdat}
If you set `timestamps: true`, Mongoose will now make the `createdAt` property `immutable`. See [gh-10139](https://github.com/Automattic/mongoose/issues/10139)
-
+## Removed Validator `isAsync` {#removed-validator-isasync}
`isAsync` is no longer an option for `validate`. Use an `async function` instead.
-
+## Removed `safe` {#removed-safe}
`safe` is no longer an option for schemas, queries, or `save()`. Use `writeConcern` instead.
-
+## SchemaType `set` parameters {#schematype-set-parameters}
Mongoose now calls setter functions with `priorValue` as the 2nd parameter, rather than `schemaType` in Mongoose 5.
-
```js
const userSchema = new Schema({
name: {
@@ -441,7 +452,7 @@ const user = new User({ name: 'Robert Martin' });
console.log(user.name); // 'robert martin'
```
-
+## `toObject()` and `toJSON()` Use Nested Schema `minimize` {#toobject-and-tojson-use-nested-schema-minimize}
This change was technically released with 5.10.5, but [caused issues for users migrating from 5.9.x to 6.x](https://github.com/Automattic/mongoose/issues/10827).
In Mongoose `< 5.10.5`, `toObject()` and `toJSON()` would use the top-level schema's `minimize` option by default.
@@ -472,7 +483,7 @@ const parent = new Schema({
}, { minimize: false });
```
-
+## No default model for `Query.prototype.populate()` {#no-default-model-for-query-prototype-populate}
In Mongoose 5, calling `populate()` on a mixed type or other path with no `ref` would fall back to using the query's model.
@@ -497,6 +508,45 @@ In Mongoose 6, populating a path with no `ref`, `refPath`, or `model` is a no-op
await Test.findOne().populate('parents');
```
+## MongoDB Driver's New URL Parser Incompatible with Some npm Packages {#mongodb-drivers-new-url-parser-incompatible-with-some-npm-packages}
+
+The MongoDB Node driver version that Mongoose 6 uses relies on a [URL parser module](https://npmjs.com/package/whatwg-url) that has several known compatibility issues with other npm packages.
+This can lead to errors like `Invalid URL: mongodb+srv://username:password@development.xyz.mongodb.net/abc` if you use one of the incompatible packages.
+[You can find a list of incompatible packages here](https://mongoosejs.com/docs/incompatible_packages).
+
+## Removed `reconnectTries` and `reconnectInterval` options {#removed-reconnecttries-and-reconnectinterval-options}
+
+The `reconnectTries` and `reconnectInterval` options have been removed since they are no longer necessary.
+
+The MongoDB node driver will always attempt to retry any operation for up to `serverSelectionTimeoutMS`, even if MongoDB is down for a long period of time.
+So, it will never run out of retries or try to reconnect to MongoDB.
+
+## Lodash `.isEmpty()` returns true for ObjectIds {#lodash-object-id}
+
+Lodash's `isEmpty()` function returns true for primitives and primitive wrappers.
+`ObjectId()` is an object wrapper that is treated as a primitive by Mongoose.
+But starting in Mongoose 6, `_.isEmpty()` will return true for ObjectIds because of Lodash implementation details.
+
+An ObjectId in mongoose is never empty, so if you're using `isEmpty()` you should check for `instanceof ObjectId`.
+
+```javascript
+if (!(val instanceof Types.ObjectId) && _.isEmpty(val)) {
+ // Handle empty object here
+}
+```
+
+## Removed `mongoose.modelSchemas` {#model-schemas}
+
+The `mongoose.modelSchemas` property was removed. This may have been used to delete a model schema.
+
+```javascript
+// before
+delete mongoose.modelSchemas.User;
+
+// with Mongoose 6.x
+delete mongoose.deleteModel('User');
+```
+
## TypeScript changes
The `Schema` class now takes 3 generic params instead of 4. The 3rd generic param, `SchemaDefinitionType`, is now the same as the 1st generic param `DocType`. Replace `new Schema(schemaDefinition)` with `new Schema(schemaDefinition)`
@@ -534,10 +584,3 @@ schema.virtual('myVirtual').get(function() {
this.name; // string
});
```
-
-
-
-The `reconnectTries` and `reconnectInterval` options have been removed since they are no longer necessary.
-
-The MongoDB node driver will always attempt to retry any operation for up to `serverSelectionTimeoutMS`, even if MongoDB is down for a long period of time.
-So, it will never run out of retries or try to reconnect to MongoDB.
diff --git a/docs/migrating_to_7.md b/docs/migrating_to_7.md
new file mode 100644
index 00000000000..4283c41b6df
--- /dev/null
+++ b/docs/migrating_to_7.md
@@ -0,0 +1,411 @@
+# Migrating from 6.x to 7.x
+
+
+
+There are several backwards-breaking changes
+you should be aware of when migrating from Mongoose 6.x to Mongoose 7.x.
+
+If you're still on Mongoose 5.x, please read the [Mongoose 5.x to 6.x migration guide](migrating_to_6.html) and upgrade to Mongoose 6.x first.
+
+* [`strictQuery`](#strictquery)
+* [Removed `remove()`](#removed-remove)
+* [Dropped callback support](#dropped-callback-support)
+* [Removed `update()`](#removed-update)
+* [ObjectId requires `new`](#objectid-requires-new)
+* [`id` setter](#id-setter)
+* [Discriminator schemas use base schema options by default](#discriminator-schemas-use-base-schema-options-by-default)
+* [Removed `castForQueryWrapper()`, updated `castForQuery()` signature](#removed-castforquerywrapper)
+* [Copy schema options in `Schema.prototype.add()`](#copy-schema-options-in-schema-prototype-add)
+* [ObjectId bsontype now has lowercase d](#objectid-bsontype-now-has-lowercase-d)
+* [Removed support for custom promise libraries](#removed-support-for-custom-promise-libraries)
+* [Removed mapReduce](#removed-mapreduce)
+* [Deprecated `keepAlive`](#deprecated-keepalive)
+* [TypeScript-specific changes](#typescript-specific-changes)
+ * [Removed `LeanDocument` and support for `extends Document`](#removed-leandocument-and-support-for-extends-document)
+ * [New parameters for `HydratedDocument`](#new-parameters-for-hydrateddocument)
+
+## `strictQuery` {#strictquery}
+
+`strictQuery` is now false by default.
+
+```javascript
+const mySchema = new Schema({ field: Number });
+const MyModel = mongoose.model('Test', mySchema);
+
+// Mongoose will not strip out `notInSchema: 1` because `strictQuery` is false by default
+const docs = await MyModel.find({ notInSchema: 1 });
+// Empty array in Mongoose 7. In Mongoose 6, this would contain all documents in MyModel
+docs;
+```
+
+## Removed `remove()` {#removed-remove}
+
+The `remove()` method on documents and models has been removed.
+Use `deleteOne()` or `deleteMany()` instead.
+
+```javascript
+const mySchema = new Schema({ field: Number });
+const MyModel = mongoose.model('Test', mySchema);
+
+// Change this:
+await MyModel.remove(filter);
+
+// To this:
+await MyModel.deleteOne(filter);
+// Or this, if you want to delete multiple:
+await MyModel.deleteMany(filter);
+
+// For documents, change this:
+await doc.remove();
+
+// To this:
+await doc.deleteOne();
+```
+
+Keep in mind that `deleteOne()` hooks are treated as query middleware by default.
+So for middleware, please do the following:
+
+```javascript
+// Replace this:
+schema.pre('remove', function() {
+ /* ... */
+});
+
+// With this:
+schema.pre('deleteOne', { document: true, query: false }, function() {
+ /* ... */
+});
+```
+
+## Dropped callback support {#dropped-callback-support}
+
+The following functions no longer accept callbacks.
+They always return promises.
+
+* `Aggregate.prototype.exec`
+* `Aggregate.prototype.explain`
+* `AggregationCursor.prototype.close`
+* `AggregationCursor.prototype.next`
+* `AggregationCursor.prototype.eachAsync`
+* `Connection.prototype.startSession`
+* `Connection.prototype.dropCollection`
+* `Connection.prototype.createCollection`
+* `Connection.prototype.dropDatabase`
+* `Connection.prototype.openUri`
+* `Connection.prototype.close`
+* `Connection.prototype.destroy`
+* `Document.prototype.populate`
+* `Document.prototype.validate`
+* `Mongoose.prototype.connect`
+* `Mongoose.prototype.createConnection`
+* `Model.prototype.save`
+* `Model.aggregate`
+* `Model.bulkWrite`
+* `Model.cleanIndexes`
+* `Model.countDocuments`
+* `Model.create`
+* `Model.createCollection`
+* `Model.createIndexes`
+* `Model.deleteOne`
+* `Model.deleteMany`
+* `Model.distinct`
+* `Model.ensureIndexes`
+* `Model.estimatedDocumentCount`
+* `Model.exists`
+* `Model.find`
+* `Model.findById`
+* `Model.findByIdAndUpdate`
+* `Model.findByIdAndReplace`
+* `Model.findOne`
+* `Model.findOneAndDelete`
+* `Model.findOneAndUpdate`
+* `Model.findOneAndRemove`
+* `Model.insertMany`
+* `Model.listIndexes`
+* `Model.replaceOne`
+* `Model.syncIndexes`
+* `Model.updateMany`
+* `Model.updateOne`
+* `Query.prototype.find`
+* `Query.prototype.findOne`
+* `Query.prototype.findOneAndDelete`
+* `Query.prototype.findOneAndUpdate`
+* `Query.prototype.findOneAndRemove`
+* `Query.prototype.findOneAndReplace`
+* `Query.prototype.validate`
+* `Query.prototype.deleteOne`
+* `Query.prototype.deleteMany`
+* `Query.prototype.exec`
+* `QueryCursor.prototype.close`
+* `QueryCursor.prototype.next`
+* `QueryCursor.prototype.eachAsync`
+
+If you are using the above functions with callbacks, we recommend switching to async/await, or promises if async functions don't work for you.
+If you need help refactoring a legacy codebase, [this tool from Mastering JS callbacks to async await](https://masteringjs.io/tutorials/tools/callback-to-async-await) using ChatGPT.
+
+```javascript
+// Before
+conn.startSession(function(err, session) {
+ // ...
+});
+
+// After
+const session = await conn.startSession();
+// Or:
+conn.startSession().then(sesson => { /* ... */ });
+
+// With error handling
+try {
+ await conn.startSession();
+} catch (err) { /* ... */ }
+// Or:
+const [err, session] = await conn.startSession().then(
+ session => ([null, session]),
+ err => ([err, null])
+);
+```
+
+## Removed `update()` {#removed-update}
+
+`Model.update()`, `Query.prototype.update()`, and `Document.prototype.update()` have been removed.
+Use `updateOne()` instead.
+
+```javascript
+// Before
+await Model.update(filter, update);
+await doc.update(update);
+
+// After
+await Model.updateOne(filter, update);
+await doc.updateOne(update);
+```
+
+## ObjectId requires `new` {#objectid-requires-new}
+
+In Mongoose 6 and older, you could define a new ObjectId without using the `new` keyword:
+
+```javascript
+// Works in Mongoose 6
+// Throws "Class constructor ObjectId cannot be invoked without 'new'" in Mongoose 7
+const oid = mongoose.Types.ObjectId('0'.repeat(24));
+```
+
+In Mongoose 7, `ObjectId` is now a [JavaScript class](https://masteringjs.io/tutorials/fundamentals/class), so you need to use the `new` keyword.
+
+```javascript
+// Works in Mongoose 6 and Mongoose 7
+const oid = new mongoose.Types.ObjectId('0'.repeat(24));
+```
+
+## `id` Setter {#id-setter}
+
+Starting in Mongoose 7.4, Mongoose's built-in `id` virtual (which stores the document's `_id` as a string) has a setter which allows modifying the document's `_id` property via `id`.
+
+```javascript
+const doc = await TestModel.findOne();
+
+doc.id = '000000000000000000000000';
+doc._id; // ObjectId('000000000000000000000000')
+```
+
+This can cause surprising behavior if you create a `new TestModel(obj)` where `obj` contains both an `id` and an `_id`, or if you use `doc.set()`
+
+```javascript
+// Because `id` is after `_id`, the `id` will overwrite the `_id`
+const doc = new TestModel({
+ _id: '000000000000000000000000',
+ id: '111111111111111111111111'
+});
+
+doc._id; // ObjectId('111111111111111111111111')
+```
+
+[The `id` setter was later removed in Mongoose 8](/docs/migrating_to_8.html#removed-id-setter) due to compatibility issues.
+
+## Discriminator schemas use base schema options by default {#discriminator-schemas-use-base-schema-options-by-default}
+
+When you use `Model.discriminator()`, Mongoose will now use the discriminator base schema's options by default.
+This means you don't need to explicitly set child schema options to match the base schema's.
+
+```javascript
+const baseSchema = Schema({}, { typeKey: '$type' });
+const Base = db.model('Base', baseSchema);
+
+// In Mongoose 6.x, the `Base.discriminator()` call would throw because
+// no `typeKey` option. In Mongoose 7, Mongoose uses the base schema's
+// `typeKey` by default.
+const childSchema = new Schema({}, {});
+const Test = Base.discriminator('Child', childSchema);
+
+Test.schema.options.typeKey; // '$type'
+```
+
+## Removed `castForQueryWrapper`, updated `castForQuery()` signature {#removed-castforquerywrapper}
+
+Mongoose now always calls SchemaType `castForQuery()` method with 3 arguments: `$conditional`, `value`, and `context`.
+If you've implemented a custom schema type that defines its own `castForQuery()` method, you need to update the method as follows.
+
+```javascript
+// Mongoose 6.x format:
+MySchemaType.prototype.castForQuery = function($conditional, value) {
+ if (arguments.length === 2) {
+ // Handle casting value with `$conditional` - $eq, $in, $not, etc.
+ } else {
+ value = $conditional;
+ // Handle casting `value` with no conditional
+ }
+};
+
+// Mongoose 7.x format
+MySchemaType.prototype.castForQuery = function($conditional, value, context) {
+ if ($conditional != null) {
+ // Handle casting value with `$conditional` - $eq, $in, $not, etc.
+ } else {
+ // Handle casting `value` with no conditional
+ }
+};
+```
+
+## Copy Schema options in `Schema.prototype.add()` {#copy-schema-options-in-schema-prototype-add}
+
+Mongoose now copies user defined schema options when adding one schema to another.
+For example, `childSchema` below will get `baseSchema`'s `id` and `toJSON` options.
+
+```javascript
+const baseSchema = new Schema({ created: Date }, { id: true, toJSON: { virtuals: true } });
+const childSchema = new Schema([baseSchema, { name: String }]);
+
+childSchema.options.toJSON; // { virtuals: true } in Mongoose 7. undefined in Mongoose 6.
+```
+
+This applies both when creating a new schema using an array of schemas, as well as when calling `add()` as follows.
+
+```javascript
+childSchema.add(new Schema({}, { toObject: { virtuals: true } }));
+
+childSchema.options.toObject; // { virtuals: true } in Mongoose 7. undefined in Mongoose 6.
+```
+
+## ObjectId bsontype now has lowercase d {#objectid-bsontype-now-has-lowercase-d}
+
+The internal `_bsontype` property on ObjectIds is equal to `'ObjectId'` in Mongoose 7, as opposed to `'ObjectID'` in Mongoose 6.
+
+```javascript
+const oid = new mongoose.Types.ObjectId();
+
+oid._bsontype; // 'ObjectId' in Mongoose 7, 'ObjectID' in older versions of Mongoose
+```
+
+Please update any places where you use `_bsontype` to check if an object is an ObjectId.
+This may also affect libraries that use Mongoose.
+
+## Removed `mapReduce` {#removed-mapreduce}
+
+MongoDB no longer supports `mapReduce`, so Mongoose 7 no longer has a `Model.mapReduce()` function.
+Use the aggregation framework as a replacement for `mapReduce()`.
+
+```javascript
+// The following no longer works in Mongoose 7.
+const o = {
+ map: function() {
+ emit(this.author, 1);
+ },
+ reduce: function(k, vals) {
+ return vals.length;
+ }
+};
+
+await MR.mapReduce(o);
+```
+
+## Removed Support for custom promise libraries {#removed-support-for-custom-promise-libraries}
+
+Mongoose 7 no longer supports plugging in custom promise libraries. So the following no longer makes Mongoose return Bluebird promises in Mongoose 7.
+
+```javascript
+const mongoose = require('mongoose');
+
+// No-op on Mongoose 7
+mongoose.Promise = require('bluebird');
+```
+
+If you want to use Bluebird for all promises globally, you can do the following:
+
+```javascript
+global.Promise = require('bluebird');
+```
+
+## Deprecated `keepAlive` {#deprecated-keepalive}
+
+Before Mongoose 5.2.0, you needed to enable the `keepAlive` option to initiate [TCP keepalive](https://tldp.org/HOWTO/TCP-Keepalive-HOWTO/overview.html) to prevent `"connection closed"` errors.
+However, `keepAlive` has been `true` by default since Mongoose 5.2.0, and the `keepAlive` is deprecated as of Mongoose 7.2.0.
+Please remove `keepAlive` and `keepAliveInitialDelay` options from your Mongoose connections.
+
+## TypeScript-specific Changes {#typescript-specific-changes}
+
+### Removed `LeanDocument` and support for `extends Document` {#removed-leandocument-and-support-for-extends-document}
+
+Mongoose 7 no longer exports a `LeanDocument` type, and no longer supports passing a document type that `extends Document` into `Model<>`.
+
+```ts
+// No longer supported
+interface ITest extends Document {
+ name?: string;
+}
+const Test = model('Test', schema);
+
+// Do this instead, no `extends Document`
+interface ITest {
+ name?: string;
+}
+const Test = model('Test', schema);
+
+// If you need to access the hydrated document type, use the following code
+type TestDocument = ReturnType<(typeof Test)['hydrate']>;
+```
+
+### New Parameters for `HydratedDocument` {#new-parameters-for-hydrateddocument}
+
+Mongoose's `HydratedDocument` type transforms a raw document interface into the type of the hydrated Mongoose document, including virtuals, methods, etc.
+In Mongoose 7, the generic parameters to `HydratedDocument` have changed.
+In Mongoose 6, the generic parameters were:
+
+```ts
+type HydratedDocument<
+ DocType,
+ TMethodsAndOverrides = {},
+ TVirtuals = {}
+> = Document &
+Require_id &
+TMethodsAndOverrides &
+TVirtuals;
+```
+
+In Mongoose 7, the new type is as follows.
+
+```ts
+type HydratedDocument<
+ DocType,
+ TOverrides = {},
+ TQueryHelpers = {}
+> = Document &
+Require_id &
+TOverrides;
+```
+
+In Mongoose 7, the first parameter is the raw document interface, the 2nd parameter is any document-specific overrides (usually virtuals and methods), and the 3rd parameter is any query helpers associated with the document's model.
+
+The key difference is that, in Mongoose 6, the 3rd generic param was the document's *virtuals*.
+In Mongoose 7, the 3rd generic param is the document's *query helpers*.
+
+```ts
+// Mongoose 6 version:
+type UserDocument = HydratedDocument;
+
+// Mongoose 7:
+type UserDocument = HydratedDocument;
+```
diff --git a/docs/migrating_to_8.md b/docs/migrating_to_8.md
new file mode 100644
index 00000000000..e2748dcb9cd
--- /dev/null
+++ b/docs/migrating_to_8.md
@@ -0,0 +1,314 @@
+# Migrating from 7.x to 8.x
+
+
+
+There are several backwards-breaking changes you should be aware of when migrating from Mongoose 7.x to Mongoose 8.x.
+
+If you're still on Mongoose 6.x or earlier, please read the [Mongoose 6.x to 7.x migration guide](migrating_to_7.html) and upgrade to Mongoose 7.x first before upgrading to Mongoose 8.
+
+We also recommend reviewing the [MongoDB Node.js driver's release notes for v6.0.0](https://github.com/mongodb/node-mongodb-native/releases/tag/v6.0.0) before upgrading to Mongoose 8.
+
+* [Removed `rawResult` option for `findOneAndUpdate()`](#removed-rawresult-option-for-findoneandupdate)
+* [`Document.prototype.deleteOne()` now returns a query](#document-prototype-deleteone-now-returns-a-query)
+* [MongoDB Node Driver 6.0](#mongodb-node-driver-6)
+* [Removed `findOneAndRemove()`](#removed-findoneandremove)
+* [Removed `count()`](#removed-count)
+* [Removed id Setter](#removed-id-setter)
+* [`null` is valid for non-required string enums](#null-is-valid-for-non-required-string-enums)
+* [Apply minimize when `save()` updates an existing document](#apply-minimize-when-save-updates-an-existing-document)
+* [Apply base schema paths before discriminator paths](#apply-base-schema-paths-before-discriminator-paths)
+* [Removed `overwrite` option for `findOneAndUpdate()`](#removed-overwrite-option-for-findoneandupdate)
+* [Changed behavior for `findOneAndUpdate()` with `orFail()` and upsert](#changed-behavior-for-findoneandupdate-with-orfail-and-upsert)
+* [`create()` waits until all saves are done before throwing any error](#create-waits-until-all-saves-are-done-before-throwing-any-error)
+* [`Model.validate()` returns copy of object](#model-validate-returns-copy-of-object)
+* [Allow `null` For Optional Fields in TypeScript](#allow-null-for-optional-fields-in-typescript)
+* [Model constructor properties are all optional in TypeScript](#model-constructor-properties-are-all-optional-in-typescript)
+* [Infer `distinct()` return types from schema](#infer-distinct-return-types-from-schema)
+
+## Removed `rawResult` option for `findOneAndUpdate()` {#removed-rawresult-option-for-findoneandupdate}
+
+The `rawResult` option for `findOneAndUpdate()`, `findOneAndReplace()`, and `findOneAndDelete()` has been replaced by the `includeResultMetadata` option.
+
+```javascript
+const filter = { name: 'Will Riker' };
+const update = { age: 29 };
+
+const res = await Character.findOneAndUpdate(filter, update, {
+ new: true,
+ upsert: true,
+ // Replace `rawResult: true` with `includeResultMetadata: true`
+ includeResultMetadata: true
+});
+```
+
+`includeResultMetadata` in Mongoose 8 behaves identically to `rawResult`.
+
+## `Document.prototype.deleteOne` now returns a query {#document-prototype-deleteone-now-returns-a-query}
+
+In Mongoose 7, `doc.deleteOne()` returned a promise that resolved to `doc`.
+In Mongoose 8, `doc.deleteOne()` returns a query for easier chaining, as well as consistency with `doc.updateOne()`.
+
+```javascript
+const numberOne = await Character.findOne({ name: 'Will Riker' });
+
+// In Mongoose 7, q is a Promise that resolves to `numberOne`
+// In Mongoose 8, q is a Query.
+const q = numberOne.deleteOne();
+
+// In Mongoose 7, `res === numberOne`
+// In Mongoose 8, `res` is a `DeleteResult`.
+const res = await q;
+```
+
+## MongoDB Node Driver 6 {#mongodb-node-driver-6}
+
+Mongoose 8 uses [v6.x of the MongoDB Node driver](https://github.com/mongodb/node-mongodb-native/releases/tag/v6.0.0).
+There's a few noteable changes in MongoDB Node driver v6 that affect Mongoose:
+
+1. The `ObjectId` constructor no longer accepts strings of length 12. In Mongoose 7, `new mongoose.Types.ObjectId('12charstring')` was perfectly valid. In Mongoose 8, `new mongoose.Types.ObjectId('12charstring')` throws an error.
+
+1. Deprecated SSL options have been removed
+
+ * `sslCA` -> `tlsCAFile`
+ * `sslCRL` -> `tlsCRLFile`
+ * `sslCert` -> `tlsCertificateKeyFile`
+ * `sslKey` -> `tlsCertificateKeyFile`
+ * `sslPass` -> `tlsCertificateKeyFilePassword`
+ * `sslValidate` -> `tlsAllowInvalidCertificates`
+ * `tlsCertificateFile` -> `tlsCertificateKeyFile`
+
+## Removed `findOneAndRemove()` {#removed-findoneandremove}
+
+In Mongoose 7, `findOneAndRemove()` was an alias for `findOneAndDelete()` that Mongoose supported for backwards compatibility.
+Mongoose 8 no longer supports `findOneAndRemove()`.
+Use `findOneAndDelete()` instead.
+
+Similarly, Mongoose 8 no longer supports `findByIdAndRemove()`, which was an alias for `findByIdAndDelete()`.
+Please use `findByIdAndDelete()` instead.
+
+## Removed `count()` {#removed-count}
+
+`Model.count()` and `Query.prototype.count()` were removed in Mongoose 8. Use `Model.countDocuments()` and `Query.prototype.countDocuments()` instead.
+
+## Removed id Setter {#removed-id-setter}
+
+In Mongoose 7.4, Mongoose introduced an `id` setter that made `doc.id = '0'.repeat(24)` equivalent to `doc._id = '0'.repeat(24)`.
+In Mongoose 8, that setter is now removed.
+
+## `null` is valid for non-required string enums {#null-is-valid-for-non-required-string-enums}
+
+Before Mongoose 8, setting a string path with an `enum` to `null` would lead to a validation error, even if that path wasn't `required`.
+In Mongoose 8, it is valid to set a string path to `null` if `required` is not set, even with `enum`.
+
+```javascript
+const schema = new Schema({
+ status: {
+ type: String,
+ enum: ['on', 'off']
+ }
+});
+const Test = mongoose.model('Test', schema);
+
+// Works fine in Mongoose 8
+// Throws a `ValidationError` in Mongoose 7
+await Test.create({ status: null });
+```
+
+## Apply minimize when `save()` updates an existing document {#apply-minimize-when-save-updates-an-existing-document}
+
+In Mongoose 7, Mongoose would only apply minimize when saving a new document, not when updating an existing document.
+
+```javascript
+const schema = new Schema({
+ nested: {
+ field1: Number
+ }
+});
+const Test = mongoose.model('Test', schema);
+
+// Both Mongoose 7 and Mongoose 8 strip out empty objects when saving
+// a new document in MongoDB by default
+const { _id } = await Test.create({ nested: {} });
+let rawDoc = await Test.findById(_id).lean();
+rawDoc.nested; // undefined
+
+// Mongoose 8 will also strip out empty objects when saving an
+// existing document in MongoDB
+const doc = await Test.findById(_id);
+doc.nested = {};
+doc.markModified('nested');
+await doc.save();
+
+let rawDoc = await Test.findById(_id).lean();
+rawDoc.nested; // undefined in Mongoose 8, {} in Mongoose 7
+```
+
+## Apply base schema paths before discriminator paths {#apply-base-schema-paths-before-discriminator-paths}
+
+This means that, in Mongoose 8, getters and setters on discriminator paths run *after* getters and setters on base paths.
+In Mongoose 7, getters and setters on discriminator paths ran *before* getters and setters on base paths.
+
+```javascript
+
+const schema = new Schema({
+ name: {
+ type: String,
+ get(v) {
+ console.log('Base schema getter');
+ return v;
+ }
+ }
+});
+
+const Test = mongoose.model('Test', schema);
+const D = Test.discriminator('D', new Schema({
+ otherProp: {
+ type: String,
+ get(v) {
+ console.log('Discriminator schema getter');
+ return v;
+ }
+ }
+}));
+
+const doc = new D({ name: 'test', otherProp: 'test' });
+// In Mongoose 8, prints "Base schema getter" followed by "Discriminator schema getter"
+// In Mongoose 7, prints "Discriminator schema getter" followed by "Base schema getter"
+console.log(doc.toObject({ getters: true }));
+```
+
+## Removed `overwrite` option for `findOneAndUpdate()` {#removed-overwrite-option-for-findoneandupdate}
+
+Mongoose 7 and earlier supported an `overwrite` option for `findOneAndUpdate()`, `updateOne()`, and `update()`.
+Before Mongoose 7, `overwrite` would skip wrapping the `update` parameter in `$set`, which meant that `findOneAndUpdate()` and `update()` would overwrite the matched document.
+In Mongoose 7, setting `overwrite` would convert `findOneAndUpdate()` to `findOneAndReplace()` and `updateOne()` to `replaceOne()` to retain backwards compatibility.
+
+In Mongoose 8, the `overwrite` option is no longer supported.
+If you want to overwrite the entire document, use `findOneAndReplace()` or `replaceOne()`.
+
+## Changed behavior for `findOneAndUpdate()` with `orFail()` and upsert {#changed-behavior-for-findoneandupdate-with-orfail-and-upsert}
+
+In Mongoose 7, `findOneAndUpdate(filter, update, { upsert: true }).orFail()` would throw a `DocumentNotFoundError` if a new document was upserted.
+In other words, `findOneAndUpdate().orFail()` always threw an error if no document was found, even if a new document was upserted.
+
+In Mongoose 8, `findOneAndUpdate(filter, update, { upsert: true }).orFail()` always succeeds.
+`findOneAndUpdate().orFail()` now throws a `DocumentNotFoundError` if there's no document returned, rather than if no document was found.
+
+## Create waits until all saves are done before throwing any error {#create-waits-until-all-saves-are-done-before-throwing-any-error}
+
+In Mongoose 7, `create()` would immediately throw if any `save()` threw an error by default.
+Mongoose 8 instead waits for all `save()` calls to finish before throwing the first error that occurred.
+So `create()` will throw the same error in both Mongoose 7 and Mongoose 8, Mongoose 8 just may take longer to throw the error.
+
+```javascript
+const schema = new Schema({
+ name: {
+ type: String,
+ enum: ['Badger', 'Mushroom']
+ }
+});
+schema.pre('save', async function() {
+ await new Promise(resolve => setTimeout(resolve, 1000));
+});
+const Test = mongoose.model('Test', schema);
+
+const err = await Test.create([
+ { name: 'Badger' },
+ { name: 'Mushroom' },
+ { name: 'Cow' }
+]).then(() => null, err => err);
+err; // ValidationError
+
+// In Mongoose 7, there would be 0 documents, because `Test.create()`
+// would throw before 'Badger' and 'Mushroom' are inserted
+// In Mongoose 8, there will be 2 documents. `Test.create()` waits until
+// 'Badger' and 'Mushroom' are inserted before throwing.
+await Test.countDocuments();
+```
+
+## `Model.validate()` returns copy of object {#model-validate-returns-copy-of-object}
+
+In Mongoose 7, `Model.validate()` would potentially modify the passed in object.
+Mongoose 8 instead copies the passed in object first.
+
+```javascript
+const schema = new Schema({ answer: Number });
+const Test = mongoose.model('Test', schema);
+
+const obj = { answer: '42' };
+const res = Test.validate(obj);
+
+typeof obj.answer; // 'string' in Mongoose 8, 'number' in Mongoose 7
+typeof res.answer; // 'number' in both Mongoose 7 and Mongoose 8
+```
+
+## Allow `null` For Optional Fields in TypeScript {#allow-null-for-optional-fields-in-typescript}
+
+In Mongoose 8, automatically inferred schema types in TypeScript allow `null` for optional fields.
+In Mongoose 7, optional fields only allowed `undefined`, not `null`.
+
+```typescript
+const schema = new Schema({ name: String });
+const TestModel = model('Test', schema);
+
+const doc = new TestModel();
+
+// In Mongoose 8, this type is `string | null | undefined`.
+// In Mongoose 7, this type is `string | undefined`
+doc.name;
+```
+
+## Model constructor properties are all optional in TypeScript {#model-constructor-properties-are-all-optional-in-typescript}
+
+In Mongoose 8, no properties are required on model constructors by default.
+
+```ts
+import {Schema, model, Model} from 'mongoose';
+
+interface IDocument {
+ name: string;
+ createdAt: Date;
+ updatedAt: Date;
+}
+
+const documentSchema = new Schema(
+ { name: { type: String, required: true } },
+ { timestamps: true }
+);
+
+const TestModel = model('Document', documentSchema);
+
+// Would throw a compile error in Mongoose 7, compiles in Mongoose 8
+const newDoc = new TestModel({
+ name: 'Foo'
+});
+
+// Explicitly pass generic param to constructor to specify the expected
+// type of the model constructor param. The following will cause TS
+// to complain about missing `createdAt` and `updatedAt` in Mongoose 8.
+const newDoc2 = new TestModel({
+ name: 'Foo'
+});
+```
+
+## Infer `distinct()` return types from schema {#infer-distinct-return-types-from-schema}
+
+```ts
+interface User {
+ name: string;
+ email: string;
+ avatar?: string;
+}
+const schema = new Schema({
+ name: { type: String, required: true },
+ email: { type: String, required: true },
+ avatar: String
+});
+
+// Works in Mongoose 8. Compile error in Mongoose 7.
+const names: string[] = await MyModel.distinct('name');
+```
diff --git a/docs/migration.md b/docs/migration.md
index 17ed7ba8a14..dc9b112ca8b 100644
--- a/docs/migration.md
+++ b/docs/migration.md
@@ -2,7 +2,7 @@
There are several [backwards-breaking changes](https://github.com/Automattic/mongoose/wiki/4.0-Release-Notes) to be aware of when migrating from Mongoose 3 to Mongoose 4.
-`findOneAndUpdate()` new field is now `false` by default
+## `findOneAndUpdate()` new field is now `false` by default {#findandmodify-new}
Mongoose's `findOneAndUpdate()`, `findOneAndRemove()`,
`findByIdAndUpdate()`, and `findByIdAndRemove()` functions are just
@@ -25,7 +25,7 @@ MyModel.findOneAndUpdate({}, { $set: { test: 1 } }, { new: true }, callback);
In Mongoose 3, CastError and ValidationError had a `type` field. For instance, user defined validation errors would have a `type` property that contained the string 'user defined'. In Mongoose 4, this property has been renamed to `kind` due to [the V8 JavaScript engine using the Error.type property internally](https://code.google.com/p/v8/issues/detail?id=2397).
-Query now has a `.then()` function
+## Query now has a `.then()` function {#promises}
In mongoose 3, you needed to call `.exec()` on a query chain to get a
promise back, like `MyModel.find().exec().then();`. Mongoose 4 queries are
@@ -34,8 +34,8 @@ you're using functions like
[q's `Q.ninvoke()`](https://github.com/kriskowal/q#adapting-node) or
otherwise returning a mongoose query from a promise.
-More Info
+## More Info {#moreinfo}
Related blog posts:
-- [Introducing Version 4.0 of the Mongoose NodeJS ODM](http://www.mongodb.com/blog/post/introducing-version-40-mongoose-nodejs-odm)
+* [Introducing Version 4.0 of the Mongoose NodeJS ODM](http://www.mongodb.com/blog/post/introducing-version-40-mongoose-nodejs-odm)
diff --git a/docs/models.md b/docs/models.md
index 30f179744f5..a5c7b3a39dc 100644
--- a/docs/models.md
+++ b/docs/models.md
@@ -13,17 +13,17 @@ reading documents from the underlying MongoDB database.
* [Change Streams](#change-streams)
* [Views](#views)
-
+## Compiling your first model {#compiling}
-When you call `mongoose.model()` on a schema, Mongoose _compiles_ a model
+When you call `mongoose.model()` on a schema, Mongoose *compiles* a model
for you.
```javascript
-const schema = new mongoose.Schema({ name: 'string', size: 'string' });
+const schema = new mongoose.Schema({ name: String, size: String });
const Tank = mongoose.model('Tank', schema);
```
-The first argument is the _singular_ name of the collection your model is
+The first argument is the *singular* name of the collection your model is
for. **Mongoose automatically looks for the plural, lowercased version of your model name.**
Thus, for the example above, the model Tank is for the **tanks** collection
in the database.
@@ -41,22 +41,14 @@ them and saving to the database is easy.
const Tank = mongoose.model('Tank', yourSchema);
const small = new Tank({ size: 'small' });
-small.save(function(err) {
- if (err) return handleError(err);
- // saved!
-});
+await small.save();
// or
-Tank.create({ size: 'small' }, function(err, small) {
- if (err) return handleError(err);
- // saved!
-});
+await Tank.create({ size: 'small' });
// or, for inserting large batches of documents
-Tank.insertMany([{ size: 'small' }], function(err) {
-
-});
+await Tank.insertMany([{ size: 'small' }]);
```
Note that no tanks will be created/removed until the connection your model
@@ -64,11 +56,12 @@ uses is open. Every model has an associated connection. When you use
`mongoose.model()`, your model will use the default mongoose connection.
```javascript
-mongoose.connect('mongodb://127.0.0.1/gettingstarted');
+await mongoose.connect('mongodb://127.0.0.1/gettingstarted');
```
If you create a custom connection, use that connection's `model()` function
instead.
+
```javascript
const connection = mongoose.createConnection('mongodb://127.0.0.1:27017/test');
const Tank = connection.model('Tank', yourSchema);
@@ -80,7 +73,7 @@ Finding documents is easy with Mongoose, which supports the [rich](https://www.m
Documents can be retrieved using a `model`'s [find](api/model.html#model_Model-find), [findById](api/model.html#model_Model-findById), [findOne](api/model.html#model_Model-findOne), or [where](api/model.html#model_Model-where) static functions.
```javascript
-Tank.find({ size: 'small' }).where('createdDate').gt(oneYearAgo).exec(callback);
+await Tank.find({ size: 'small' }).where('createdDate').gt(oneYearAgo).exec();
```
See the chapter on [queries](queries.html) for more details on how to use the [Query](api/query.html) api.
@@ -91,10 +84,7 @@ Models have static `deleteOne()` and `deleteMany()` functions
for removing all documents matching the given `filter`.
```javascript
-Tank.deleteOne({ size: 'large' }, function(err) {
- if (err) return handleError(err);
- // deleted at most one tank document
-});
+await Tank.deleteOne({ size: 'large' });
```
## Updating
@@ -104,15 +94,14 @@ database without returning them to your application. See the
[API](api/model.html#model_Model-updateOne) docs for more detail.
```javascript
-Tank.updateOne({ size: 'large' }, { name: 'T-90' }, function(err, res) {
- // Updated at most one doc, `res.nModified` contains the number
- // of docs that MongoDB updated
-});
+// Updated at most one doc, `res.nModified` contains the number
+// of docs that MongoDB updated
+await Tank.updateOne({ size: 'large' }, { name: 'T-90' });
```
-_If you want to update a single document in the db and return it to your
+*If you want to update a single document in the db and return it to your
application, use [findOneAndUpdate](api/model.html#model_Model-findOneAndUpdate)
-instead._
+instead.*
## Change Streams
@@ -204,7 +193,7 @@ If you attempt to `save()` a document from a View, you will get an error from th
## Yet more
-The [API docs](api/model.html#model_Model) cover many additional methods available like [count](api/model.html#model_Model-count), [mapReduce](api/model.html#model_Model-mapReduce), [aggregate](api/model.html#model_Model-aggregate), and [more](api/model.html#model_Model-findOneAndRemove).
+The [API docs](api/model.html#model_Model) cover many additional methods available like [count](api/model.html#model_Model-count), [mapReduce](api/model.html#model_Model-mapReduce), [aggregate](api/model.html#model_Model-aggregate), and more.
## Next Up
diff --git a/docs/nextjs.md b/docs/nextjs.md
new file mode 100644
index 00000000000..673587c6829
--- /dev/null
+++ b/docs/nextjs.md
@@ -0,0 +1,38 @@
+# Using Mongoose With [Next.js](https://nextjs.org/)
+
+Next.js is a popular framework for building full stack applications with React.
+Mongoose works out of the box with Next.js.
+If you're looking to get started, please use [Next.js' official Mongoose sample app](https://github.com/vercel/next.js/tree/canary/examples/with-mongodb-mongoose).
+Furthermore, if you are using Next.js with [Vercel Serverless Functions](https://vercel.com/docs/concepts/functions/serverless-functions), please review [Mongoose's AWS Lambda docs](https://mongoosejs.com/docs/lambda.html).
+
+There are a few common issues when working with Next.js that you should be aware of.
+
+## TypeError: Cannot read properties of undefined (reading 'prototype')
+
+You can fix this issue by adding the following to your `next.config.js`:
+
+```js
+const nextConfig = {
+ experimental: {
+ esmExternals: "loose", // <-- add this
+ serverComponentsExternalPackages: ["mongoose"] // <-- and this
+ },
+ // and the following to enable top-level await support for Webpack
+ webpack: (config) => {
+ config.experiments = {
+ topLevelAwait: true
+ };
+ return config;
+ },
+}
+```
+
+This issue is caused by [this change in MongoDB's bson parser](https://github.com/mongodb/js-bson/pull/564/files).
+MongoDB's bson parser uses top-level await and dynamic import in ESM mode to avoid some Webpack bundling issues.
+And Next.js forces ESM mode.
+
+## Next.js Edge Runtime
+
+Mongoose does **not** currently support [Next.js Edge Runtime](https://nextjs.org/docs/app/building-your-application/rendering/edge-and-nodejs-runtimes#edge-runtime).
+While you can import Mongoose in Edge Runtime, you'll get [Mongoose's browser library](browser.html).
+There is no way for Mongoose to connect to MongoDB in Edge Runtime, because [Edge Runtime currently doesn't support Node.js `net` API](https://edge-runtime.vercel.app/features/available-apis#unsupported-apis), which is what the MongoDB Node Driver uses to connect to MongoDB.
diff --git a/docs/plugins.md b/docs/plugins.md
index ea11b8e8ec6..1cbfedcd3ba 100644
--- a/docs/plugins.md
+++ b/docs/plugins.md
@@ -9,7 +9,7 @@ Schemas are pluggable, that is, they allow for applying pre-packaged capabilitie
Officially Supported Plugins
-
+## Example {#example}
Plugins are a tool for reusing logic in multiple schemas. Suppose you have
several models in your database and want to add a `loadedAt` property
@@ -44,9 +44,9 @@ const playerSchema = new Schema({ /* ... */ });
playerSchema.plugin(loadedAtPlugin);
```
-We just added last-modified behavior to both our `Game` and `Player` schemas and declared an index on the `lastMod` path of our Games to boot. Not bad for a few lines of code.
+We just added loaded-time behavior to both our `Game` and `Player` schemas and declared an index on the `loadedAt` path of our Games to boot. Not bad for a few lines of code.
-
+## Global Plugins {#global}
Want to register a plugin for all schemas? The mongoose singleton has a
`.plugin()` function that registers a plugin for every schema. For
@@ -63,7 +63,7 @@ const Game = mongoose.model('Game', gameSchema);
const Player = mongoose.model('Player', playerSchema);
```
-
+## Apply Plugins Before Compiling Models {#apply-plugins-before-compiling-models}
Because many plugins rely on [middleware](middleware.html), you should make sure to apply plugins **before**
you call `mongoose.model()` or `conn.model()`. Otherwise, [any middleware the plugin registers won't get applied](middleware.html#defining).
@@ -96,7 +96,7 @@ const Game = mongoose.model('Game', gameSchema);
gameSchema.plugin(loadedAtPlugin);
```
-
+## Officially Supported Plugins {#official}
The Mongoose team maintains several plugins that add cool new features to
Mongoose. Here's a couple:
@@ -107,7 +107,7 @@ Mongoose. Here's a couple:
You can find a full list of officially supported plugins on [Mongoose's plugins search site](https://plugins.mongoosejs.io/).
-## Community!
+## Community
Not only can you re-use schema functionality in your own projects, but you
also reap the benefits of the Mongoose community as well. Any plugin
diff --git a/docs/populate.md b/docs/populate.md
index a850383688a..c17710a397a 100644
--- a/docs/populate.md
+++ b/docs/populate.md
@@ -31,10 +31,6 @@ what tells Mongoose which model to use during population, in our case
the `Story` model. All `_id`s we store here must be document `_id`s from
the `Story` model.
-**Note**: `ObjectId`, `Number`, `String`, and `Buffer` are valid for use
-as refs. However, you should use `ObjectId` unless you are an advanced
-user and have a good reason for doing so.
-
-
+## Saving refs {#saving-refs}
Saving refs to other documents works the same way you normally save
properties, just assign the `_id` value:
@@ -72,36 +69,35 @@ const author = new Person({
age: 50
});
-author.save(function(err) {
- if (err) return handleError(err);
-
- const story1 = new Story({
- title: 'Casino Royale',
- author: author._id // assign the _id from the person
- });
+await author.save();
- story1.save(function(err) {
- if (err) return handleError(err);
- // that's it!
- });
+const story1 = new Story({
+ title: 'Casino Royale',
+ author: author._id // assign the _id from the person
});
+
+await story1.save();
+// that's it!
```
-
+You can set the `ref` option on `ObjectId`, `Number`, `String`, and `Buffer` paths.
+`populate()` works with ObjectIds, numbers, strings, and buffers.
+However, we recommend using ObjectIds as `_id` properties (and thus ObjectIds for `ref` properties) unless you have a good reason not to.
+That is because MongoDB will set `_id` to an ObjectId if you create a new document without an `_id` property, so if you make your `_id` property a Number, you need to be extra careful not to insert a document without a numeric `_id`.
+
+## Population {#population}
So far we haven't done anything much different. We've merely created a
`Person` and a `Story`. Now let's take a look at populating our story's
`author` using the query builder:
```javascript
-Story.
+const story = await Story.
findOne({ title: 'Casino Royale' }).
populate('author').
- exec(function(err, story) {
- if (err) return handleError(err);
- console.log('The author is %s', story.author.name);
- // prints "The author is Ian Fleming"
- });
+ exec();
+// prints "The author is Ian Fleming"
+console.log('The author is %s', story.author.name);
```
Populated paths are no longer set to their original `_id` , their value
@@ -110,24 +106,47 @@ performing a separate query before returning the results.
Arrays of refs work the same way. Just call the
[populate](api/query.html#query_Query-populate) method on the query and an
-array of documents will be returned _in place_ of the original `_id`s.
+array of documents will be returned *in place* of the original `_id`s.
-
+## Setting Populated Fields {#setting-populated-fields}
You can manually populate a property by setting it to a document. The document
must be an instance of the model your `ref` property refers to.
```javascript
-Story.findOne({ title: 'Casino Royale' }, function(error, story) {
- if (error) {
- return handleError(error);
- }
- story.author = author;
- console.log(story.author.name); // prints "Ian Fleming"
-});
+const story = await Story.findOne({ title: 'Casino Royale' });
+story.author = author;
+console.log(story.author.name); // prints "Ian Fleming"
```
-
+You can also push documents or POJOs onto a populated array, and Mongoose will add those documents if their `ref` matches.
+
+```javascript
+const fan1 = await Person.create({ name: 'Sean' });
+await Story.updateOne({ title: 'Casino Royale' }, { $push: { fans: { $each: [fan1._id] } } });
+
+const story = await Story.findOne({ title: 'Casino Royale' }).populate('fans');
+story.fans[0].name; // 'Sean'
+
+const fan2 = await Person.create({ name: 'George' });
+story.fans.push(fan2);
+story.fans[1].name; // 'George'
+
+story.fans.push({ name: 'Roger' });
+story.fans[2].name; // 'Roger'
+```
+
+If you push a non-POJO and non-document value, like an ObjectId, Mongoose `>= 8.7.0` will depopulate the entire array.
+
+```javascript
+const fan4 = await Person.create({ name: 'Timothy' });
+story.fans.push(fan4._id); // Push the `_id`, not the full document
+
+story.fans[0].name; // undefined, `fans[0]` is now an ObjectId
+story.fans[0].toString() === fan1._id.toString(); // true
+```
+
+## Checking Whether a Field is Populated {#checking-populated}
You can call the `populated()` function to check whether a field is populated.
If `populated()` returns a [truthy value](https://masteringjs.io/tutorials/fundamentals/truthy),
@@ -155,7 +174,7 @@ story.author instanceof ObjectId; // true
story.author._id; // ObjectId, because Mongoose adds a special getter
```
-
+## What If There's No Foreign Document? {#doc-not-found}
Mongoose populate doesn't behave like conventional
[SQL joins](https://www.w3schools.com/sql/sql_join.asp). When there's no
@@ -184,7 +203,7 @@ const story = await Story.findOne({ title: 'Casino Royale' }).populate('authors'
story.authors; // `[]`
```
-
+## Field Selection {#field-selection}
What if we only want a few specific fields returned for the populated
documents? This can be accomplished by passing the usual
@@ -192,26 +211,22 @@ documents? This can be accomplished by passing the usual
to the populate method:
```javascript
-Story.
+const story = await Story.
findOne({ title: /casino royale/i }).
- populate('author', 'name'). // only return the Persons name
- exec(function(err, story) {
- if (err) return handleError(err);
-
- console.log('The author is %s', story.author.name);
- // prints "The author is Ian Fleming"
-
- console.log('The authors age is %s', story.author.age);
- // prints "The authors age is null"
- });
+ populate('author', 'name').
+ exec(); // only return the Persons name
+// prints "The author is Ian Fleming"
+console.log('The author is %s', story.author.name);
+// prints "The authors age is null"
+console.log('The authors age is %s', story.author.age);
```
-
+## Populating Multiple Paths {#populating-multiple-paths}
What if we wanted to populate multiple paths at the same time?
```javascript
-Story.
+await Story.
find({ /* ... */ }).
populate('fans').
populate('author').
@@ -224,21 +239,21 @@ one will take effect.
```javascript
// The 2nd `populate()` call below overwrites the first because they
// both populate 'fans'.
-Story.
+await Story.
find().
populate({ path: 'fans', select: 'name' }).
populate({ path: 'fans', select: 'email' });
// The above is equivalent to:
-Story.find().populate({ path: 'fans', select: 'email' });
+await Story.find().populate({ path: 'fans', select: 'email' });
```
-
+## Query conditions and other options {#query-conditions}
What if we wanted to populate our fans array based on their age and
select just their names?
```javascript
-Story.
+await Story.
find().
populate({
path: 'fans',
@@ -258,7 +273,7 @@ the story's `author` will be `null`.
```javascript
const story = await Story.
findOne({ title: 'Casino Royale' }).
- populate({ path: 'author', name: { $ne: 'Ian Fleming' } }).
+ populate({ path: 'author', match: { name: { $ne: 'Ian Fleming' } } }).
exec();
story.author; // `null`
```
@@ -276,14 +291,14 @@ story; // null
If you want to filter stories by their author's name, you should use [denormalization](https://www.mongodb.com/blog/post/6-rules-of-thumb-for-mongodb-schema-design-part-3).
-
+## `limit` vs. `perDocumentLimit` {#limit-vs-perDocumentLimit}
Populate does support a `limit` option, however, it currently
does **not** limit on a per-document basis for backwards compatibility. For example,
suppose you have 2 stories:
```javascript
-Story.create([
+await Story.create([
{ title: 'Casino Royale', fans: [1, 2, 3, 4, 5, 6, 7, 8] },
{ title: 'Live and Let Die', fans: [9, 10] }
]);
@@ -328,7 +343,7 @@ stories[1].name; // 'Live and Let Die'
stories[1].fans.length; // 2
```
-
+## Refs to children {#refs-to-children}
We may find however, if we use the `author` object, we are unable to get a
list of the stories. This is because no `story` objects were ever 'pushed'
@@ -341,22 +356,20 @@ But, if you have a good reason to want an array of child pointers, you
can `push()` documents onto the array as shown below.
```javascript
-story1.save();
+await story1.save();
author.stories.push(story1);
-author.save(callback);
+await author.save();
```
This allows us to perform a `find` and `populate` combo:
```javascript
-Person.
+const person = await Person.
findOne({ name: 'Ian Fleming' }).
- populate('stories'). // only works if we pushed refs to children
- exec(function(err, person) {
- if (err) return handleError(err);
- console.log(person);
- });
+ populate('stories').
+ exec(); // only works if we pushed refs to children
+console.log(person);
```
It is debatable that we really want two sets of pointers as they may get
@@ -364,12 +377,10 @@ out of sync. Instead we could skip populating and directly `find()` the
stories we are interested in.
```javascript
-Story.
+const stories = await Story.
find({ author: author._id }).
- exec(function(err, stories) {
- if (err) return handleError(err);
- console.log('The stories are an array: ', stories);
- });
+ exec();
+console.log('The stories are an array: ', stories);
```
The documents returned from
@@ -380,7 +391,7 @@ them with [sub docs](subdocs.html). Take caution when calling its
remove method because you'll be removing it from the database, not just
the array.
-
+## Populating an existing document {#populate_an_existing_mongoose_document}
If you have an existing mongoose document and want to populate some of its
paths, you can use the
@@ -406,15 +417,15 @@ await person.populate(['stories', 'fans']);
person.populated('fans'); // Array of ObjectIds
```
-
+## Populating multiple existing documents {#populate_multiple_documents}
If we have one or many mongoose documents or even plain objects
-(_like [mapReduce](api/model.html#model_Model-mapReduce) output_), we may
+(*like [mapReduce](api/model.html#model_Model-mapReduce) output*), we may
populate them using the [Model.populate()](api/model.html#model_Model-populate)
method. This is what `Document#populate()`
and `Query#populate()` use to populate documents.
-
+## Populating across multiple levels {#deep-populate}
Say you have a user schema which keeps track of the user's friends.
@@ -430,7 +441,7 @@ wanted a user's friends of friends? Specify the `populate` option to tell
mongoose to populate the `friends` array of all the user's friends:
```javascript
-User.
+await User.
findOne({ name: 'Val' }).
populate({
path: 'friends',
@@ -439,7 +450,7 @@ User.
});
```
-
+## Cross Database Populate {#cross-db-populate}
Let's say you have a schema representing events, and a schema representing
conversations. Each event has a corresponding conversation thread.
@@ -486,8 +497,9 @@ const events = await Event.
populate({ path: 'conversation', model: Conversation });
```
-
-
+## Dynamic References via `refPath` {#dynamic-refpath}
+
+Mongoose can also populate from multiple collections based on the value
of a property in the document. Let's say you're building a schema for
storing comments. A user may comment on either a blog post or a product.
@@ -577,7 +589,54 @@ also need an extra `populate()` call for every property, unless you use
Using `refPath` means you only need 2 schema paths and one `populate()` call
regardless of how many models your `commentSchema` can point to.
-
+You could also assign a function to `refPath`, which means Mongoose selects a refPath depending on a value on the document being populated.
+
+```javascript
+const commentSchema = new Schema({
+ body: { type: String, required: true },
+ commentType: {
+ type: String,
+ enum: ['comment', 'review']
+ },
+ entityId: {
+ type: Schema.Types.ObjectId,
+ required: true,
+ refPath: function () {
+ return this.commentType === 'review' ? this.reviewEntityModel : this.commentEntityModel; // 'this' refers to the document being populated
+ }
+ },
+ commentEntityModel: {
+ type: String,
+ required: true,
+ enum: ['BlogPost', 'Review']
+ },
+ reviewEntityModel: {
+ type: String,
+ required: true,
+ enum: ['Vendor', 'Product']
+ }
+});
+```
+
+## Dynamic References via `ref` {#dynamic-ref}
+
+Just like `refPath`, `ref` can also be assigned a function.
+
+```javascript
+const commentSchema = new Schema({
+ body: { type: String, required: true },
+ verifiedBuyer: Boolean
+ doc: {
+ type: Schema.Types.ObjectId,
+ required: true,
+ ref: function() {
+ return this.verifiedBuyer ? 'Product' : 'BlogPost'; // 'this' refers to the document being populated
+ }
+ },
+});
+```
+
+## Populate Virtuals {#populate-virtuals}
So far you've only populated based on the `_id` field.
However, that's sometimes not the right choice.
@@ -623,7 +682,7 @@ const BlogPostSchema = new Schema({
```
Unfortunately, these two schemas, as written, don't support populating an author's list of blog posts.
-That's where _virtual populate_ comes in.
+That's where *virtual populate* comes in.
Virtual populate means calling `populate()` on a virtual property that has a `ref` option as shown below.
```javascript
@@ -647,7 +706,7 @@ const author = await Author.findOne().populate('posts');
author.posts[0].title; // Title of the first blog post
```
-Keep in mind that virtuals are _not_ included in `toJSON()` and `toObject()` output by default.
+Keep in mind that virtuals are *not* included in `toJSON()` and `toObject()` output by default.
If you want populate virtuals to show up when using functions like Express' [`res.json()` function](https://masteringjs.io/tutorials/express/json) or `console.log()`, set the `virtuals: true` option on your schema's `toJSON` and `toObject()` options.
```javascript
@@ -674,7 +733,7 @@ authors = await Author.
exec();
```
-
+## Populate Virtuals: The Count Option {#count}
Populate virtuals also support counting the number of documents with
matching `foreignField` as opposed to the documents themselves. Set the
@@ -702,7 +761,7 @@ const doc = await Band.findOne({ name: 'Motley Crue' }).
doc.numMembers; // 2
```
-
+## Populate Virtuals: The Match Option {#match}
Another option for Populate virtuals is `match`.
This option adds an extra filter condition to the query Mongoose uses to `populate()`:
@@ -741,7 +800,30 @@ AuthorSchema.virtual('posts', {
});
```
-
+You can overwrite the `match` option when calling `populate()` as follows.
+
+```javascript
+// Overwrite the `match` option specified in `AuthorSchema.virtual()` for this
+// single `populate()` call.
+await Author.findOne().populate({ path: posts, match: {} });
+```
+
+You can also set the `match` option to a function in your `populate()` call.
+If you want to merge your `populate()` match option, rather than overwriting, use the following.
+
+```javascript
+await Author.findOne().populate({
+ path: posts,
+ // Add `isDeleted: false` to the virtual's default `match`, so the `match`
+ // option would be `{ tags: author.favoriteTags, isDeleted: false }`
+ match: (author, virtual) => ({
+ ...virtual.options.match(author),
+ isDeleted: false
+ })
+});
+```
+
+## Populating Maps {#populating-maps}
[Maps](schematypes.html#maps) are a type that represents an object with arbitrary
string keys. For example, in the below schema, `members` is a map from strings to ObjectIds.
@@ -811,7 +893,7 @@ You can `populate()` every book's author by populating `books.$*.author`:
const libraries = await Library.find().populate('books.$*.author');
```
-
+## Populate in Middleware {#populate-middleware}
You can populate in either pre or post [hooks](http://mongoosejs.com/docs/middleware.html). If you want to
always populate a certain field, check out the [mongoose-autopopulate plugin](http://npmjs.com/package/mongoose-autopopulate).
@@ -845,7 +927,7 @@ MySchema.post('save', function(doc, next) {
});
```
-
+## Populating Multiple Paths in Middleware {#populating-multiple-paths-middleware}
Populating multiple paths in middleware can be helpful when you always want to populate some fields. But, the implementation is just a tiny bit trickier than what you may think. Here's how you may expect it to work:
@@ -878,9 +960,10 @@ userSchema.pre('find', function(next) {
next();
});
```
+
Alternatively, you can check out the [mongoose-autopopulate plugin](http://npmjs.com/package/mongoose-autopopulate).
-
+## Transform populated documents {#transform-populated-documents}
You can manipulate populated documents using the `transform` option.
If you specify a `transform` function, Mongoose will call this function on every populated document in the result with two arguments: the populated document, and the original id used to populate the document.
diff --git a/docs/promises.md b/docs/promises.md
index eb4062b9f55..782c785c49a 100644
--- a/docs/promises.md
+++ b/docs/promises.md
@@ -39,8 +39,8 @@ promise chaining or [async await](https://asyncawait.net)
There are two alternatives for using `await` with queries:
-- `await Band.findOne();`
-- `await Band.findOne().exec();`
+* `await Band.findOne();`
+* `await Band.findOne().exec();`
As far as functionality is concerned, these two are equivalent.
However, we recommend using `.exec()` because that gives you
@@ -50,17 +50,6 @@ better stack traces.
[require:Should You Use `exec\(\)` With `await`]
```
-### Plugging in your own Promises Library
-
-If you're an advanced user, you may want to plug in your own promise
-library like [bluebird](https://www.npmjs.com/package/bluebird). Just set
-`mongoose.Promise` to your favorite
-ES6-style promise constructor and mongoose will use it.
-
-```javascript
-[require:Plugging in your own Promises Library]
-```
-
Want to learn how to check whether your favorite npm modules work with
async/await without cobbling together contradictory answers from Google
@@ -71,5 +60,5 @@ ES6-style promise constructor and mongoose will use it.
-
+
diff --git a/docs/queries.md b/docs/queries.md
index 9b7ffc160a9..df552c5043f 100644
--- a/docs/queries.md
+++ b/docs/queries.md
@@ -5,21 +5,20 @@ for [CRUD operations](https://en.wikipedia.org/wiki/Create,_read,_update_and_del
Each of these functions returns a
[mongoose `Query` object](api/query.html#Query).
-- [`Model.deleteMany()`](api.html#model_Model-deleteMany)
-- [`Model.deleteOne()`](api.html#model_Model-deleteOne)
-- [`Model.find()`](api.html#model_Model-find)
-- [`Model.findById()`](api.html#model_Model-findById)
-- [`Model.findByIdAndDelete()`](api.html#model_Model-findByIdAndDelete)
-- [`Model.findByIdAndRemove()`](api.html#model_Model-findByIdAndRemove)
-- [`Model.findByIdAndUpdate()`](api.html#model_Model-findByIdAndUpdate)
-- [`Model.findOne()`](api.html#model_Model-findOne)
-- [`Model.findOneAndDelete()`](api.html#model_Model-findOneAndDelete)
-- [`Model.findOneAndRemove()`](api.html#model_Model-findOneAndRemove)
-- [`Model.findOneAndReplace()`](api.html#model_Model-findOneAndReplace)
-- [`Model.findOneAndUpdate()`](api.html#model_Model-findOneAndUpdate)
-- [`Model.replaceOne()`](api.html#model_Model-replaceOne)
-- [`Model.updateMany()`](api.html#model_Model-updateMany)
-- [`Model.updateOne()`](api.html#model_Model-updateOne)
+* [`Model.deleteMany()`](api.html#model_Model-deleteMany)
+* [`Model.deleteOne()`](api.html#model_Model-deleteOne)
+* [`Model.find()`](api.html#model_Model-find)
+* [`Model.findById()`](api.html#model_Model-findById)
+* [`Model.findByIdAndDelete()`](api.html#model_Model-findByIdAndDelete)
+* [`Model.findByIdAndRemove()`](api.html#model_Model-findByIdAndRemove)
+* [`Model.findByIdAndUpdate()`](api.html#model_Model-findByIdAndUpdate)
+* [`Model.findOne()`](api.html#model_Model-findOne)
+* [`Model.findOneAndDelete()`](api.html#model_Model-findOneAndDelete)
+* [`Model.findOneAndReplace()`](api.html#model_Model-findOneAndReplace)
+* [`Model.findOneAndUpdate()`](api.html#model_Model-findOneAndUpdate)
+* [`Model.replaceOne()`](api.html#model_Model-replaceOne)
+* [`Model.updateMany()`](api.html#model_Model-updateMany)
+* [`Model.updateOne()`](api.html#model_Model-updateOne)
A mongoose query can be executed in one of two ways. First, if you
pass in a `callback` function, Mongoose will execute the query asynchronously
@@ -37,29 +36,21 @@ A query also has a `.then()` function, and thus can be used as a promise.
## Executing
-When executing a query with a `callback` function, you specify your query as a JSON document. The JSON document's syntax is the same as the [MongoDB shell](http://www.mongodb.com/docs/manual/tutorial/query-documents/).
+When executing a query, you specify your query as a JSON document. The JSON document's syntax is the same as the [MongoDB shell](http://www.mongodb.com/docs/manual/tutorial/query-documents/).
```javascript
const Person = mongoose.model('Person', yourSchema);
// find each person with a last name matching 'Ghost', selecting the `name` and `occupation` fields
-Person.findOne({ 'name.last': 'Ghost' }, 'name occupation', function(err, person) {
- if (err) return handleError(err);
- // Prints "Space Ghost is a talk show host".
- console.log('%s %s is a %s.', person.name.first, person.name.last,
- person.occupation);
-});
+const person = await Person.findOne({ 'name.last': 'Ghost' }, 'name occupation');
+// Prints "Space Ghost is a talk show host".
+console.log('%s %s is a %s.', person.name.first, person.name.last, person.occupation);
```
-Mongoose executed the query and passed the results to `callback`. All callbacks in Mongoose use the pattern:
-`callback(error, result)`. If an error occurs executing the query, the `error` parameter will contain an error document, and `result`
-will be null. If the query is successful, the `error` parameter will be null, and the `result` will be populated with the results of the query.
-
-Anywhere a callback is passed to a query in Mongoose, the callback follows the pattern `callback(error, results)`.
-What `results` is depends on the operation: For `findOne()` it is a [potentially-null single document](api/model.html#model_Model-findOne), `find()` a [list of documents](api/model.html#model_Model-find), `count()` [the number of documents](api/model.html#model_Model-count), `update()` the [number of documents affected](api/model.html#model_Model-update), etc.
-The [API docs for Models](api/model.html) provide more detail on what is passed to the callbacks.
+What `person` is depends on the operation: For `findOne()` it is a [potentially-null single document](api/model.html#model_Model-findOne), `find()` a [list of documents](api/model.html#model_Model-find), `count()` [the number of documents](api/model.html#model_Model-count), `update()` the [number of documents affected](api/model.html#model_Model-update), etc.
+The [API docs for Models](api/model.html) provide more details.
-Now let's look at what happens when no `callback` is passed:
+Now let's look at what happens when no `await` is used:
```javascript
// find each person with a last name matching 'Ghost'
@@ -69,12 +60,9 @@ const query = Person.findOne({ 'name.last': 'Ghost' });
query.select('name occupation');
// execute the query at a later time
-query.exec(function(err, person) {
- if (err) return handleError(err);
- // Prints "Space Ghost is a talk show host."
- console.log('%s %s is a %s.', person.name.first, person.name.last,
- person.occupation);
-});
+const person = await query.exec();
+// Prints "Space Ghost is a talk show host."
+console.log('%s %s is a %s.', person.name.first, person.name.last, person.occupation);
```
In the above code, the `query` variable is of type [Query](api/query.html).
@@ -83,7 +71,7 @@ The below 2 examples are equivalent.
```javascript
// With a JSON doc
-Person.
+await Person.
find({
occupation: /host/,
'name.last': 'Ghost',
@@ -93,10 +81,10 @@ Person.
limit(10).
sort({ occupation: -1 }).
select({ name: 1, occupation: 1 }).
- exec(callback);
+ exec();
// Using query builder
-Person.
+await Person.
find({ occupation: /host/ }).
where('name.last').equals('Ghost').
where('age').gt(17).lt(66).
@@ -104,65 +92,33 @@ Person.
limit(10).
sort('-occupation').
select('name occupation').
- exec(callback);
+ exec();
```
A full list of [Query helper functions can be found in the API docs](api/query.html).
-
+## Queries are Not Promises
-Mongoose queries are **not** promises. They have a `.then()`
-function for [co](https://www.npmjs.com/package/co) and
-[async/await](http://thecodebarbarian.com/common-async-await-design-patterns-in-node.js.html)
-as a convenience. However, unlike promises, calling a query's `.then()`
-can execute the query multiple times.
-
-For example, the below code will execute 3 `updateMany()` calls, one
-because of the callback, and two because `.then()` is called twice.
+Mongoose queries are **not** promises.
+Queries are [thenables](https://masteringjs.io/tutorials/fundamentals/thenable), meaning they have a `.then()` method for [async/await](http://thecodebarbarian.com/common-async-await-design-patterns-in-node.js.html) as a convenience.
+However, unlike promises, calling a query's `.then()` executes the query, so calling `then()` multiple times will throw an error.
```javascript
-const q = MyModel.updateMany({}, { isDeleted: true }, function() {
- console.log('Update 1');
-});
+const q = MyModel.updateMany({}, { isDeleted: true });
-q.then(() => console.log('Update 2'));
-q.then(() => console.log('Update 3'));
-```
-
-Don't mix using callbacks and promises with queries, or you may end up
-with duplicate operations. That's because passing a callback to a query function
-immediately executes the query, and calling [`then()`](https://masteringjs.io/tutorials/fundamentals/then)
-executes the query again.
-
-Mixing promises and callbacks can lead to duplicate entries in arrays.
-For example, the below code inserts 2 entries into the `tags` array, **not** just 1.
-
-```javascript
-const BlogPost = mongoose.model('BlogPost', new Schema({
- title: String,
- tags: [String]
-}));
-
-// Because there's both `await` **and** a callback, this `updateOne()` executes twice
-// and thus pushes the same string into `tags` twice.
-const update = { $push: { tags: ['javascript'] } };
-await BlogPost.updateOne({ title: 'Introduction to Promises' }, update, (err, res) => {
- console.log(res);
-});
+await q.then(() => console.log('Update 2'));
+// Throws "Query was already executed: Test.updateMany({}, { isDeleted: true })"
+await q.then(() => console.log('Update 3'));
```
-
+## References to other documents {#refs}
There are no joins in MongoDB but sometimes we still want references to
documents in other collections. This is where [population](populate.html)
comes in. Read more about how to include documents from other collections in
your query results [here](api/query.html#query_Query-populate).
-
+## Streaming {#streaming}
You can [stream](http://nodejs.org/api/stream.html) query results from
MongoDB. You need to call the
@@ -199,7 +155,7 @@ However, cursors can still time out because of [session idle timeouts](https://w
So even a cursor with `noCursorTimeout` set will still time out after 30 minutes
of inactivity. You can read more about working around session idle timeouts in the [MongoDB documentation](https://www.mongodb.com/docs/manual/reference/method/cursor.noCursorTimeout/#session-idle-timeout-overrides-nocursortimeout).
-
+## Versus Aggregation {#versus-aggregation}
[Aggregation](api/aggregate.html#aggregate_Aggregate) can
do many of the same things that queries can. For example, below is
@@ -241,9 +197,9 @@ const queryRes = await Person.findOne({ _id: idString });
const aggRes = await Person.aggregate([{ $match: { _id: idString } }]);
```
-
+## Sorting {#sorting}
-[Sorting](/docs/api.html#query_Query-sort) is how you can ensure you query results come back in the desired order.
+[Sorting](/docs/api.html#query_Query-sort) is how you can ensure your query results come back in the desired order.
```javascript
const personSchema = new mongoose.Schema({
@@ -324,6 +280,6 @@ As you can see, age is sorted from 0 to 2 but when age is equal, sorts by weight
];
```
-
+## Next Up {#next}
Now that we've covered `Queries`, let's take a look at [Validation](validation.html).
diff --git a/docs/schematypes.md b/docs/schematypes.md
index 1c3fb9a34f7..062b3251311 100644
--- a/docs/schematypes.md
+++ b/docs/schematypes.md
@@ -1,4 +1,4 @@
-
+# SchemaTypes
SchemaTypes handle definition of path
[defaults](api/schematype.html#schematype_SchemaType-default),
@@ -18,7 +18,7 @@ and other general characteristics for Mongoose document properties.
* [The `schema.path()` Function](#path)
* [Further Reading](#further-reading)
-
+## What is a SchemaType? {#what-is-a-schematype}
You can think of a Mongoose schema as the configuration object for a
Mongoose model. A SchemaType is then a configuration object for an individual
@@ -42,20 +42,23 @@ The following are all the valid SchemaTypes in Mongoose. Mongoose plugins
can also add custom SchemaTypes like [int32](http://plugins.mongoosejs.io/plugins/int32).
Check out [Mongoose's plugins search](http://plugins.mongoosejs.io) to find plugins.
-- [String](#strings)
-- [Number](#numbers)
-- [Date](#dates)
-- [Buffer](#buffers)
-- [Boolean](#booleans)
-- [Mixed](#mixed)
-- [ObjectId](#objectids)
-- [Array](#arrays)
-- [Decimal128](api/mongoose.html#mongoose_Mongoose-Decimal128)
-- [Map](#maps)
-- [Schema](#schemas)
-- [UUID](#uuid)
-
-Example
+* [String](#strings)
+* [Number](#numbers)
+* [Date](#dates)
+* [Buffer](#buffers)
+* [Boolean](#booleans)
+* [Mixed](#mixed)
+* [ObjectId](#objectids)
+* [Array](#arrays)
+* [Decimal128](api/mongoose.html#mongoose_Mongoose-Decimal128)
+* [Map](#maps)
+* [Schema](#schemas)
+* [UUID](#uuid)
+* [BigInt](#bigint)
+* [Double](#double)
+* [Int32](#int32)
+
+### Example
```javascript
const schema = new Schema({
@@ -67,6 +70,8 @@ const schema = new Schema({
mixed: Schema.Types.Mixed,
_someId: Schema.Types.ObjectId,
decimal: Schema.Types.Decimal128,
+ double: Schema.Types.Double,
+ int32bit: Schema.Types.Int32,
array: [],
ofString: [String],
ofNumber: [Number],
@@ -111,7 +116,7 @@ m.map = new Map([['key', 'value']]);
m.save(callback);
```
-
+## The `type` Key {#type-key}
`type` is a special property in Mongoose schemas. When Mongoose finds
a nested property named `type` in your schema, Mongoose assumes that
@@ -162,7 +167,7 @@ const holdingSchema = new Schema({
});
```
-
+## SchemaType Options {#schematype-options}
You can declare a schema type using the type directly, or an object with
a `type` property.
@@ -200,7 +205,7 @@ The `lowercase` option only works for strings. There are certain options
which apply for all schema types, and some that apply for specific schema
types.
-All Schema Types
+### All Schema Types
* `required`: boolean or function, if true adds a [required validator](validation.html#built-in-validators) for this property
* `default`: Any or function, sets a default value for the path. If the value is a function, the return value of the function is used as the default.
@@ -233,7 +238,7 @@ doc.integerOnly; // 3
doc.i; // 3
```
-Indexes
+### Indexes
You can also define [MongoDB indexes](https://www.mongodb.com/docs/manual/indexes/)
using schema type options.
@@ -253,7 +258,7 @@ const schema2 = new Schema({
});
```
-String
+### String {#string-validators}
* `lowercase`: boolean, whether to always call `.toLowerCase()` on the value
* `uppercase`: boolean, whether to always call `.toUpperCase()` on the value
@@ -264,26 +269,26 @@ const schema2 = new Schema({
* `maxLength`: Number, creates a [validator](validation.html) that checks if the value length is not greater than the given number
* `populate`: Object, sets default [populate options](populate.html#query-conditions)
-Number
+### Number {#number-validators}
* `min`: Number, creates a [validator](validation.html) that checks if the value is greater than or equal to the given minimum.
* `max`: Number, creates a [validator](validation.html) that checks if the value is less than or equal to the given maximum.
* `enum`: Array, creates a [validator](validation.html) that checks if the value is strictly equal to one of the values in the given array.
* `populate`: Object, sets default [populate options](populate.html#query-conditions)
-Date
+### Date
* `min`: Date, creates a [validator](validation.html) that checks if the value is greater than or equal to the given minimum.
* `max`: Date, creates a [validator](validation.html) that checks if the value is less than or equal to the given maximum.
* `expires`: Number or String, creates a TTL index with the value expressed in seconds.
-ObjectId
+### ObjectId
* `populate`: Object, sets default [populate options](populate.html#query-conditions)
-
+## Usage Notes {#usage-notes}
-String
+### String {#strings}
To declare a path as a string, you may use either the `String` global
constructor or the string `'String'`.
@@ -307,7 +312,7 @@ new Person({ name: { toString: () => 42 } }).name; // "42" as a string
new Person({ name: { foo: 42 } }).name;
```
-Number
+### Number {#numbers}
To declare a path as a number, you may use either the `Number` global
constructor or the string `'Number'`.
@@ -336,22 +341,21 @@ The values `null` and `undefined` are not cast.
NaN, strings that cast to NaN, arrays, and objects that don't have a `valueOf()` function
will all result in a [CastError](validation.html#cast-errors) once validated, meaning that it will not throw on initialization, only when validated.
-Dates
+### Dates {#dates}
-[Built-in `Date` methods](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) are [__not__ hooked into](https://github.com/Automattic/mongoose/issues/1598) the mongoose change tracking logic which in English means that if you use a `Date` in your document and modify it with a method like `setMonth()`, mongoose will be unaware of this change and `doc.save()` will not persist this modification. If you must modify `Date` types using built-in methods, tell mongoose about the change with `doc.markModified('pathToYourDate')` before saving.
+[Built-in `Date` methods](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) are [**not** hooked into](https://github.com/Automattic/mongoose/issues/1598) the mongoose change tracking logic which in English means that if you use a `Date` in your document and modify it with a method like `setMonth()`, mongoose will be unaware of this change and `doc.save()` will not persist this modification. If you must modify `Date` types using built-in methods, tell mongoose about the change with `doc.markModified('pathToYourDate')` before saving.
```javascript
const Assignment = mongoose.model('Assignment', { dueDate: Date });
-Assignment.findOne(function(err, doc) {
- doc.dueDate.setMonth(3);
- doc.save(callback); // THIS DOES NOT SAVE YOUR CHANGE
+const doc = await Assignment.findOne();
+doc.dueDate.setMonth(3);
+await doc.save(); // THIS DOES NOT SAVE YOUR CHANGE
- doc.markModified('dueDate');
- doc.save(callback); // works
-});
+doc.markModified('dueDate');
+await doc.save(); // works
```
-Buffer
+### Buffer {#buffers}
To declare a path as a Buffer, you may use either the `Buffer` global
constructor or the string `'Buffer'`.
@@ -365,13 +369,13 @@ const Data = mongoose.model('Data', schema2);
Mongoose will successfully cast the below values to buffers.
-```
+```js
const file1 = new Data({ binData: 'test'}); // {"type":"Buffer","data":[116,101,115,116]}
const file2 = new Data({ binData: 72987 }); // {"type":"Buffer","data":[27]}
const file4 = new Data({ binData: { type: 'Buffer', data: [1, 2, 3]}}); // {"type":"Buffer","data":[1,2,3]}
```
-Mixed
+### Mixed {#mixed}
An "anything goes" SchemaType. Mongoose will not do any casting on mixed paths.
You can define a mixed path using `Schema.Types.Mixed` or by passing an empty
@@ -398,7 +402,7 @@ person.markModified('anything');
person.save(); // Mongoose will save changes to `anything`.
```
-ObjectIds
+### ObjectIds {#objectids}
An [ObjectId](https://www.mongodb.com/docs/manual/reference/method/ObjectId/)
is a special type typically used for unique identifiers. Here's how
@@ -425,7 +429,7 @@ car.driver instanceof mongoose.Types.ObjectId; // true
car.driver.toString(); // Something like "5e1a0651741b255ddda996c4"
```
-Boolean
+### Boolean {#booleans}
Booleans in Mongoose are [plain JavaScript booleans](https://www.w3schools.com/js/js_booleans.asp).
By default, Mongoose casts the below values to `true`:
@@ -459,12 +463,12 @@ mongoose.Schema.Types.Boolean.convertToFalse.add('nay');
console.log(new M({ b: 'nay' }).b); // false
```
-Arrays
+### Arrays {#arrays}
Mongoose supports arrays of [SchemaTypes](api/schema.html#schema_Schema-Types)
and arrays of [subdocuments](subdocs.html). Arrays of SchemaTypes are
-also called _primitive arrays_, and arrays of subdocuments are also called
-_document arrays_.
+also called *primitive arrays*, and arrays of subdocuments are also called
+*document arrays*.
```javascript
const ToySchema = new Schema({ name: String });
@@ -505,7 +509,7 @@ const Empty3 = new Schema({ any: [Schema.Types.Mixed] });
const Empty4 = new Schema({ any: [{}] });
```
-Maps
+### Maps {#maps}
A `MongooseMap` is a subclass of [JavaScript's `Map` class](http://thecodebarbarian.com/the-80-20-guide-to-maps-in-javascript.html).
In these docs, we'll use the terms 'map' and `MongooseMap` interchangeably.
@@ -591,7 +595,7 @@ on `socialMediaHandles.$*.oauth`:
const user = await User.findOne().populate('socialMediaHandles.$*.oauth');
```
-UUID
+### UUID {#uuid}
Mongoose also supports a UUID type that stores UUID instances as [Node.js buffers](https://thecodebarbarian.com/an-overview-of-buffers-in-node-js.html).
We recommend using [ObjectIds](#objectids) rather than UUIDs for unique document ids in Mongoose, but you may use UUIDs if you need to.
@@ -607,7 +611,7 @@ const authorSchema = new Schema({
const Author = mongoose.model('Author', authorSchema);
-const bookSchema = new Schema({
+const bookSchema = new Schema({
authorId: { type: Schema.Types.UUID, ref: 'Author' }
});
const Book = mongoose.model('Book', bookSchema);
@@ -632,7 +636,91 @@ const schema = new mongoose.Schema({
});
```
-
+### BigInt {#bigint}
+
+Mongoose supports [JavaScript BigInts](https://thecodebarbarian.com/an-overview-of-bigint-in-node-js.html) as a SchemaType.
+BigInts are stored as [64-bit integers in MongoDB (BSON type "long")](https://www.mongodb.com/docs/manual/reference/bson-types/).
+
+```javascript
+const questionSchema = new Schema({
+ answer: BigInt
+});
+const Question = mongoose.model('Question', questionSchema);
+
+const question = new Question({ answer: 42n });
+typeof question.answer; // 'bigint'
+```
+
+### Double {#double}
+
+Mongoose supports [64-bit IEEE 754-2008 floating point numbers](https://en.wikipedia.org/wiki/IEEE_754-2008_revision) as a SchemaType.
+Int32s are stored as [BSON type "double" in MongoDB](https://www.mongodb.com/docs/manual/reference/bson-types/).
+
+```javascript
+const studentsSchema = new Schema({
+ id: Int32
+});
+const Student = mongoose.model('Student', schema);
+
+const student = new Temperature({ celsius: 1339 });
+typeof student.id; // 'number'
+```
+
+There are several types of values that will be successfully cast to a Double.
+
+```javascript
+new Temperature({ celsius: '1.2e12' }).celsius; // 15 as a Double
+new Temperature({ celsius: true }).celsius; // 1 as a Double
+new Temperature({ celsius: false }).celsius; // 0 as a Double
+new Temperature({ celsius: { valueOf: () => 83.0033 } }).celsius; // 83 as a Double
+new Temperature({ celsius: '' }).celsius; // null as a Double
+```
+
+The following inputs will result will all result in a [CastError](validation.html#cast-errors) once validated, meaning that it will not throw on initialization, only when validated:
+
+* strings that do not represent a numeric string, a NaN or a null-ish value
+* objects that don't have a `valueOf()` function
+* an input that represents a value outside the bounds of a IEEE 754-2008 floating point
+
+### Int32 {#int32}
+
+Mongoose supports 32-bit integers as a SchemaType.
+Int32s are stored as [32-bit integers in MongoDB (BSON type "int")](https://www.mongodb.com/docs/manual/reference/bson-types/).
+
+```javascript
+const studentsSchema = new Schema({
+ id: Int32
+});
+const Student = mongoose.model('Student', schema);
+
+const student = new Temperature({ celsius: 1339 });
+typeof student.id; // 'number'
+```
+
+There are several types of values that will be successfully cast to a Number.
+
+```javascript
+new Student({ id: '15' }).id; // 15 as a Int32
+new Student({ id: true }).id; // 1 as a Int32
+new Student({ id: false }).id; // 0 as a Int32
+new Student({ id: { valueOf: () => 83 } }).id; // 83 as a Int32
+new Student({ id: '' }).id; // null as a Int32
+```
+
+If you pass an object with a `valueOf()` function that returns a Number, Mongoose will
+call it and assign the returned value to the path.
+
+The values `null` and `undefined` are not cast.
+
+The following inputs will result will all result in a [CastError](validation.html#cast-errors) once validated, meaning that it will not throw on initialization, only when validated:
+
+* NaN
+* strings that cast to NaN
+* objects that don't have a `valueOf()` function
+* a decimal that must be rounded to be an integer
+* an input that represents a value outside the bounds of an 32-bit integer
+
+## Getters {#getters}
Getters are like virtuals for paths defined in your schema. For example,
let's say you wanted to store user profile pictures as relative paths and
@@ -694,7 +782,7 @@ const root = 'https://s3.amazonaws.com/mybucket';
schema.path('arr.0.url').get(v => `${root}${v}`);
```
-
+## Schemas {#schemas}
To declare a path as another [schema](guide.html#definition),
set `type` to the sub-schema's instance.
@@ -716,7 +804,7 @@ const schema = new mongoose.Schema({
});
```
-
+## Creating Custom Types {#customtypes}
Mongoose can also be extended with [custom SchemaTypes](customschematypes.html). Search the
[plugins](http://plugins.mongoosejs.io)
@@ -728,7 +816,7 @@ and
Read more about creating custom SchemaTypes [here](customschematypes.html).
-
+## The `schema.path()` Function {#path}
The `schema.path()` function returns the instantiated schema type for a
given path.
@@ -750,7 +838,7 @@ console.log(sampleSchema.path('name'));
You can use this function to inspect the schema type for a given path,
including what validators it has and what the type is.
-
+## Further Reading {#further-reading}
An Introduction to Mongoose SchemaTypes
diff --git a/docs/search.pug b/docs/search.pug
index 1902035fa56..9686c67a25d 100644
--- a/docs/search.pug
+++ b/docs/search.pug
@@ -1,33 +1,7 @@
extends layout
append style
- script(type="text/javascript" src="https://unpkg.com/unfetch/polyfill")
-
style.
- .search-bar {
- margin-top: 1em;
- }
-
- .search-bar input {
- width: 810px;
- border: 1px solid #ddd;
- padding: 0.25em;
- border-radius: 3px;
- }
-
- .search-bar button {
- background-color: #777;
- color: white;
- border: 1px solid transparent;
- border-radius: 3px;
- height: 30px;
- width: 30px;
- padding: 0px;
- position: relative;
- top: 7px;
- left: 1px;
- }
-
.title {
font-size: 1.5em;
}
@@ -45,7 +19,6 @@ append style
p {
margin-top: 0.5em;
- line-height: 1.5em;
}
block content
@@ -54,7 +27,7 @@ block content
include includes/native
- div.search-bar
+ div.search
input#search-input(type="text", placeholder="Search")
button#search-button
img(src=`${versions.versionedPath}/docs/images/search.svg`)
diff --git a/docs/shared-schemas.md b/docs/shared-schemas.md
new file mode 100644
index 00000000000..a504f986d48
--- /dev/null
+++ b/docs/shared-schemas.md
@@ -0,0 +1,73 @@
+# Sharing Schemas Between Mongoose Projects
+
+In larger organizations, it is common to have a project that contains schemas which are shared between multiple projects.
+For example, suppose your company has an `@initech/shared-schemas` private npm package, and `npm list` looks like the following:
+
+```sh
+@initech/web-app1@1.0.0
+├── @initech/shared-schemas@1.0.0
+├── mongoose@8.0.1
+```
+
+In the above output, `@initech/web-app1` is a *client project* and `@initech/shared-schemas` is the *shared library*.
+
+## Put Mongoose as a Peer Dependency
+
+First, and most importantly, we recommend that `@initech/shared-schemas` list Mongoose in [your shared-schema's `peerDependencies`](https://docs.npmjs.com/cli/v10/configuring-npm/package-json#peerdependencies), **not** as a top-level dependency.
+For example, `@initech/shared-schemas`'s `package.json` should look like the following.
+
+```javascript
+{
+ "name": "@initech/shared-schemas",
+ "peerDependencies": {
+ "mongoose": "8.x"
+ }
+}
+```
+
+We recommend this approach for the following reasons:
+
+1. Easier to upgrade. For example, suppose `@initech/shared-schemas` has a dependency on Mongoose 8, and `@initech/web-app1` works fine with Mongoose 8; but `@initech/web-app2` cannot upgrade from Mongoose 7. Peer dependencies makes it easier for the projects that rely on your shared schemas to determine which version of Mongoose they want, without risking having conflicting versions of the Mongoose module.
+2. Reduce risk of Mongoose module duplicates. Using Mongoose schemas and models from one version of Mongoose with another version is not supported.
+
+## Export Schemas, Not Models
+
+We recommend that `@initech/shared-schemas` export Mongoose schemas, **not** models.
+This approach is more flexible and allows client projects to instantiate models using their preferred patterns.
+In particular, if `@initech/shared-schemas` exports a model that is registered using `mongoose.model()`, there is no way to transfer that model to a different connection.
+
+```javascript
+// `userSchema.js` in `@initech/shared-schemas`
+const userSchema = new mongoose.Schema({ name: String });
+
+// Do this:
+module.exports = userSchema;
+
+// Not this:
+module.exports = mongoose.model('User', userSchema);
+```
+
+## Workaround: Export a POJO
+
+Sometimes, existing shared libraries don't follow the above best practices.
+If you find yourself with a shared library that depends on an old version of Mongoose, a helpful workaround is to export a [POJO](https://masteringjs.io/tutorials/fundamentals/pojo) rather than a schema or model.
+This will remove any conflicts between the shared library's version of Mongoose and the client project's version of Mongoose.
+
+```javascript
+// Replace this:
+module.exports = new mongoose.Schema({ name: String });
+
+// With this:
+module.exports = { name: String };
+```
+
+And update your client project to do the following:
+
+```javascript
+// Replace this:
+const { userSchema } = require('@initech/shared-schemas');
+
+// With this:
+const { userSchemaDefinition } = require('@initech/shared-schemas');
+const userSchema = new mongoose.Schema(userSchemaDefinition);
+```
diff --git a/docs/source/api.js b/docs/source/api.js
index 03a364062f9..e398164056a 100644
--- a/docs/source/api.js
+++ b/docs/source/api.js
@@ -9,59 +9,93 @@ const fs = require('fs');
const md = require('marked');
const files = [
- 'lib/index.js',
+ 'lib/mongoose.js',
'lib/schema.js',
'lib/connection.js',
'lib/document.js',
'lib/model.js',
'lib/query.js',
- 'lib/cursor/QueryCursor.js',
+ 'lib/cursor/queryCursor.js',
'lib/aggregate.js',
- 'lib/cursor/AggregationCursor.js',
- 'lib/schematype.js',
- 'lib/virtualtype.js',
+ 'lib/cursor/aggregationCursor.js',
+ 'lib/schemaType.js',
+ 'lib/virtualType.js',
'lib/error/index.js',
'lib/schema/array.js',
- 'lib/schema/documentarray.js',
- 'lib/schema/SubdocumentPath.js',
- 'lib/options/SchemaTypeOptions.js',
- 'lib/options/SchemaArrayOptions.js',
- 'lib/options/SchemaBufferOptions.js',
- 'lib/options/SchemaDateOptions.js',
- 'lib/options/SchemaNumberOptions.js',
- 'lib/options/SchemaObjectIdOptions.js',
- 'lib/options/SchemaStringOptions.js',
- 'lib/types/DocumentArray/methods/index.js',
+ 'lib/schema/documentArray.js',
+ 'lib/schema/subdocument.js',
+ 'lib/schema/boolean.js',
+ 'lib/schema/buffer.js',
+ 'lib/schema/number.js',
+ 'lib/schema/objectId.js',
+ 'lib/schema/string.js',
+ 'lib/options/schemaTypeOptions.js',
+ 'lib/options/schemaArrayOptions.js',
+ 'lib/options/schemaBufferOptions.js',
+ 'lib/options/schemaDateOptions.js',
+ 'lib/options/schemaNumberOptions.js',
+ 'lib/options/schemaObjectIdOptions.js',
+ 'lib/options/schemaStringOptions.js',
+ 'lib/types/documentArray/methods/index.js',
'lib/types/subdocument.js',
- 'lib/types/ArraySubdocument.js',
+ 'lib/types/arraySubdocument.js',
'lib/types/buffer.js',
'lib/types/decimal128.js',
- 'lib/types/map.js'
+ 'lib/types/map.js',
+ 'lib/types/array/methods/index.js'
];
-module.exports = {
- docs: [],
- github: 'https://github.com/Automattic/mongoose/blob/',
- title: 'API docs'
-};
-
-/** @type {DocsObj[]} */
-const out = module.exports.docs;
-
-const combinedFiles = [];
-for (const file of files) {
- try {
- const comments = dox.parseComments(fs.readFileSync(`./${file}`, 'utf8'), { raw: true });
- comments.file = file;
- combinedFiles.push(comments);
- } catch (err) {
- // show log of which file has thrown a error for easier debugging
- console.error('Error while trying to parseComments for ', file);
- throw err;
- }
+/** @type {Map.} */
+const out = module.exports.docs = new Map();
+
+// add custom matchers to dox, to recognize things it does not know about
+// see https://github.com/tj/dox/issues/198
+{
+ // Some matchers need to be in a specific order, like the "prototype" matcher must be before the static matcher (and inverted because "unshift")
+
+ // "unshift" is used, because the first function to return a object from "contextPatternMatchers" is used (and we need to "overwrite" those specific functions)
+
+ // push a matcher to recognize "Class.fn = async function" as a method
+ dox.contextPatternMatchers.unshift(function(str) {
+ const match = /^\s*([\w$.]+)\s*\.\s*([\w$]+)\s*=\s*(?:async\s+)?function/.exec(str);
+ if (match) {
+ return {
+ type: 'method',
+ receiver: match[1],
+ name: match[2],
+ string: match[1] + '.' + match[2] + '()'
+ };
+ }
+ });
+
+ // push a matcher to recognize "Class.prototype.fn = async function" as a method
+ dox.contextPatternMatchers.unshift(function(str) {
+ const match = /^\s*([\w$.]+)\s*\.\s*prototype\s*\.\s*([\w$]+)\s*=\s*(?:async\s+)?function/.exec(str);
+ if (match) {
+ return {
+ type: 'method',
+ constructor: match[1],
+ cons: match[1],
+ name: match[2],
+ string: match[1] + '.prototype.' + match[2] + '()'
+ };
+ }
+ });
+
+ // push a matcher to recognize "async function" as a function
+ dox.contextPatternMatchers.unshift(function(str) {
+ const match = /^\s*(export(\s+default)?\s+)?(?:async\s+)?function\s+([\w$]+)\s*\(/.exec(str);
+ if (match) {
+ return {
+ type: 'function',
+ name: match[3],
+ string: match[3] + '()'
+ };
+ }
+ });
}
-parse();
+parseAllFiles();
/**
* @typedef {Object} TagObject
@@ -133,20 +167,32 @@ function processName(input) {
replace('/methods', '');
const lastSlash = name.lastIndexOf('/');
const fullName = name;
- name = name.substr(lastSlash === -1 ? 0 : lastSlash + 1);
- if (name === 'core_array') {
+ const basename = name.substr(lastSlash === -1 ? 0 : lastSlash + 1);
+ name = basename;
+ if (basename === 'core_array') {
name = 'array';
}
- if (fullName === 'schema/array') {
- name = 'SchemaArray';
+ if (fullName.startsWith('schema/')) {
+ name = 'Schema';
+ if (basename.charAt(0) !== basename.charAt(0).toUpperCase()) {
+ name += basename.charAt(0).toUpperCase() + basename.substring(1);
+ } else {
+ name += basename;
+ }
+ }
+ if (fullName === 'types/array/methods/index') {
+ name = 'Array';
+ }
+ if (basename === 'SubdocumentPath') {
+ name = 'SubdocumentPath';
}
- if (name === 'documentarray') {
+ if (basename === 'documentarray') {
name = 'DocumentArrayPath';
}
- if (name === 'DocumentArray') {
+ if (basename === 'DocumentArray') {
name = 'MongooseDocumentArray';
}
- if (name === 'index') {
+ if (basename === 'index') {
name = 'Mongoose';
}
@@ -165,223 +211,242 @@ function convertTypesToString(types) {
return Array.isArray(types) ? types.join('|') : types;
}
-function parse() {
- for (const props of combinedFiles) {
- const { docName: name, docFileName } = processName(props.file);
- /** @type {DocsObj} */
- const data = {
- title: name,
- fileName: docFileName,
- props: []
- };
-
- for (const prop of props) {
- if (prop.ignore || prop.isPrivate) {
- continue;
- }
+/**
+ * Parse all files defined in "files"
+ */
+function parseAllFiles() {
+ for (const file of files) {
+ parseFile(file, true);
+ }
+}
- /** @type {PropContext} */
- const ctx = prop.ctx || {};
+/**
+ * Parse a specific file
+ * @param {String} file The file to parse
+ * @param {Boolean} throwErr throw the error if one is encountered?
+ */
+function parseFile(file, throwErr = true) {
+ try {
+ const comments = dox.parseComments(fs.readFileSync(file, 'utf8'), { raw: true });
+ comments.file = file;
+ processFile(comments);
+ } catch (err) {
+ // show log of which file has thrown a error for easier debugging
+ console.error('Error while trying to parseComments for ', file);
+ if (throwErr) {
+ throw err;
+ }
+ }
+}
- // somehow in "dox", it is named "receiver" sometimes, not "constructor"
- // this is used as a fall-back if the handling below does not overwrite it
- if ('receiver' in ctx) {
- ctx.constructor = ctx.receiver;
- delete ctx.receiver;
- }
+function processFile(props) {
+ const { docName: name, docFileName } = processName(props.file);
+ /** @type {DocsObj} */
+ const data = {
+ title: name,
+ fileName: docFileName,
+ props: []
+ };
- // in some cases "dox" has "ctx.constructor" defined but set to "undefined", which will later be used for setting "ctx.string"
- if ('constructor' in ctx && ctx.constructor === undefined) {
- ctx.constructorWasUndefined = true;
- }
+ for (const prop of props) {
+ if (prop.ignore || prop.isPrivate) {
+ continue;
+ }
- for (const __tag of prop.tags) {
- // the following has been done, because otherwise no type could be applied for intellisense
- /** @type {TagObject} */
- const tag = __tag;
- switch (tag.type) {
- case 'see':
- if (!Array.isArray(ctx.see)) {
- ctx.see = [];
- }
+ /** @type {PropContext} */
+ const ctx = prop.ctx || {};
- // for this type, it needs to be parsed from the string itself to support more than 1 word
- // this is required because "@see" is kinda badly defined and mongoose uses a slightly customized way (longer text and different kinds of links)
-
- ctx.see.push(extractTextUrlFromTag(tag, ctx, true));
- break;
- case 'receiver':
- console.warn(`Found "@receiver" tag in ${ctx.constructor} ${ctx.name}`);
- break;
- case 'property':
- ctx.type = 'property';
-
- // using "name" over "string" because "string" also contains the type and maybe other stuff
- ctx.name = tag.name;
- // only assign "type" if there are types
- if (tag.types.length > 0) {
- ctx.type = convertTypesToString(tag.types);
- }
+ // somehow in "dox", it is named "receiver" sometimes, not "constructor"
+ // this is used as a fall-back if the handling below does not overwrite it
+ if ('receiver' in ctx) {
+ ctx.constructor = ctx.receiver;
+ delete ctx.receiver;
+ }
+
+ // in some cases "dox" has "ctx.constructor" defined but set to "undefined", which will later be used for setting "ctx.string"
+ if ('constructor' in ctx && ctx.constructor === undefined) {
+ ctx.constructorWasUndefined = true;
+ }
- break;
- case 'type':
+ for (const __tag of prop.tags) {
+ // the following has been done, because otherwise no type could be applied for intellisense
+ /** @type {TagObject} */
+ const tag = __tag;
+ switch (tag.type) {
+ case 'see':
+ if (!Array.isArray(ctx.see)) {
+ ctx.see = [];
+ }
+
+ // for this type, it needs to be parsed from the string itself to support more than 1 word
+ // this is required because "@see" is kinda badly defined and mongoose uses a slightly customized way (longer text and different kinds of links)
+
+ ctx.see.push(extractTextUrlFromTag(tag, ctx, true));
+ break;
+ case 'receiver':
+ console.warn(`Found "@receiver" tag in ${ctx.constructor} ${ctx.name}`);
+ break;
+ case 'property':
+ ctx.type = 'property';
+
+ // using "name" over "string" because "string" also contains the type and maybe other stuff
+ ctx.name = tag.name;
+ // only assign "type" if there are types
+ if (tag.types.length > 0) {
ctx.type = convertTypesToString(tag.types);
- break;
- case 'static':
- ctx.type = 'property';
- ctx.isStatic = true;
- // dont take "string" as "name" from here, because jsdoc definitions of "static" do not have parameters, also its defined elsewhere anyway
- // ctx.name = tag.string;
- break;
- case 'function':
- ctx.type = 'function';
- ctx.isStatic = true;
- ctx.name = tag.string;
- // extra parameter to make function definitions independant of where "@function" is defined
- // like "@static" could have overwritten "ctx.string" again if defined after "@function"
- ctx.isFunction = true;
- break;
- case 'return':
- tag.description = tag.description ?
- md.parse(tag.description).replace(/^/, '').replace(/<\/p>\n?$/, '') :
- '';
-
- // dox does not add "void" / "undefined" to types, so in the documentation it would result in a empty "«»"
- if (tag.string.includes('void') || tag.string.includes('undefined')) {
- tag.types.push('void');
- }
+ }
- ctx.return = tag;
- break;
- case 'inherits': {
- const obj = extractTextUrlFromTag(tag, ctx);
- // try to get the documentation name for the "@inherits" value
- // example: "@inherits SchemaType" -> "schematype.html"
- if (!obj.url || obj.url === obj.text) {
- let match = undefined;
- for (const file of files) {
- const { docName, docFileName } = processName(file);
- if (docName.toLowerCase().includes(obj.text.toLowerCase())) {
- match = docFileName;
- break;
- }
- }
+ break;
+ case 'type':
+ ctx.type = convertTypesToString(tag.types);
+ break;
+ case 'static':
+ ctx.type = 'property';
+ ctx.isStatic = true;
+ // dont take "string" as "name" from here, because jsdoc definitions of "static" do not have parameters, also its defined elsewhere anyway
+ // ctx.name = tag.string;
+ break;
+ case 'function':
+ ctx.type = 'function';
+ ctx.isStatic = true;
+ ctx.name = tag.string;
+ // extra parameter to make function definitions independent of where "@function" is defined
+ // like "@static" could have overwritten "ctx.string" again if defined after "@function"
+ ctx.isFunction = true;
+ break;
+ case 'return':
+ tag.description = tag.description ?
+ md.parse(tag.description).replace(/^
/, '').replace(/<\/p>\n?$/, '') :
+ '';
+
+ // dox does not add "void" / "undefined" to types, so in the documentation it would result in a empty "«»"
+ if (tag.string.includes('void') || tag.string.includes('undefined')) {
+ tag.types.push('void');
+ }
- if (match) {
- obj.url = match + '.html';
- } else {
- console.warn(`no match found in files for inherits "${obj.text}" on "${ctx.constructor}.${ctx.name}"`);
+ ctx.return = tag;
+ break;
+ case 'inherits': {
+ const obj = extractTextUrlFromTag(tag, ctx);
+ // try to get the documentation name for the "@inherits" value
+ // example: "@inherits SchemaType" -> "schematype.html"
+ if (!obj.url || obj.url === obj.text) {
+ let match = undefined;
+ for (const file of files) {
+ const { docName, docFileName } = processName(file);
+ if (docName.toLowerCase().includes(obj.text.toLowerCase())) {
+ match = docFileName;
+ break;
}
}
- ctx.inherits = obj;
- break;
- }
- case 'event':
- case 'param':
- ctx[tag.type] = (ctx[tag.type] || []);
- // the following is required, because in newer "dox" version "null" is not included in "types" anymore, but a seperate property
- if (tag.nullable) {
- tag.types.push('null');
- }
- if (tag.types) {
- tag.types = convertTypesToString(tag.types);
- }
- ctx[tag.type].push(tag);
- if (tag.name != null && tag.name.startsWith('[') && tag.name.endsWith(']') && tag.name.includes('.')) {
- tag.nested = true;
- }
- if (tag.variable) {
- if (tag.name.startsWith('[')) {
- tag.name = '[...' + tag.name.slice(1);
- } else {
- tag.name = '...' + tag.name;
- }
+
+ if (match) {
+ obj.url = match + '.html';
+ } else {
+ console.warn(`no match found in files for inherits "${obj.text}" on "${ctx.constructor}.${ctx.name}"`);
}
- tag.description = tag.description ?
- md.parse(tag.description).replace(/^
/, '').replace(/<\/p>$/, '') :
- '';
- break;
- case 'method':
- ctx.type = 'method';
- ctx.name = tag.string;
- ctx.isFunction = true;
- break;
- case 'memberOf':
- ctx.constructor = tag.parent;
- break;
- case 'constructor':
- ctx.string = tag.string;
- ctx.name = tag.string;
- ctx.isFunction = true;
- break;
- case 'instance':
- ctx.isInstance = true;
- break;
- case 'deprecated':
- ctx.deprecated = true;
- break;
+ }
+ ctx.inherits = obj;
+ break;
}
+ case 'event':
+ case 'param':
+ ctx[tag.type] = (ctx[tag.type] || []);
+ // the following is required, because in newer "dox" version "null" is not included in "types" anymore, but a separate property
+ if (tag.nullable) {
+ tag.types.push('null');
+ }
+ if (tag.types) {
+ tag.types = convertTypesToString(tag.types);
+ }
+ ctx[tag.type].push(tag);
+ if (tag.name != null && tag.name.startsWith('[') && tag.name.endsWith(']') && tag.name.includes('.')) {
+ tag.nested = true;
+ }
+ if (tag.variable) {
+ if (tag.name.startsWith('[')) {
+ tag.name = '[...' + tag.name.slice(1);
+ } else {
+ tag.name = '...' + tag.name;
+ }
+ }
+ tag.description = tag.description ?
+ md.parse(tag.description).replace(/^
/, '').replace(/<\/p>$/, '') :
+ '';
+ break;
+ case 'method':
+ ctx.type = 'method';
+ ctx.name = tag.string;
+ ctx.isFunction = true;
+ break;
+ case 'memberOf':
+ ctx.constructor = tag.parent;
+ break;
+ case 'constructor':
+ ctx.string = tag.string;
+ ctx.name = tag.string;
+ ctx.isFunction = true;
+ break;
+ case 'instance':
+ ctx.isInstance = true;
+ break;
+ case 'deprecated':
+ ctx.deprecated = true;
+ break;
}
+ }
- if (ctx.isInstance && ctx.isStatic) {
- console.warn(`Property "${ctx.name}" in "${ctx.constructor}" has both instance and static JSDOC markings (most likely both @instance and @static)! (File: "${props.file}")`);
- }
-
- // the following if-else-if statement is in this order, because there are more "instance" methods thans static
- // the following condition will be true if "isInstance = true" or if "isInstance = false && isStatic = false" AND "ctx.string" are empty or not defined
- // if "isStatic" and "isInstance" are falsy and "ctx.string" is not falsy, then rely on the "ctx.string" set by "dox"
- if (ctx.isInstance || (!ctx.isStatic && !ctx.isInstance && (!ctx.string || ctx.constructorWasUndefined))) {
- // to transform things like "[Symbol.toStringTag]" to ".prototype[Symbol.toStringTag]" instead of ".prototype.[Symbol.toStringTag]"
- if (ctx.name.startsWith('[')) {
- ctx.string = `${ctx.constructor}.prototype${ctx.name}`;
-
- } else {
- ctx.string = `${ctx.constructor}.prototype.${ctx.name}`;
- }
- } else if (ctx.isStatic) {
- ctx.string = `${ctx.constructor}.${ctx.name}`;
- }
+ if (ctx.isInstance && ctx.isStatic) {
+ console.warn(`Property "${ctx.name}" in "${ctx.constructor}" has both instance and static JSDOC markings (most likely both @instance and @static)! (File: "${props.file}")`);
+ }
- // add "()" to the end of the string if function
- if ((ctx.isFunction || ctx.type === 'method') && !ctx.string.endsWith('()')) {
- ctx.string = ctx.string + '()';
- }
+ // the following if-else-if statement is in this order, because there are more "instance" methods thans static
+ // the following condition will be true if "isInstance = true" or if "isInstance = false && isStatic = false" AND "ctx.string" are empty or not defined
+ // if "isStatic" and "isInstance" are falsy and "ctx.string" is not falsy, then rely on the "ctx.string" set by "dox"
+ if (ctx.isInstance || (!ctx.isStatic && !ctx.isInstance && (!ctx.string || ctx.constructorWasUndefined))) {
+ // to transform things like "[Symbol.toStringTag]" to ".prototype[Symbol.toStringTag]" instead of ".prototype.[Symbol.toStringTag]"
+ if (ctx.name.startsWith('[')) {
+ ctx.string = `${ctx.constructor}.prototype${ctx.name}`;
- // Backwards compat anchors
- if (typeof ctx.constructor === 'string') {
- ctx.anchorId = `${ctx.constructor.toLowerCase()}_${ctx.constructor}-${ctx.name}`;
- } else if (typeof ctx.receiver === 'string') {
- ctx.anchorId = `${ctx.receiver.toLowerCase()}_${ctx.receiver}.${ctx.name}`;
} else {
- ctx.anchorId = `${ctx.name.toLowerCase()}_${ctx.name}`;
+ ctx.string = `${ctx.constructor}.prototype.${ctx.name}`;
}
+ } else if (ctx.isStatic) {
+ ctx.string = `${ctx.constructor}.${ctx.name}`;
+ }
- ctx.description = prop.description.full.
- replace(/ /ig, ' ').
- replace(/>/ig, '>');
- ctx.description = md.parse(ctx.description);
-
- data.props.push(ctx);
+ // add "()" to the end of the string if function
+ if ((ctx.isFunction || ctx.type === 'method') && !ctx.string.endsWith('()')) {
+ ctx.string = ctx.string + '()';
}
- data.props.sort(function(a, b) {
- if (a.string < b.string) {
- return -1;
- } else {
- return 1;
- }
- });
+ ctx.anchorId = ctx.string;
+
+ ctx.description = prop.description.full.
+ replace(/ /ig, ' ').
+ replace(/>/ig, '>');
+ ctx.description = md.parse(ctx.description);
+
+ data.props.push(ctx);
+ }
- if (props.file.startsWith('lib/options')) {
- data.hideFromNav = true;
+ data.props.sort(function(a, b) {
+ if (a.string < b.string) {
+ return -1;
+ } else {
+ return 1;
}
+ });
- data.file = props.file;
- data.editLink = 'https://github.com/Automattic/mongoose/blob/master/' +
+ if (props.file.startsWith('lib/options')) {
+ data.hideFromNav = true;
+ }
+
+ data.file = props.file;
+ data.editLink = 'https://github.com/Automattic/mongoose/blob/master/' +
props.file;
- out.push(data);
- }
+ out.set(data.file, data);
}
/**
@@ -400,8 +465,10 @@ function extractTextUrlFromTag(tag, ctx, warnOnMissingUrl = false) {
// "No Href" -> "No Href"
// "https://someurl.com" -> "" (fallback added)
// "Some#Method #something" -> "Some#Method"
+ // "Test ./somewhere" -> "Test"
+ // "Test2 ./somewhere#andsomewhere" -> "Test2"
// The remainder is simply taken by a call to "slice" (also the text is trimmed later)
- const textMatches = /^(.*? (?=#|\/|(?:https?:)|$))/i.exec(tag.string);
+ const textMatches = /^(.*? (?=#|\/|(?:https?:)|\.\/|$))/i.exec(tag.string);
let text = undefined;
let url = undefined;
@@ -424,3 +491,6 @@ function extractTextUrlFromTag(tag, ctx, warnOnMissingUrl = false) {
url: url || undefined // change to be "undefined" if text is empty or non-valid
};
}
+
+module.exports.parseFile = parseFile;
+module.exports.parseAllFiles = parseAllFiles;
diff --git a/docs/source/index.js b/docs/source/index.js
index 58c899f39a7..50a9a597063 100644
--- a/docs/source/index.js
+++ b/docs/source/index.js
@@ -32,12 +32,16 @@ const docs = {
...require('./typescript')
};
-for (const apidoc of api.docs) {
+for (const apidoc of api.docs.values()) {
docs[`docs/api/${apidoc.fileName}.html`] = { ...apidoc, api: true };
}
docs['index.pug'] = require('./home');
-docs['docs/api.pug'] = require('./api');
+docs['docs/api.md'] = {
+ docs: [],
+ title: 'Redirect to API',
+ markdown: true
+};
docs['docs/advanced_schemas.md'] = { title: 'Advanced Schemas', acquit: true, markdown: true };
docs['docs/validation.md'] = { title: 'Validation', acquit: true, markdown: true };
@@ -60,6 +64,8 @@ docs['docs/populate.md'] = { guide: true, title: 'Query Population', markdown: t
docs['docs/migration.md'] = { guide: true, title: 'Migration Guide', markdown: true };
docs['docs/migrating_to_5.md'] = { guide: true, title: 'Migrating to Mongoose 5', markdown: true };
docs['docs/migrating_to_6.md'] = { guide: true, title: 'Migrating to Mongoose 6', markdown: true };
+docs['docs/migrating_to_7.md'] = { guide: true, title: 'Migrating to Mongoose 7', markdown: true };
+docs['docs/migrating_to_8.md'] = { guide: true, title: 'Migrating to Mongoose 8', markdown: true };
docs['docs/connections.md'] = { guide: true, title: 'Connecting to MongoDB', markdown: true };
docs['docs/lambda.md'] = { guide: true, title: 'Using Mongoose With AWS Lambda', markdown: true };
docs['docs/geojson.md'] = { guide: true, title: 'Using GeoJSON', acquit: true, markdown: true };
@@ -67,6 +73,7 @@ docs['docs/transactions.md'] = { guide: true, title: 'Transactions', acquit: tru
docs['docs/deprecations.md'] = { guide: true, title: 'Deprecation Warnings', markdown: true };
docs['docs/further_reading.md'] = { title: 'Further Reading', markdown: true };
docs['docs/jest.md'] = { title: 'Testing Mongoose with Jest', markdown: true };
+docs['docs/nextjs.md'] = { title: 'Using Mongoose With Next.js', markdown: true };
docs['docs/faq.md'] = { guide: true, title: 'FAQ', markdown: true };
docs['docs/typescript.md'] = { guide: true, title: 'Using TypeScript with Mongoose', markdown: true };
docs['docs/compatibility.md'] = {
@@ -74,6 +81,8 @@ docs['docs/compatibility.md'] = {
guide: true,
markdown: true
};
+docs['docs/shared-schemas.md'] = { guide: true, title: 'Sharing Schemas Between Mongoose Projects', markdown: true };
+docs['docs/field-level-encryption.md'] = { guide: true, title: 'Field Level Encryption', markdown: true };
docs['docs/timestamps.md'] = { title: 'Mongoose Timestamps', markdown: true };
docs['docs/search.pug'] = { title: 'Search' };
docs['docs/enterprise.md'] = { title: 'Mongoose for Enterprise', markdown: true };
diff --git a/docs/source/utils.js b/docs/source/utils.js
index 4eaa3225763..0668867ad6c 100644
--- a/docs/source/utils.js
+++ b/docs/source/utils.js
@@ -2,7 +2,7 @@
const fs = require('fs');
/**
- * @typedef {import("./docsIndex").DocsOptions} DocsOptions
+ * @typedef {import("./index").DocsOptions} DocsOptions
*/
/**
diff --git a/docs/subdocs.md b/docs/subdocs.md
index a0adbe2214d..5fbfae5109a 100644
--- a/docs/subdocs.md
+++ b/docs/subdocs.md
@@ -38,7 +38,7 @@ doc.child;
```
- What is a Subdocument?
+ What is a Subdocument?
Subdocuments versus Nested Paths
Subdocument Defaults
Finding a Subdocument
@@ -65,7 +65,7 @@ parent.children[0].name = 'Matthew';
// `parent.children[0].save()` is a no-op, it triggers middleware but
// does **not** actually save the subdocument. You need to save the parent
// doc.
-parent.save(callback);
+await parent.save();
```
Subdocuments have `save` and `validate` [middleware](middleware.html)
@@ -82,9 +82,11 @@ childSchema.pre('save', function(next) {
});
const parent = new Parent({ children: [{ name: 'invalid' }] });
-parent.save(function(err) {
- console.log(err.message); // #sadpanda
-});
+try {
+ await parent.save();
+} catch (err) {
+ err.message; // '#sadpanda'
+}
```
Subdocuments' `pre('save')` and `pre('validate')` middleware execute
@@ -244,10 +246,8 @@ const subdoc = parent.children[0];
console.log(subdoc); // { _id: '501d86090d371bab2c0341c5', name: 'Liesl' }
subdoc.isNew; // true
-parent.save(function(err) {
- if (err) return handleError(err);
- console.log('Success!');
-});
+await parent.save();
+console.log('Success!');
```
You can also create a subdocument without adding it to an array by using the [`create()` method](api/mongoosedocumentarray.html#mongoosedocumentarray_MongooseDocumentArray-create) of Document Arrays.
@@ -258,24 +258,21 @@ const newdoc = parent.children.create({ name: 'Aaron' });
## Removing Subdocs
-Each subdocument has its own
-[remove](api/subdocument.html#subdocument_Subdocument-remove) method. For
-an array subdocument, this is equivalent to calling `.pull()` on the
-subdocument. For a single nested subdocument, `remove()` is equivalent
-to setting the subdocument to `null`.
+Each subdocument has its own [deleteOne](api/subdocument.html#Subdocument.prototype.deleteOne()) method.
+For an array subdocument, this is equivalent to calling `.pull()` on the subdocument.
+For a single nested subdocument, `deleteOne()` is equivalent to setting the subdocument to [`null`](https://masteringjs.io/tutorials/fundamentals/null).
```javascript
// Equivalent to `parent.children.pull(_id)`
-parent.children.id(_id).remove();
+parent.children.id(_id).deleteOne();
// Equivalent to `parent.child = null`
-parent.child.remove();
-parent.save(function(err) {
- if (err) return handleError(err);
- console.log('the subdocs were removed');
-});
+parent.child.deleteOne();
+
+await parent.save();
+console.log('the subdocs were removed');
```
-Parents of Subdocs
+## Parents of Subdocs {#subdoc-parents}
Sometimes, you need to get the parent of a subdoc. You can access the
parent using the `parent()` function.
@@ -316,10 +313,9 @@ doc.level1.level2.parent() === doc.level1; // true
doc.level1.level2.ownerDocument() === doc; // true
```
-
+### Alternate declaration syntax for arrays {#altsyntaxarrays}
-If you create a schema with an array of objects, Mongoose will automatically
-convert the object to a schema for you:
+If you create a schema with an array of objects, Mongoose will automatically convert the object to a schema for you:
```javascript
const parentSchema = new Schema({
diff --git a/docs/timestamps.md b/docs/timestamps.md
index b0fba3ef69e..8a722b4b356 100644
--- a/docs/timestamps.md
+++ b/docs/timestamps.md
@@ -47,6 +47,7 @@ console.log(doc.updatedAt); // 2022-02-26T17:08:13.991Z
// Mongoose also blocks changing `createdAt` and sets its own `updatedAt`
// on `findOneAndUpdate()`, `updateMany()`, and other query operations
+// **except** `replaceOne()` and `findOneAndReplace()`.
doc = await User.findOneAndUpdate(
{ _id: doc._id },
{ name: 'test3', createdAt: new Date(0), updatedAt: new Date(0) },
@@ -56,6 +57,35 @@ console.log(doc.createdAt); // 2022-02-26T17:08:13.930Z
console.log(doc.updatedAt); // 2022-02-26T17:08:14.008Z
```
+Keep in mind that `replaceOne()` and `findOneAndReplace()` overwrite all non-`_id` properties, **including** immutable properties like `createdAt`.
+Calling `replaceOne()` or `findOneAndReplace()` will update the `createdAt` timestamp as shown below.
+
+```javascript
+// `findOneAndReplace()` and `replaceOne()` without timestamps specified in `replacement`
+// sets `createdAt` and `updatedAt` to current time.
+doc = await User.findOneAndReplace(
+ { _id: doc._id },
+ { name: 'test3' },
+ { new: true }
+);
+console.log(doc.createdAt); // 2022-02-26T17:08:14.008Z
+console.log(doc.updatedAt); // 2022-02-26T17:08:14.008Z
+
+// `findOneAndReplace()` and `replaceOne()` with timestamps specified in `replacement`
+// sets `createdAt` and `updatedAt` to the values in `replacement`.
+doc = await User.findOneAndReplace(
+ { _id: doc._id },
+ {
+ name: 'test3',
+ createdAt: new Date('2022-06-01'),
+ updatedAt: new Date('2022-06-01')
+ },
+ { new: true }
+);
+console.log(doc.createdAt); // 2022-06-01T00:00:00.000Z
+console.log(doc.updatedAt); // 2022-06-01T00:00:00.000Z
+```
+
## Alternate Property Names
For the purposes of these docs, we'll always refer to `createdAt` and `updatedAt`.
@@ -199,10 +229,27 @@ Mongoose: users.findOneAndUpdate({}, { '$setOnInsert': { createdAt: new Date("Su
Notice the `$setOnInsert` for `createdAt` and `$set` for `updatedAt`.
MongoDB's [`$setOnInsert` operator](https://www.mongodb.com/docs/manual/reference/operator/update/setOnInsert/) applies the update only if a new document is [upserted](https://masteringjs.io/tutorials/mongoose/upsert).
-So, for example, if you want to _only_ set `updatedAt` if the document if a new document is created, you can disable the `updatedAt` timestamp and set it yourself as shown below:
+So, for example, if you want to *only* set `updatedAt` if a new document is created, you can disable the `updatedAt` timestamp and set it yourself as shown below:
```javascript
await User.findOneAndUpdate({}, { $setOnInsert: { updatedAt: new Date() } }, {
timestamps: { createdAt: true, updatedAt: false }
});
```
+
+## Updating Timestamps
+
+If you need to disable Mongoose's timestamps and update a document's timestamps to a different value using `updateOne()` or `findOneAndUpdate()`, you need to do the following:
+
+1. Set the `timestamps` option to `false` to prevent Mongoose from setting `updatedAt`.
+2. Set `overwriteImmutable` to `true` to allow overwriting `createdAt`, which is an immutable property by default.
+
+```javascript
+const createdAt = new Date('2011-06-01');
+// Update a document's `createdAt` to a custom value.
+// Normally Mongoose would prevent doing this because `createdAt` is immutable.
+await Model.updateOne({ _id: doc._id }, { createdAt }, { overwriteImmutable: true, timestamps: false });
+
+doc = await Model.collection.findOne({ _id: doc._id });
+doc.createdAt.valueOf() === createdAt.valueOf(); // true
+```
diff --git a/docs/transactions.md b/docs/transactions.md
index 4be5f79c35c..d69ae7b8efc 100644
--- a/docs/transactions.md
+++ b/docs/transactions.md
@@ -1,13 +1,12 @@
# Transactions in Mongoose
-[Transactions](https://www.mongodb.com/transactions) are new in MongoDB
-4.0 and Mongoose 5.2.0. Transactions let you execute multiple operations
-in isolation and potentially undo all the operations if one of them fails.
+[Transactions](https://www.mongodb.com/transactions) let you execute multiple operations in isolation and potentially undo all the operations if one of them fails.
This guide will get you started using transactions with Mongoose.
-
+## Getting Started with Transactions {#getting-started-with-transactions}
If you haven't already, import mongoose:
+
```javascript
import mongoose from 'mongoose';
```
@@ -27,10 +26,10 @@ const session = await db.startSession();
In practice, you should use either the [`session.withTransaction()` helper](https://mongodb.github.io/node-mongodb-native/3.2/api/ClientSession.html#withTransaction)
or Mongoose's `Connection#transaction()` function to run a transaction. The `session.withTransaction()` helper handles:
-- Creating a transaction
-- Committing the transaction if it succeeds
-- Aborting the transaction if your operation throws
-- Retrying in the event of a [transient transaction error](https://stackoverflow.com/questions/52153538/what-is-a-transienttransactionerror-in-mongoose-or-mongodb).
+* Creating a transaction
+* Committing the transaction if it succeeds
+* Aborting the transaction if your operation throws
+* Retrying in the event of a [transient transaction error](https://stackoverflow.com/questions/52153538/what-is-a-transienttransactionerror-in-mongoose-or-mongodb).
```acquit
[require:transactions.*withTransaction]
@@ -62,7 +61,12 @@ await db.transaction(async function setRank(session) {
doc.isNew;
```
-
+## Note About Parallelism in Transactions {#note-about-parallelism-in-transactions}
+
+Running operations in parallel is **not supported** during a transaction. The use of `Promise.all`, `Promise.allSettled`, `Promise.race`, etc. to parallelize operations inside a transaction is
+undefined behaviour and should be avoided.
+
+## With Mongoose Documents and `save()` {#with-mongoose-documents-and-save}
If you get a [Mongoose document](documents.html) from [`findOne()`](api/model.html#model_Model-findOne)
or [`find()`](api/model.html#model_Model-find) using a session, the document will
@@ -74,7 +78,7 @@ To get/set the session associated with a given document, use [`doc.$session()`](
[require:transactions.*save]
```
-
+## With the Aggregation Framework {#with-the-aggregation-framework}
The `Model.aggregate()` function also supports transactions. Mongoose
aggregations have a [`session()` helper](api/aggregate.html#aggregate_Aggregate-session)
@@ -85,7 +89,34 @@ Below is an example of executing an aggregation within a transaction.
[require:transactions.*aggregate]
```
-
+## Using AsyncLocalStorage {#asynclocalstorage}
+
+One major pain point with transactions in Mongoose is that you need to remember to set the `session` option on every operation.
+If you don't, your operation will execute outside of the transaction.
+Mongoose 8.4 is able to set the `session` operation on all operations within a `Connection.prototype.transaction()` executor function using Node's [AsyncLocalStorage API](https://nodejs.org/api/async_context.html#class-asynclocalstorage).
+Set the `transactionAsyncLocalStorage` option using `mongoose.set('transactionAsyncLocalStorage', true)` to enable this feature.
+
+```javascript
+mongoose.set('transactionAsyncLocalStorage', true);
+
+const Test = mongoose.model('Test', mongoose.Schema({ name: String }));
+
+const doc = new Test({ name: 'test' });
+
+// Save a new doc in a transaction that aborts
+await connection.transaction(async() => {
+ await doc.save(); // Notice no session here
+ throw new Error('Oops');
+}).catch(() => {});
+
+// false, `save()` was rolled back
+await Test.exists({ _id: doc._id });
+```
+
+With `transactionAsyncLocalStorage`, you no longer need to pass sessions to every operation.
+Mongoose will add the session by default under the hood.
+
+## Advanced Usage {#advanced-usage}
Advanced users who want more fine-grained control over when they commit or abort transactions
can use `session.startTransaction()` to start a transaction:
diff --git a/docs/tutorials/dates.md b/docs/tutorials/dates.md
index 7fd564fe15a..78787b7139b 100644
--- a/docs/tutorials/dates.md
+++ b/docs/tutorials/dates.md
@@ -48,7 +48,7 @@ examples of querying by dates, date ranges, and sorting by date:
## Casting Edge Cases
-Date casting has a couple small cases where it differs from JavaScript's
+Date casting has a couple small cases where it differs from JavaScript's
native date parsing. First, Mongoose looks for a [`valueOf()` function](https://www.w3schools.com/jsref/jsref_valueof_string.asp) on the given object,
and calls `valueOf()` before casting the date. This means Mongoose can cast
[moment objects](http://npmjs.com/package/moment) to dates automatically.
diff --git a/docs/tutorials/findoneandupdate.md b/docs/tutorials/findoneandupdate.md
index de3dece2d6e..896c5d77736 100644
--- a/docs/tutorials/findoneandupdate.md
+++ b/docs/tutorials/findoneandupdate.md
@@ -1,15 +1,26 @@
# How to Use `findOneAndUpdate()` in Mongoose
-The [`findOneAndUpdate()` function in Mongoose](../api/query.html#query_Query-findOneAndUpdate) has a wide variety of use cases. [You should use `save()` to update documents where possible](https://masteringjs.io/tutorials/mongoose/update), but there are some cases where you need to use [`findOneAndUpdate()`](https://masteringjs.io/tutorials/mongoose/findoneandupdate). In this tutorial, you'll see how to use `findOneAndUpdate()`, and learn when you need to use it.
+The [`findOneAndUpdate()` function in Mongoose](../api/query.html#query_Query-findOneAndUpdate) has a wide variety of use cases. [You should use `save()` to update documents where possible](https://masteringjs.io/tutorials/mongoose/update), for better [validation](../validation.html) and [middleware](../middleware.html) support.
+However, there are some cases where you need to use [`findOneAndUpdate()`](https://masteringjs.io/tutorials/mongoose/findoneandupdate). In this tutorial, you'll see how to use `findOneAndUpdate()`, and learn when you need to use it.
* [Getting Started](#getting-started)
* [Atomic Updates](#atomic-updates)
* [Upsert](#upsert)
-* [The `rawResult` Option](#raw-result)
+* [The `includeResultMetadata` Option](#includeresultmetadata)
+* [Updating Discriminator Keys](#updating-discriminator-keys)
## Getting Started
-As the name implies, `findOneAndUpdate()` finds the first document that matches a given `filter`, applies an `update`, and returns the document. By default, `findOneAndUpdate()` returns the document as it was **before** `update` was applied.
+As the name implies, `findOneAndUpdate()` finds the first document that matches a given `filter`, applies an `update`, and returns the document.
+The `findOneAndUpdate()` function has the following signature:
+
+```javascript
+function findOneAndUpdate(filter, update, options) {}
+```
+
+By default, `findOneAndUpdate()` returns the document as it was **before** `update` was applied.
+In the following example, `doc` initially only has `name` and `_id` properties.
+`findOneAndUpdate()` adds an `age` property, but the result of `findOneAndUpdate()` does **not** have an `age` property.
```acquit
[require:Tutorial.*findOneAndUpdate.*basic case]
@@ -34,7 +45,7 @@ which has the same option.
## Atomic Updates
-With the exception of an [unindexed upsert](https://www.mongodb.com/docs/manual/reference/method/db.collection.findAndModify/#upsert-with-unique-index), [`findOneAndUpdate()` is atomic](https://www.mongodb.com/docs/manual/core/write-operations-atomicity/#atomicity). That means you can assume the document doesn't change between when MongoDB finds the document and when it updates the document, _unless_ you're doing an [upsert](#upsert).
+With the exception of an [unindexed upsert](https://www.mongodb.com/docs/manual/reference/method/db.collection.findAndModify/#upsert-with-unique-index), [`findOneAndUpdate()` is atomic](https://www.mongodb.com/docs/manual/core/write-operations-atomicity/#atomicity). That means you can assume the document doesn't change between when MongoDB finds the document and when it updates the document, *unless* you're doing an [upsert](#upsert).
For example, if you're using `save()` to update a document, the document can change in MongoDB in between when you load the document using `findOne()` and when you save the document using `save()` as show below. For many use cases, the `save()` race condition is a non-issue. But you can work around it with `findOneAndUpdate()` (or [transactions](../transactions.html)) if you need to.
@@ -50,22 +61,22 @@ Using the `upsert` option, you can use `findOneAndUpdate()` as a find-and-[upser
[require:Tutorial.*findOneAndUpdate.*upsert]
```
-The `rawResult` Option
+
Mongoose transforms the result of `findOneAndUpdate()` by default: it
returns the updated document. That makes it difficult to check whether
a document was upserted or not. In order to get the updated document
and check whether MongoDB upserted a new document in the same operation,
-you can set the `rawResult` flag to make Mongoose return the raw result
+you can set the `includeResultMetadata` flag to make Mongoose return the raw result
from MongoDB.
```acquit
-[require:Tutorial.*findOneAndUpdate.*rawResult$]
+[require:Tutorial.*findOneAndUpdate.*includeResultMetadata$]
```
Here's what the `res` object from the above example looks like:
-```
+```txt
{ lastErrorObject:
{ n: 1,
updatedExisting: false,
@@ -77,3 +88,31 @@ Here's what the `res` object from the above example looks like:
age: 29 },
ok: 1 }
```
+
+## Updating Discriminator Keys
+
+Mongoose prevents updating the [discriminator key](../discriminators.html#discriminator-keys) using `findOneAndUpdate()` by default.
+For example, suppose you have the following discriminator models.
+
+```javascript
+const eventSchema = new mongoose.Schema({ time: Date });
+const Event = db.model('Event', eventSchema);
+
+const ClickedLinkEvent = Event.discriminator(
+ 'ClickedLink',
+ new mongoose.Schema({ url: String })
+);
+
+const SignedUpEvent = Event.discriminator(
+ 'SignedUp',
+ new mongoose.Schema({ username: String })
+);
+```
+
+Mongoose will remove `__t` (the default discriminator key) from the `update` parameter, if `__t` is set.
+This is to prevent unintentional updates to the discriminator key; for example, if you're passing untrusted user input to the `update` parameter.
+However, you can tell Mongoose to allow updating the discriminator key by setting the `overwriteDiscriminatorKey` option to `true` as shown below.
+
+```acquit
+[require:use overwriteDiscriminatorKey to change discriminator key]
+```
diff --git a/docs/tutorials/getters-setters.md b/docs/tutorials/getters-setters.md
index 80464a1d6be..e358e01127a 100644
--- a/docs/tutorials/getters-setters.md
+++ b/docs/tutorials/getters-setters.md
@@ -55,7 +55,7 @@ To skip getters on a one-off basis, use [`user.get()` with the `getters` option
## Setters
-Suppose you want to make sure all user emails in your database are lowercased to
+Suppose you want to make sure all user emails in your database are lowercased to
make it easy to search without worrying about case. Below is an example
`userSchema` that ensures emails are lowercased.
@@ -97,7 +97,7 @@ For example, the following shows how you can use `$locals` to configure the lang
Mongoose setters are different from [ES6 setters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/set) because they allow you to transform the value being set. With ES6 setters, you
would need to store an internal `_email` property to use a setter. With Mongoose,
-you do **not** need to define an internal `_email` property or define a
+you do **not** need to define an internal `_email` property or define a
corresponding getter for `email`.
```acquit
diff --git a/docs/tutorials/lean.md b/docs/tutorials/lean.md
index 8edd3438ba7..e11641449d1 100644
--- a/docs/tutorials/lean.md
+++ b/docs/tutorials/lean.md
@@ -10,8 +10,9 @@ In this tutorial, you'll learn more about the tradeoffs of using `lean()`.
* [Lean and Populate](#lean-and-populate)
* [When to Use Lean](#when-to-use-lean)
* [Plugins](#plugins)
+* [BigInts](#bigints)
-Using Lean
+## Using Lean
By default, Mongoose queries return an instance of the
[Mongoose `Document` class](../api/document.html#Document). Documents are much
@@ -52,7 +53,7 @@ and virtuals don't run if you enable `lean`.
[require:Lean Tutorial.*getters and virtuals]
```
-Lean and Populate
+## Lean and Populate
[Populate](../populate.html) works with `lean()`. If you
use both `populate()` and `lean()`, the `lean` option propagates to the
@@ -69,7 +70,7 @@ populated documents as well. In the below example, both the top-level
[require:Lean Tutorial.*virtual populate]
```
-When to Use Lean
+## When to Use Lean
If you're executing a query and sending the results without modification to,
say, an [Express response](http://expressjs.com/en/4x/api.html#res), you should
@@ -122,9 +123,9 @@ Using `lean()` bypasses all Mongoose features, including [virtuals](virtuals.htm
and [defaults](../api/schematype.html#schematype_SchemaType-default). If you want to
use these features with `lean()`, you need to use the corresponding plugin:
-- [mongoose-lean-virtuals](https://plugins.mongoosejs.io/plugins/lean-virtuals)
-- [mongoose-lean-getters](https://plugins.mongoosejs.io/plugins/lean-getters)
-- [mongoose-lean-defaults](https://www.npmjs.com/package/mongoose-lean-defaults)
+* [mongoose-lean-virtuals](https://plugins.mongoosejs.io/plugins/lean-virtuals)
+* [mongoose-lean-getters](https://plugins.mongoosejs.io/plugins/lean-getters)
+* [mongoose-lean-defaults](https://www.npmjs.com/package/mongoose-lean-defaults)
However, you need to keep in mind that Mongoose does not hydrate lean documents,
so `this` will be a POJO in virtuals, getters, and default functions.
@@ -140,3 +141,12 @@ schema.virtual('lowercase', function() {
this.get('name'); // Crashes because `this` is not a Mongoose document.
});
```
+
+## BigInts
+
+By default, the MongoDB Node driver converts longs stored in MongoDB into JavaScript numbers, **not** [BigInts](https://thecodebarbarian.com/an-overview-of-bigint-in-node-js.html).
+Set the `useBigInt64` option on your `lean()` queries to inflate longs into BigInts.
+
+```acquit
+[require:Lean Tutorial.*bigint]
+```
diff --git a/docs/tutorials/query_casting.md b/docs/tutorials/query_casting.md
index b90e00c42a3..9a3d7937ac1 100644
--- a/docs/tutorials/query_casting.md
+++ b/docs/tutorials/query_casting.md
@@ -7,7 +7,7 @@ In older content this parameter is sometimes called `query` or `conditions`. For
[require:Cast Tutorial.*get and set]
```
-When you execute the query using [`Query#exec()`](../api/query.html#query_Query-exec) or [`Query#then()`](../api/query.html#query_Query-then), Mongoose _casts_ the filter to match your schema.
+When you execute the query using [`Query#exec()`](../api/query.html#query_Query-exec) or [`Query#then()`](../api/query.html#query_Query-then), Mongoose *casts* the filter to match your schema.
```acquit
[require:Cast Tutorial.*cast values]
diff --git a/docs/tutorials/ssl.md b/docs/tutorials/ssl.md
index 3e3d9d2bd47..cbea4253445 100644
--- a/docs/tutorials/ssl.md
+++ b/docs/tutorials/ssl.md
@@ -1,19 +1,18 @@
-# SSL Connections
+# TLS/SSL Connections
-Mongoose supports connecting to [MongoDB clusters that require SSL connections](https://www.mongodb.com/docs/manual/tutorial/configure-ssl/). Setting the `ssl` option to `true` in [`mongoose.connect()`](../api/mongoose.html#mongoose_Mongoose-connect) or your connection string is enough to connect to a MongoDB cluster using SSL:
+Mongoose supports connecting to [MongoDB clusters that require TLS/SSL connections](https://www.mongodb.com/docs/manual/tutorial/configure-ssl/). Setting the `tls` option to `true` in [`mongoose.connect()`](../api/mongoose.html#mongoose_Mongoose-connect) or your connection string is enough to connect to a MongoDB cluster using TLS/SSL:
```javascript
-mongoose.connect('mongodb://127.0.0.1:27017/test', { ssl: true });
+mongoose.connect('mongodb://127.0.0.1:27017/test', { tls: true });
// Equivalent:
-mongoose.connect('mongodb://127.0.0.1:27017/test?ssl=true');
+mongoose.connect('mongodb://127.0.0.1:27017/test?tls=true');
```
-The `ssl` option defaults to `false` for connection strings that start with `mongodb://`. However,
-the `ssl` option defaults to `true` for connection strings that start with `mongodb+srv://`. So if you are using an srv connection string to connect to [MongoDB Atlas](https://www.mongodb.com/cloud/atlas), SSL is enabled by default.
+The `tls` option defaults to `false` for connection strings that start with `mongodb://`. However,
+the `tls` option defaults to `true` for connection strings that start with `mongodb+srv://`. So if you are using an srv connection string to connect to [MongoDB Atlas](https://www.mongodb.com/cloud/atlas), TLS/SSL is enabled by default.
-If you try to connect to a MongoDB cluster that requires SSL without enabling the `ssl` option, `mongoose.connect()`
-will error out with the below error:
+If you try to connect to a MongoDB cluster that requires TLS/SSL without enabling the `tls`/`ssl` option, `mongoose.connect()` will error out with the below error:
```no-highlight
MongooseServerSelectionError: connection to 127.0.0.1:27017 closed
@@ -21,22 +20,21 @@ MongooseServerSelectionError: connection to 127.0.0.1:27017 closed
...
```
-## SSL Validation
+## TLS/SSL Validation
-By default, Mongoose validates the SSL certificate against a [certificate authority](https://en.wikipedia.org/wiki/Certificate_authority) to ensure the SSL certificate is valid. To disable this validation, set the `sslValidate` option
-to `false`.
+By default, Mongoose validates the TLS/SSL certificate against a [certificate authority](https://en.wikipedia.org/wiki/Certificate_authority) to ensure the TLS/SSL certificate is valid. To disable this validation, set the `tlsAllowInvalidCertificates` (or `tlsInsecure`) option to `true`.
```javascript
mongoose.connect('mongodb://127.0.0.1:27017/test', {
- ssl: true,
- sslValidate: false
+ tls: true,
+ tlsAllowInvalidCertificates: true,
});
```
-In most cases, you should not disable SSL validation in production. However, `sslValidate: false` is often helpful
-for debugging SSL connection issues. If you can connect to MongoDB with `sslValidate: false`, but not with
-`sslValidate: true`, then you can confirm Mongoose can connect to the server and the server is configured to use
-SSL correctly, but there's some issue with the SSL certificate.
+In most cases, you should not disable TLS/SSL validation in production. However, `tlsAllowInvalidCertificates: true` is often helpful
+for debugging SSL connection issues. If you can connect to MongoDB with `tlsAllowInvalidCertificates: true`, but not with
+`tlsAllowInvalidCertificates: false`, then you can confirm Mongoose can connect to the server and the server is configured to use
+TLS/SSL correctly, but there's some issue with the certificate.
For example, a common issue is the below error message:
@@ -45,17 +43,14 @@ MongooseServerSelectionError: unable to verify the first certificate
```
This error is often caused by [self-signed MongoDB certificates](https://medium.com/@rajanmaharjan/secure-your-mongodb-connections-ssl-tls-92e2addb3c89) or other situations where the certificate sent by the MongoDB
-server is not registered with an established certificate authority. The solution is to set the `sslCA` option, which essentially sets a list of allowed SSL certificates.
+server is not registered with an established certificate authority. The solution is to set the `tlsCAFile` option, which essentially sets a list of allowed SSL certificates.
```javascript
await mongoose.connect('mongodb://127.0.0.1:27017/test', {
- ssl: true,
- sslValidate: true,
+ tls: true,
// For example, see https://medium.com/@rajanmaharjan/secure-your-mongodb-connections-ssl-tls-92e2addb3c89
// for where the `rootCA.pem` file comes from.
- // Please note that, in Mongoose >= 5.8.3, `sslCA` needs to be
- // the **path to** the CA file, **not** the contents of the CA file
- sslCA: `${__dirname}/rootCA.pem`
+ tlsCAFile: `${__dirname}/rootCA.pem`,
});
```
@@ -66,28 +61,49 @@ MongooseServerSelectionError: Hostname/IP does not match certificate's altnames:
```
The SSL certificate's [common name](https://knowledge.digicert.com/solution/SO7239.html) **must** line up with the host name
-in your connection string. If the SSL certificate is for `hostname2.mydomain.com`, your connection string must connect to `hostname2.mydomain.com`, not any other hostname or IP address that may be equivalent to `hostname2.mydomain.com`. For replica sets, this also means that the SSL certificate's common name must line up with the [machine's `hostname`](../connections.html#replicaset-hostnames).
+in your connection string. If the SSL certificate is for `hostname2.mydomain.com`, your connection string must connect to `hostname2.mydomain.com`, not any other hostname or IP address that may be equivalent to `hostname2.mydomain.com`. For replica sets, this also means that the SSL certificate's common name must line up with the [machine's `hostname`](../connections.html#replicaset-hostnames). To disable this validation, set the `tlsAllowInvalidHostnames` option to `true`.
-## X509 Auth
+## X.509 Authentication
-If you're using [X509 authentication](https://www.mongodb.com/docs/drivers/node/current/fundamentals/authentication/mechanisms/#x.509), you should set the user name in the connection string, **not** the `connect()` options.
+If you're using [X.509 authentication](https://www.mongodb.com/docs/drivers/node/current/fundamentals/authentication/mechanisms/#x.509), you should set the user name in the connection string, **not** the `connect()` options.
```javascript
// Do this:
const username = 'myusername';
await mongoose.connect(`mongodb://${encodeURIComponent(username)}@127.0.0.1:27017/test`, {
- ssl: true,
- sslValidate: true,
- sslCA: `${__dirname}/rootCA.pem`,
- authMechanism: 'MONGODB-X509'
+ tls: true,
+ tlsCAFile: `${__dirname}/rootCA.pem`,
+ authMechanism: 'MONGODB-X509',
});
// Not this:
await mongoose.connect('mongodb://127.0.0.1:27017/test', {
- ssl: true,
- sslValidate: true,
- sslCA: `${__dirname}/rootCA.pem`,
+ tls: true,
+ tlsCAFile: `${__dirname}/rootCA.pem`,
authMechanism: 'MONGODB-X509',
- auth: { username }
+ auth: { username },
});
```
+
+## X.509 Authentication with MongoDB Atlas
+
+With MongoDB Atlas, X.509 certificates are not Root CA certificates and will not work with the `tlsCAFile` parameter as self-signed certificates would. If the `tlsCAFile` parameter is used an error similar to the following would be raised:
+
+```no-highlight
+MongoServerSelectionError: unable to get local issuer certificate
+```
+
+To connect to a MongoDB Atlas cluster using X.509 authentication the correct option to set is `tlsCertificateKeyFile`. The connection string already specifies the `authSource` and `authMechanism`, however they're included below as `connect()` options for completeness:
+
+```javascript
+const url = 'mongodb+srv://xyz.mongodb.net/test?authSource=%24external&authMechanism=MONGODB-X509';
+await mongoose.connect(url, {
+ tls: true,
+ // location of a local .pem file that contains both the client's certificate and key
+ tlsCertificateKeyFile: '/path/to/certificate.pem',
+ authMechanism: 'MONGODB-X509',
+ authSource: '$external',
+});
+```
+
+**Note** The connection string options must be URL escaped correctly.
diff --git a/docs/tutorials/virtuals.md b/docs/tutorials/virtuals.md
index 6d2e7766000..160decfc9ea 100644
--- a/docs/tutorials/virtuals.md
+++ b/docs/tutorials/virtuals.md
@@ -1,6 +1,6 @@
# Mongoose Virtuals
-In Mongoose, a virtual is a property that is **not** stored in MongoDB.
+In Mongoose, a virtual is a property that is **not** stored in MongoDB.
Virtuals are typically used for computed properties on documents.
* [Your First Virtual](#your-first-virtual)
@@ -9,12 +9,13 @@ Virtuals are typically used for computed properties on documents.
* [Virtuals with Lean](#virtuals-with-lean)
* [Limitations](#limitations)
* [Populate](#populate)
+* [Virtuals via schema options](#virtuals-via-schema-options)
* [Further Reading](#further-reading)
## Your First Virtual
Suppose you have a `User` model. Every user has an `email`, but you also
-want the email's domain. For example, the domain portion of
+want the email's domain. For example, the domain portion of
'test@gmail.com' is 'gmail.com'.
Below is one way to implement the `domain` property using a virtual.
@@ -98,13 +99,27 @@ Mongoose also supports [populating virtuals](../populate.html). A populated
virtual contains documents from another collection. To define a populated
virtual, you need to specify:
-- The `ref` option, which tells Mongoose which model to populate documents from.
-- The `localField` and `foreignField` options. Mongoose will populate documents from the model in `ref` whose `foreignField` matches this document's `localField`.
+* The `ref` option, which tells Mongoose which model to populate documents from.
+* The `localField` and `foreignField` options. Mongoose will populate documents from the model in `ref` whose `foreignField` matches this document's `localField`.
```acquit
[require:Virtuals.*populate]
```
+## Virtuals via schema options
+
+Virtuals can also be defined in the schema-options directly without having to use [`.virtual`](../api/schema.html#Schema.prototype.virtual):
+
+```acquit
+[require:Virtuals.*schema-options fullName]
+```
+
+The same also goes for virtual options, like virtual populate:
+
+```acquit
+[require:Virtuals.*schema-options populate]
+```
+
## Further Reading
* [Virtuals in Mongoose Schemas](../guide.html#virtuals)
diff --git a/docs/typescript.md b/docs/typescript.md
index 9278c7eda5e..a27f5cc8b95 100644
--- a/docs/typescript.md
+++ b/docs/typescript.md
@@ -6,7 +6,7 @@ This guide describes Mongoose's recommended approach to working with Mongoose in
## Creating Your First Document
-To get started with Mongoose in TypeScript, you need to:
+To get started with Mongoose in TypeScript, you need to:
1. Create an interface representing a document in MongoDB.
2. Create a [Schema](guide.html) corresponding to the document interface.
@@ -54,7 +54,7 @@ You as the developer are responsible for ensuring that your document interface l
For example, Mongoose won't report an error if `email` is `required` in your Mongoose schema but optional in your document interface.
The `User()` constructor returns an instance of `HydratedDocument`.
-`IUser` is a _document interface_, it represents the raw object structure that `IUser` objects look like in MongoDB.
+`IUser` is a *document interface*, it represents the raw object structure that `IUser` objects look like in MongoDB.
`HydratedDocument` represents a hydrated Mongoose document, with methods, virtuals, and other Mongoose-specific features.
```ts
@@ -93,35 +93,12 @@ const userSchema = new Schema({
That's because `Schema.Types.ObjectId` is a [class that inherits from SchemaType](schematypes.html), **not** the class you use to create a new MongoDB ObjectId.
-## Using `extends Document`
-
-Alternatively, your document interface can extend Mongoose's `Document` class.
-
-We **strongly** recommend against using this approach, its support will be dropped in the next major version as it causes major performance issues.
-Many Mongoose TypeScript codebases use the below approach.
-
-```typescript
-import { Document, Schema, model, connect } from 'mongoose';
-
-interface IUser extends Document {
- name: string;
- email: string;
- avatar?: string;
-}
-```
-
-This approach works, but we recommend your document interface _not_ extend `Document`.
-Using `extends Document` makes it difficult for Mongoose to infer which properties are present on [query filters](queries.html), [lean documents](tutorials/lean.html), and other cases.
-
-We recommend your document interface contain the properties defined in your schema and line up with what your documents look like in MongoDB.
-Although you can add [instance methods](guide.html#methods) to your document interface, we do not recommend doing so.
-
## Using Custom Bindings
If Mongoose's built-in `index.d.ts` file does not work for you, you can remove it in a postinstall script in your `package.json` as shown below.
However, before you do, please [open an issue on Mongoose's GitHub page](https://github.com/Automattic/mongoose/issues/new) and describe the issue you're experiencing.
-```
+```json
{
"postinstall": "rm ./node_modules/mongoose/index.d.ts"
}
diff --git a/docs/typescript/populate.md b/docs/typescript/populate.md
index 87e53f08444..418ef167498 100644
--- a/docs/typescript/populate.md
+++ b/docs/typescript/populate.md
@@ -96,4 +96,4 @@ However, we recommend using the `.populate<{ child: Child }>` syntax from the fi
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
+2. In the `Parent` interface, `child` is a hydrated document, which makes it difficult for Mongoose to infer the type of `child` when you use `lean()` or `toObject()`.
diff --git a/docs/typescript/query-helpers.md b/docs/typescript/query-helpers.md
index 163fe90d36c..0f1c67a5483 100644
--- a/docs/typescript/query-helpers.md
+++ b/docs/typescript/query-helpers.md
@@ -29,7 +29,7 @@ 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 { HydratedDocument, Model, Query, Schema, model } from 'mongoose';
+import { HydratedDocument, Model, QueryWithHelpers, Schema, model, connect } from 'mongoose';
interface Project {
name?: string;
@@ -64,7 +64,7 @@ ProjectSchema.query.byName = function byName(
};
// 2nd param to `model()` is the Model class to return.
-const ProjectModel = model('Project', schema);
+const ProjectModel = model('Project', ProjectSchema);
run().catch(err => console.log(err));
diff --git a/docs/typescript/schemas.md b/docs/typescript/schemas.md
index f5e21468bb7..de53923ced6 100644
--- a/docs/typescript/schemas.md
+++ b/docs/typescript/schemas.md
@@ -1,89 +1,125 @@
# Schemas in TypeScript
Mongoose [schemas](../guide.html) are how you tell Mongoose what your documents look like.
-Mongoose schemas are separate from TypeScript interfaces, so you need to either define both a _document interface_ and a _schema_; or rely on Mongoose to automatically infer the type from the schema definition.
+Mongoose schemas are separate from TypeScript interfaces, so you need to either define both a *raw document interface* and a *schema*; or rely on Mongoose to automatically infer the type from the schema definition.
-## Separate document interface definition
+## Automatic type inference
-```typescript
-import { Schema } from 'mongoose';
-
-// Document interface
-interface User {
- name: string;
- email: string;
- avatar?: string;
-}
+Mongoose can automatically infer the document type from your schema definition as follows.
+We recommend relying on automatic type inference when defining schemas and models.
+```typescript
+import { Schema, model } from 'mongoose';
// Schema
-const schema = new Schema({
+const schema = new Schema({
name: { type: String, required: true },
email: { type: String, required: true },
avatar: String
});
+
+// `UserModel` will have `name: string`, etc.
+const UserModel = mongoose.model('User', schema);
+
+const doc = new UserModel({ name: 'test', email: 'test' });
+doc.name; // string
+doc.email; // string
+doc.avatar; // string | undefined | null
```
-By default, Mongoose does **not** check if your document interface lines up with your schema.
-For example, the above code won't throw an error if `email` is optional in the document interface, but `required` in `schema`.
+There are a few caveats for using automatic type inference:
-## Automatic type inference
+1. You need to set `strictNullChecks: true` or `strict: true` in your `tsconfig.json`. Or, if you're setting flags at the command line, `--strictNullChecks` or `--strict`. There are [known issues](https://github.com/Automattic/mongoose/issues/12420) with automatic type inference with strict mode disabled.
+2. You need to define your schema in the `new Schema()` call. Don't assign your schema definition to a temporary variable. Doing something like `const schemaDefinition = { name: String }; const schema = new Schema(schemaDefinition);` will not work.
+3. Mongoose adds `createdAt` and `updatedAt` to your schema if you specify the `timestamps` option in your schema, *except* if you also specify `methods`, `virtuals`, or `statics`. There is a [known issue](https://github.com/Automattic/mongoose/issues/12807) with type inference with timestamps and methods/virtuals/statics options. If you use methods, virtuals, and statics, you're responsible for adding `createdAt` and `updatedAt` to your schema definition.
-Mongoose can also automatically infer the document type from your schema definition as follows.
+If you must define your schema separately, use [as const](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions) (`const schemaDefinition = { ... } as const;`) to prevent *type widening*. TypeScript will automatically widen types like `required: false` to `required: boolean`, which will cause Mongoose to assume the field is required. Using `as const` forces TypeScript to retain these types.
-```typescript
-import { Schema, InferSchemaType } from 'mongoose';
+If you need to explicitly get the raw document type (the value returned from `doc.toObject()`, `await Model.findOne().lean()`, etc.) from your schema definition, you can use Mongoose's `inferRawDocType` helper as follows:
-// Document interface
-// No need to define TS interface any more.
-// interface User {
-// name: string;
-// email: string;
-// avatar?: string;
-// }
+```ts
+import { Schema, InferRawDocType, model } from 'mongoose';
-// Schema
-const schema = new Schema({
+const schemaDefinition = {
name: { type: String, required: true },
email: { type: String, required: true },
avatar: String
-});
+} as const;
+const schema = new Schema(schemaDefinition);
-type User = InferSchemaType;
-// InferSchemaType will determine the type as follows:
-// type User = {
-// name: string;
-// email: string;
-// avatar?: string;
-// }
+const UserModel = model('User', schema);
+const doc = new UserModel({ name: 'test', email: 'test' });
-// `UserModel` will have `name: string`, etc.
-const UserModel = mongoose.model('User', schema);
-```
+type RawUserDocument = InferRawDocType;
-There are a few caveats for using automatic type inference:
+useRawDoc(doc.toObject());
-1. You need to set `strictNullChecks: true` or `strict: true` in your `tsconfig.json`. Or, if you're setting flags at the command line, `--strictNullChecks` or `--strict`. There are [known issues](https://github.com/Automattic/mongoose/issues/12420) with automatic type inference with strict mode disabled.
-2. You need to define your schema in the `new Schema()` call. Don't assign your schema definition to a temporary variable. Doing something like `const schemaDefinition = { name: String }; const schema = new Schema(schemaDefinition);` will not work.
-3. Mongoose adds `createdAt` and `updatedAt` to your schema if you specify the `timestamps` option in your schema, _except_ if you also specify `methods`, `virtuals`, or `statics`. There is a [known issue](https://github.com/Automattic/mongoose/issues/12807) with type inference with timestamps and methods/virtuals/statics options. If you use methods, virtuals, and statics, you're responsible for adding `createdAt` and `updatedAt` to your schema definition.
+function useRawDoc(doc: RawUserDocument) {
+ // ...
+}
+
+```
If automatic type inference doesn't work for you, you can always fall back to document interface definitions.
+## Separate document interface definition
+
+If automatic type inference doesn't work for you, you can define a separate raw document interface as follows.
+
+```typescript
+import { Schema } from 'mongoose';
+
+// Raw document interface. Contains the data type as it will be stored
+// in MongoDB. So you can ObjectId, Buffer, and other custom primitive data types.
+// But no Mongoose document arrays or subdocuments.
+interface User {
+ name: string;
+ email: string;
+ avatar?: string;
+}
+
+// Schema
+const schema = new Schema({
+ name: { type: String, required: true },
+ email: { type: String, required: true },
+ avatar: String
+});
+```
+
+By default, Mongoose does **not** check if your raw document interface lines up with your schema.
+For example, the above code won't throw an error if `email` is optional in the document interface, but `required` in `schema`.
+
## Generic parameters
-The Mongoose `Schema` class in TypeScript has 4 [generic parameters](https://www.typescriptlang.org/docs/handbook/2/generics.html):
+The Mongoose `Schema` class in TypeScript has 9 [generic parameters](https://www.typescriptlang.org/docs/handbook/2/generics.html):
-- `DocType` - An interface descibing how the data is saved in MongoDB
-- `M` - The Mongoose model type. Can be omitted if there are no query helpers or instance methods to be defined.
- - default: `Model`
-- `TInstanceMethods` - An interface containing the methods for the schema.
- - default: `{}`
-- `TQueryHelpers` - An interface containing query helpers defined on the schema. Defaults to `{}`.
+* `RawDocType` - An interface describing how the data is saved in MongoDB
+* `TModelType` - The Mongoose model type. Can be omitted if there are no query helpers or instance methods to be defined.
+ * default: `Model`
+* `TInstanceMethods` - An interface containing the methods for the schema.
+ * default: `{}`
+* `TQueryHelpers` - An interface containing query helpers defined on the schema. Defaults to `{}`.
+* `TVirtuals` - An interface containing virtuals defined on the schema. Defaults to `{}`
+* `TStaticMethods` - An interface containing methods on a model. Defaults to `{}`
+* `TSchemaOptions` - The type passed as the 2nd option to `Schema()` constructor. Defaults to `DefaultSchemaOptions`.
+* `DocType` - The inferred document type from the schema.
+* `THydratedDocumentType` - The hydrated document type. This is the default return type for `await Model.findOne()`, `Model.hydrate()`, etc.
View TypeScript definition
-
+
```typescript
- class Schema, TInstanceMethods = {}, TQueryHelpers = {}> extends events.EventEmitter {
+ export class Schema<
+ RawDocType = any,
+ TModelType = Model,
+ TInstanceMethods = {},
+ TQueryHelpers = {},
+ TVirtuals = {},
+ TStaticMethods = {},
+ TSchemaOptions = DefaultSchemaOptions,
+ DocType = ...,
+ THydratedDocumentType = HydratedDocument, TVirtuals & TInstanceMethods>
+ >
+ extends events.EventEmitter {
// ...
}
```
@@ -154,33 +190,62 @@ This is because Mongoose has numerous features that add paths to your schema tha
## Arrays
-When you define an array in a document interface, we recommend using Mongoose's `Types.Array` type for primitive arrays or `Types.DocumentArray` for arrays of documents.
+When you define an array in a document interface, we recommend using vanilla JavaScript arrays, **not** Mongoose's `Types.Array` type or `Types.DocumentArray` type.
+Instead, use the `THydratedDocumentType` generic for models and schemas to define that the hydrated document type has paths of type `Types.Array` and `Types.DocumentArray`.
```typescript
-import { Schema, Model, Types } from 'mongoose';
-
-interface BlogPost {
- _id: Types.ObjectId;
- title: string;
-}
+import mongoose from 'mongoose'
+const { Schema } = mongoose;
-interface User {
- tags: Types.Array;
- blogPosts: Types.DocumentArray;
+interface IOrder {
+ tags: Array<{ name: string }>
}
-const schema = new Schema>({
- tags: [String],
- blogPosts: [{ title: String }]
+// Define a HydratedDocumentType that describes what type Mongoose should use
+// for fully hydrated docs returned from `findOne()`, etc.
+type OrderHydratedDocument = mongoose.HydratedDocument<
+ IOrder,
+ { tags: mongoose.HydratedArraySubdocument<{ name: string }> }
+>;
+type OrderModelType = mongoose.Model<
+ IOrder,
+ {},
+ {},
+ {},
+ OrderHydratedDocument // THydratedDocumentType
+>;
+
+const orderSchema = new mongoose.Schema<
+ IOrder,
+ OrderModelType,
+ {}, // methods
+ {}, // query helpers
+ {}, // virtuals
+ {}, // statics
+ mongoose.DefaultSchemaOptions, // schema options
+ IOrder, // doctype
+ OrderHydratedDocument // THydratedDocumentType
+>({
+ tags: [{ name: { type: String, required: true } }]
});
-```
+const OrderModel = mongoose.model('Order', orderSchema);
-Using `Types.DocumentArray` is helpful when dealing with defaults.
-For example, `BlogPost` has an `_id` property that Mongoose will set by default.
-If you use `Types.DocumentArray` in the above case, you'll be able to `push()` a subdocument without an `_id`.
+// Demonstrating return types from OrderModel
+const doc = new OrderModel({ tags: [{ name: 'test' }] });
-```typescript
-const user = new User({ blogPosts: [] });
+doc.tags; // mongoose.Types.DocumentArray<{ name: string }>
+doc.toObject().tags; // Array<{ name: string }>
+
+async function run() {
+ const docFromDb = await OrderModel.findOne().orFail();
+ docFromDb.tags; // mongoose.Types.DocumentArray<{ name: string }>
-user.blogPosts.push({ title: 'test' }); // Would not work if you did `blogPosts: BlogPost[]`
+ const leanDoc = await OrderModel.findOne().orFail().lean();
+ leanDoc.tags; // Array<{ name: string }>
+};
```
+
+Use `HydratedArraySubdocument` for the type of array subdocuments, and `HydratedSingleSubdocument` for single subdocuments.
+
+If you are not using [schema methods](../guide.html#methods), middleware, or [virtuals](../tutorials/virtuals.html), you can omit the last 7 generic parameters to `Schema()` and just define your schema using `new mongoose.Schema(...)`.
+The THydratedDocumentType parameter for schemas is primarily for setting the value of `this` on methods and virtuals.
diff --git a/docs/typescript/statics.md b/docs/typescript/statics.md
index 81f04423613..1d7c2d2738d 100644
--- a/docs/typescript/statics.md
+++ b/docs/typescript/statics.md
@@ -1,5 +1,37 @@
# Statics in TypeScript
+To use Mongoose's automatic type inference to define types for your [statics](guide.html#statics) and [methods](guide.html#methods), you should define your methods and statics using the `methods` and `statics` schema options as follows.
+Do **not** use `Schema.prototype.method()` and `Schema.prototype.static()`.
+
+```typescript
+const userSchema = new mongoose.Schema(
+ { name: { type: String, required: true } },
+ {
+ methods: {
+ updateName(name: string) {
+ this.name = name;
+ return this.save();
+ }
+ },
+ statics: {
+ createWithName(name: string) {
+ return this.create({ name });
+ }
+ }
+ }
+);
+const UserModel = mongoose.model('User', userSchema);
+
+const doc = new UserModel({ name: 'test' });
+// Compiles correctly
+doc.updateName('foo');
+// Compiles correctly
+UserModel.createWithName('bar');
+```
+
+## With Generics
+
+We recommend using Mongoose's automatic type inference where possible, but you can use `Schema` and `Model` generics to set up type inference for your statics and methods.
Mongoose [models](../models.html) do **not** have an explicit generic parameter for [statics](guide.html#statics).
If your model has statics, we recommend creating an interface that [extends](https://www.typescriptlang.org/docs/handbook/interfaces.html) Mongoose's `Model` interface as shown below.
@@ -24,24 +56,27 @@ const User = model('User', schema);
const answer: number = User.myStaticMethod(); // 42
```
-Mongoose does support auto typed static functions that it are supplied in schema options.
-Static functions can be defined by:
+You should pass methods as the 3rd generic param to the `Schema` constructor as follows.
```typescript
-import { Schema, model } from 'mongoose';
+import { Model, Schema, model } from 'mongoose';
-const schema = new Schema(
- { name: String },
- {
- statics: {
- myStaticMethod() {
- return 42;
- }
- }
- }
-);
+interface IUser {
+ name: string;
+}
-const User = model('User', schema);
+interface UserMethods {
+ updateName(name: string): Promise;
+}
-const answer = User.myStaticMethod(); // 42
+const schema = new Schema, UserMethods>({ name: String });
+schema.method('updateName', function updateName(name) {
+ this.name = name;
+ return this.save();
+});
+
+const User = model('User', schema);
+const doc = new User({ name: 'test' });
+// Compiles correctly
+doc.updateName('foo');
```
diff --git a/docs/typescript/subdocuments.md b/docs/typescript/subdocuments.md
index 3d30c7fb923..49edbb4ca27 100644
--- a/docs/typescript/subdocuments.md
+++ b/docs/typescript/subdocuments.md
@@ -1,7 +1,7 @@
# Handling Subdocuments in TypeScript
Subdocuments are tricky in TypeScript.
-By default, Mongoose treats object properties in document interfaces as _nested properties_ rather than subdocuments.
+By default, Mongoose treats object properties in document interfaces as *nested properties* rather than subdocuments.
```ts
// Setup
@@ -34,23 +34,23 @@ doc.names.ownerDocument();
```
Mongoose provides a mechanism to override types in the hydrated document.
-The 3rd generic param to the `Model<>` is called `TMethodsAndOverrides`: originally it was just used to define methods, but you can also use it to override types as shown below.
+Define a separate `THydratedDocumentType` and pass it as the 5th generic param to `mongoose.Model<>`.
+`THydratedDocumentType` controls what type Mongoose uses for "hydrated documents", that is, what `await UserModel.findOne()`, `UserModel.hydrate()`, and `new UserModel()` return.
```ts
// Define property overrides for hydrated documents
-type UserDocumentOverrides = {
- names: Types.Subdocument & Names;
-};
-type UserModelType = Model;
+type THydratedUserDocument = {
+ names?: mongoose.Types.Subdocument
+}
+type UserModelType = mongoose.Model;
-const userSchema = new Schema({
- names: new Schema({ firstName: String })
+const userSchema = new mongoose.Schema({
+ names: new mongoose.Schema({ firstName: String })
});
-const UserModel = model('User', userSchema);
-
+const UserModel = mongoose.model('User', userSchema);
const doc = new UserModel({ names: { _id: '0'.repeat(24), firstName: 'foo' } });
-doc.names.ownerDocument(); // Works, `names` is a subdocument!
+doc.names!.ownerDocument(); // Works, `names` is a subdocument!
```
## Subdocument Arrays
@@ -69,10 +69,10 @@ interface User {
}
// TMethodsAndOverrides
-type UserDocumentProps = {
- names: Types.DocumentArray;
-};
-type UserModelType = Model;
+type THydratedUserDocument = {
+ names?: Types.DocumentArray
+}
+type UserModelType = Model;
// Create model
const UserModel = model('User', new Schema({
diff --git a/docs/typescript/virtuals.md b/docs/typescript/virtuals.md
index 3395d97797c..c59faba3e2d 100644
--- a/docs/typescript/virtuals.md
+++ b/docs/typescript/virtuals.md
@@ -3,7 +3,7 @@
[Virtuals](../tutorials/virtuals.html) are computed properties: you can access virtuals on hydrated Mongoose documents, but virtuals are **not** stored in MongoDB.
Mongoose supports auto typed virtuals so you don't need to define additional typescript interface anymore but you are still able to do so.
-## Automatically Inferred Types:
+## Automatically Inferred Types
To make mongoose able to infer virtuals type, You have to define them in schema constructor as following:
@@ -29,7 +29,7 @@ const schema = new Schema(
```
Note that Mongoose does **not** include virtuals in the returned type from `InferSchemaType`.
-That is because `InferSchemaType` returns the "raw" document interface, which represents the structure of the data stored in MongoDB.
+That is because `InferSchemaType` returns a value similar to the raw document interface, which represents the structure of the data stored in MongoDB.
```ts
type User = InferSchemaType;
@@ -52,7 +52,7 @@ user.fullName;
type UserDocument = ReturnType<(typeof UserModel)['hydrate']>;
```
-## Set virtuals type manually:
+## Set virtuals type manually
You shouldn't define virtuals in your TypeScript [document interface](../typescript.html).
Instead, you should define a separate interface for your virtuals, and pass this interface to `Model` and `Schema`.
diff --git a/docs/validation.md b/docs/validation.md
index f0b6102bf4b..43ae5ceaef6 100644
--- a/docs/validation.md
+++ b/docs/validation.md
@@ -2,41 +2,41 @@
Before we get into the specifics of validation syntax, please keep the following rules in mind:
-- Validation is defined in the [SchemaType](schematypes.html)
-- Validation is [middleware](middleware.html). Mongoose registers validation as a `pre('save')` hook on every schema by default.
-- Validation always runs as the **first** `pre('save')` hook. This means that validation doesn't run on any changes you make in `pre('save')` hooks.
-- You can disable automatic validation before save by setting the [validateBeforeSave](guide.html#validateBeforeSave) option
-- You can manually run validation using `doc.validate()` or `doc.validateSync()`
-- You can manually mark a field as invalid (causing validation to fail) by using [`doc.invalidate(...)`](api/document.html#document_Document-invalidate)
-- Validators are not run on undefined values. The only exception is the [`required` validator](api/schematype.html#schematype_SchemaType-required).
-- When you call [Model#save](api/model.html#model_Model-save), Mongoose also runs subdocument validation. If an error occurs, your [Model#save](api/model.html#model_Model-save) promise rejects
-- Validation is customizable
+* Validation is defined in the [SchemaType](schematypes.html)
+* Validation is [middleware](middleware.html). Mongoose registers validation as a `pre('save')` hook on every schema by default.
+* Validation always runs as the **first** `pre('save')` hook. This means that validation doesn't run on any changes you make in `pre('save')` hooks.
+* You can disable automatic validation before save by setting the [validateBeforeSave](guide.html#validateBeforeSave) option
+* You can manually run validation using `doc.validate()` or `doc.validateSync()`
+* You can manually mark a field as invalid (causing validation to fail) by using [`doc.invalidate(...)`](api/document.html#document_Document-invalidate)
+* Validators are not run on undefined values. The only exception is the [`required` validator](api/schematype.html#schematype_SchemaType-required).
+* When you call [Model#save](api/model.html#model_Model-save), Mongoose also runs subdocument validation. If an error occurs, your [Model#save](api/model.html#model_Model-save) promise rejects
+* Validation is customizable
```acquit
[require:Validation$]
```
-- [Built-in Validators](#built-in-validators)
-- [Custom Error Messages](#custom-error-messages)
-- [The `unique` Option is Not a Validator](#the-unique-option-is-not-a-validator)
-- [Custom Validators](#custom-validators)
-- [Async Custom Validators](#async-custom-validators)
-- [Validation Errors](#validation-errors)
-- [Cast Errors](#cast-errors)
-- [Global SchemaType Validation](#global-schematype-validation)
-- [Required Validators On Nested Objects](#required-validators-on-nested-objects)
-- [Update Validators](#update-validators)
-- [Update Validators and `this`](#update-validators-and-this)
-- [Update Validators Only Run On Updated Paths](#update-validators-only-run-on-updated-paths)
-- [Update Validators Only Run For Some Operations](#update-validators-only-run-for-some-operations)
+* [Built-in Validators](#built-in-validators)
+* [Custom Error Messages](#custom-error-messages)
+* [The `unique` Option is Not a Validator](#the-unique-option-is-not-a-validator)
+* [Custom Validators](#custom-validators)
+* [Async Custom Validators](#async-custom-validators)
+* [Validation Errors](#validation-errors)
+* [Cast Errors](#cast-errors)
+* [Global SchemaType Validation](#global-schematype-validation)
+* [Required Validators On Nested Objects](#required-validators-on-nested-objects)
+* [Update Validators](#update-validators)
+* [Update Validators and `this`](#update-validators-and-this)
+* [Update Validators Only Run On Updated Paths](#update-validators-only-run-on-updated-paths)
+* [Update Validators Only Run For Some Operations](#update-validators-only-run-for-some-operations)
## Built-in Validators
Mongoose has several built-in validators.
-- All [SchemaTypes](schematypes.html) have the built-in [required](api/schematype.html#schematype_SchemaType-required) validator. The required validator uses the [SchemaType's `checkRequired()` function](api/schematype.html#schematype_SchemaType-checkRequired) to determine if the value satisfies the required validator.
-- [Numbers](schematypes.html#numbers) have [`min` and `max`](schematypes.html#number-validators) validators.
-- [Strings](schematypes.html#strings) have [`enum`, `match`, `minLength`, and `maxLength`](schematypes.html#string-validators) validators.
+* All [SchemaTypes](schematypes.html) have the built-in [required](api/schematype.html#schematype_SchemaType-required) validator. The required validator uses the [SchemaType's `checkRequired()` function](api/schematype.html#schematype_SchemaType-checkRequired) to determine if the value satisfies the required validator.
+* [Numbers](schematypes.html#numbers) have [`min` and `max`](schematypes.html#number-validators) validators.
+* [Strings](schematypes.html#strings) have [`enum`, `match`, `minLength`, and `maxLength`](schematypes.html#string-validators) validators.
Each of the validator links above provide more information about how to enable them and customize their error messages.
@@ -49,8 +49,8 @@ Each of the validator links above provide more information about how to enable t
You can configure the error message for individual validators in your schema. There are two equivalent
ways to set the validator error message:
-- Array syntax: `min: [6, 'Must be at least 6, got {VALUE}']`
-- Object syntax: `enum: { values: ['Coffee', 'Tea'], message: '{VALUE} is not supported' }`
+* Array syntax: `min: [6, 'Must be at least 6, got {VALUE}']`
+* Object syntax: `enum: { values: ['Coffee', 'Tea'], message: '{VALUE} is not supported' }`
Mongoose also supports rudimentary templating for error messages.
Mongoose replaces `{VALUE}` with the value being validated.
@@ -109,23 +109,39 @@ thrown.
## Cast Errors
-Before running validators, Mongoose attempts to coerce values to the
-correct type. This process is called _casting_ the document. If
-casting fails for a given path, the `error.errors` object will contain
-a `CastError` object.
+Before running validators, Mongoose attempts to coerce values to the correct type. This process is called *casting* the document.
+If casting fails for a given path, the `error.errors` object will contain a `CastError` object.
-Casting runs before validation, and validation does not run if casting
-fails. That means your custom validators may assume `v` is `null`,
-`undefined`, or an instance of the type specified in your schema.
+Casting runs before validation, and validation does not run if casting fails.
+That means your custom validators may assume `v` is `null`, `undefined`, or an instance of the type specified in your schema.
```acquit
[require:Cast Errors]
```
+By default, Mongoose cast error messages look like `Cast to Number failed for value "pie" at path "numWheels"`.
+You can overwrite Mongoose's default cast error message by the `cast` option on your SchemaType to a string as follows.
+
+```acquit
+[require:Cast Error Message Overwrite]
+```
+
+Mongoose's cast error message templating supports the following parameters:
+
+* `{PATH}`: the path that failed to cast
+* `{VALUE}`: a string representation of the value that failed to cast
+* `{KIND}`: the type that Mongoose attempted to cast to, like `'String'` or `'Number'`
+
+You can also define a function that Mongoose will call to get the cast error message as follows.
+
+```acquit
+[require:Cast Error Message Function Overwrite]
+```
+
## Global SchemaType Validation
In addition to defining custom validators on individual schema paths, you can also configure a custom validator to run on every instance of a given `SchemaType`.
-For example, the following code demonstrates how to make empty string `''` an invalid value for _all_ string paths.
+For example, the following code demonstrates how to make empty string `''` an invalid value for *all* string paths.
```acquit
[require:Global SchemaType Validation]
@@ -190,12 +206,12 @@ you try to explicitly `$unset` the key.
One final detail worth noting: update validators **only** run on the
following update operators:
-- `$set`
-- `$unset`
-- `$push`
-- `$addToSet`
-- `$pull`
-- `$pullAll`
+* `$set`
+* `$unset`
+* `$push`
+* `$addToSet`
+* `$pull`
+* `$pullAll`
For instance, the below update will succeed, regardless of the value of
`number`, because update validators ignore `$inc`.
diff --git a/docs/version-support.md b/docs/version-support.md
index 037e5b1847a..986b69e6115 100644
--- a/docs/version-support.md
+++ b/docs/version-support.md
@@ -1,27 +1,21 @@
# Version Support
-Mongoose 7.x (released February 27, 2023) is the current Mongoose major version.
+Mongoose 8.x (released October 31, 2023) is the current Mongoose major version.
+We ship all new bug fixes and features to 8.x.
+
+## Mongoose 7
+
+Mongoose 7.x (released February 27, 2023) is currently in legacy support.
We ship all new bug fixes and features to 7.x.
## Mongoose 6
-Mongoose 6.x (released August 24, 2021) is currently in legacy support.
-We will continue to ship bug fixes to Mongoose 6 until August 24, 2023.
-After August 24, 2023, we will only ship security fixes, and backport requested fixes to Mongoose 6.
+Mongoose 6.x (released August 24, 2021) is currently only receiving security fixes and requested bug fixes as of August 24, 2023.
Please open a [bug report on GitHub](https://github.com/Automattic/mongoose/issues/new?assignees=&labels=&template=bug.yml) to request backporting a fix to Mongoose 6.
-We are **not** actively backporting any new features from Mongoose 7 into Mongoose 6.
-Until August 24, 2023, we will backport requested features into Mongoose 6; please open a [feature request on GitHub](https://github.com/Automattic/mongoose/issues/new?assignees=&labels=enhancement%2Cnew+feature&template=feature.yml) to request backporting a feature into Mongoose 6.
-After August 24, 2023, we will not backport any new features into Mongoose 6.
-
-We do not currently have a formal end of life (EOL) date for Mongoose 6.
-However, we will not end support for Mongoose 6 until at least January 1, 2024.
+Mongoose 6.x end of life (EOL) is January 1, 2025.
+Mongoose 6.x will no longer receive any updates, security or otherwise, after that date.
## Mongoose 5
-Mongoose 5.x (released January 17, 2018) is currently only receiving security fixes and requested bug fixes.
-Please open a [bug report on GitHub](https://github.com/Automattic/mongoose/issues/new?assignees=&labels=&template=bug.yml) to request backporting a fix to Mongoose 5.
-We will **not** backport any new features from Mongoose 6 or Mongoose 7 into Mongoose 5.
-
-Mongoose 5.x end of life (EOL) is March 1, 2024.
-Mongoose 5.x will no longer receive any updates, security or otherwise, after that date.
\ No newline at end of file
+Mongoose 5.x (released January 17, 2018) is End-of-Life (EOL) since March 1, 2024. Mongoose 5.x will no longer receive any updates, security or otherwise.
diff --git a/examples/README.md b/examples/README.md
index cb32898e8eb..8511ee44434 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -1,41 +1,41 @@
+# Examples
+
This directory contains runnable sample mongoose programs.
To run:
- - first install [Node.js](http://nodejs.org/)
- - from the root of the project, execute `npm install -d`
- - in the example directory, run `npm install -d`
- - from the command line, execute: `node example.js`, replacing "example.js" with the name of a program.
-
+* first install [Node.js](http://nodejs.org/)
+* from the root of the project, execute `npm install -d`
+* in the example directory, run `npm install -d`
+* from the command line, execute: `node example.js`, replacing "example.js" with the name of a program.
Goal is to show:
-- ~~global schemas~~
-- ~~GeoJSON schemas / use (with crs)~~
-- text search (once MongoDB removes the "Experimental/beta" label)
-- ~~lean queries~~
-- ~~statics~~
-- methods and statics on subdocs
-- custom types
-- ~~querybuilder~~
-- ~~promises~~
-- accessing driver collection, db
-- ~~connecting to replica sets~~
-- connecting to sharded clusters
-- enabling a fail fast mode
-- on the fly schemas
-- storing files
-- ~~map reduce~~
-- ~~aggregation~~
-- advanced hooks
-- using $elemMatch to return a subset of an array
-- query casting
-- upserts
-- pagination
-- express + mongoose session handling
-- ~~group by (use aggregation)~~
-- authentication
-- schema migration techniques
-- converting documents to plain objects (show transforms)
-- how to $unset
-
+* ~~global schemas~~
+* ~~GeoJSON schemas / use (with crs)~~
+* text search (once MongoDB removes the "Experimental/beta" label)
+* ~~lean queries~~
+* ~~statics~~
+* methods and statics on subdocs
+* custom types
+* ~~querybuilder~~
+* ~~promises~~
+* accessing driver collection, db
+* ~~connecting to replica sets~~
+* connecting to sharded clusters
+* enabling a fail fast mode
+* on the fly schemas
+* storing files
+* ~~map reduce~~
+* ~~aggregation~~
+* advanced hooks
+* using $elemMatch to return a subset of an array
+* query casting
+* upserts
+* pagination
+* express + mongoose session handling
+* ~~group by (use aggregation)~~
+* authentication
+* schema migration techniques
+* converting documents to plain objects (show transforms)
+* how to $unset
diff --git a/examples/express/README.md b/examples/express/README.md
index 7ba07b8ab6d..c3caa9c088d 100644
--- a/examples/express/README.md
+++ b/examples/express/README.md
@@ -1 +1 @@
-Mongoose + Express examples
+# Mongoose + Express examples
diff --git a/examples/express/connection-sharing/README.md b/examples/express/connection-sharing/README.md
index 9f732f0965e..b734d875bd8 100644
--- a/examples/express/connection-sharing/README.md
+++ b/examples/express/connection-sharing/README.md
@@ -1,6 +1,7 @@
+# Express Connection sharing Example
To run:
-- Execute `npm install` from this directory
-- Execute `node app.js`
-- Navigate to `127.0.0.1:8000`
+* Execute `npm install` from this directory
+* Execute `node app.js`
+* Navigate to `127.0.0.1:8000`
diff --git a/examples/mapreduce/mapreduce.js b/examples/mapreduce/mapreduce.js
deleted file mode 100644
index 23bf8f4a60e..00000000000
--- a/examples/mapreduce/mapreduce.js
+++ /dev/null
@@ -1,102 +0,0 @@
-// import async to make control flow simplier
-'use strict';
-
-const async = require('async');
-
-// import the rest of the normal stuff
-const mongoose = require('../../lib');
-
-require('./person.js')();
-
-const Person = mongoose.model('Person');
-
-// define some dummy data
-const data = [
- {
- name: 'bill',
- age: 25,
- birthday: new Date().setFullYear((new Date().getFullYear() - 25)),
- gender: 'Male'
- },
- {
- name: 'mary',
- age: 30,
- birthday: new Date().setFullYear((new Date().getFullYear() - 30)),
- gender: 'Female'
- },
- {
- name: 'bob',
- age: 21,
- birthday: new Date().setFullYear((new Date().getFullYear() - 21)),
- gender: 'Male'
- },
- {
- name: 'lilly',
- age: 26,
- birthday: new Date().setFullYear((new Date().getFullYear() - 26)),
- gender: 'Female'
- },
- {
- name: 'alucard',
- age: 1000,
- birthday: new Date().setFullYear((new Date().getFullYear() - 1000)),
- gender: 'Male'
- }
-];
-
-
-mongoose.connect('mongodb://127.0.0.1/persons', function(err) {
- if (err) throw err;
-
- // create all of the dummy people
- async.each(data, function(item, cb) {
- Person.create(item, cb);
- }, function(err) {
- if (err) {
- // handle error
- }
-
- // alright, simple map reduce example. We will find the total ages of each
- // gender
-
- // create the options object
- const o = {};
-
- o.map = function() {
- // in this function, 'this' refers to the current document being
- // processed. Return the (gender, age) tuple using
- /* global emit */
- emit(this.gender, this.age);
- };
-
- // the reduce function receives the array of ages that are grouped by the
- // id, which in this case is the gender
- o.reduce = function(id, ages) {
- return Array.sum(ages);
- };
-
- // other options that can be specified
-
- // o.query = { age : { $lt : 1000 }}; // the query object
- // o.limit = 3; // max number of documents
- // o.keeptemp = true; // default is false, specifies whether to keep temp data
- // o.finalize = someFunc; // function called after reduce
- // o.scope = {}; // the scope variable exposed to map/reduce/finalize
- // o.jsMode = true; // default is false, force execution to stay in JS
- o.verbose = true; // default is false, provide stats on the job
- // o.out = {}; // objects to specify where output goes, by default is
- // returned, but can also be stored in a new collection
- // see: http://mongoosejs.com/docs/api/model.html#model_Model-mapReduce
- Person.mapReduce(o, function(err, results, stats) {
- console.log('map reduce took %d ms', stats.processtime);
- console.log(results);
- cleanup();
- });
- });
-});
-
-function cleanup() {
- Person.remove(function() {
- mongoose.disconnect();
- });
-}
diff --git a/examples/mapreduce/package.json b/examples/mapreduce/package.json
deleted file mode 100644
index 424006845e4..00000000000
--- a/examples/mapreduce/package.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "name": "map-reduce-example",
- "private": "true",
- "version": "0.0.0",
- "description": "deps for map reduce example",
- "main": "mapreduce.js",
- "scripts": {
- "test": "echo \"Error: no test specified\" && exit 1"
- },
- "dependencies": { "async": "*" },
- "repository": "",
- "author": "",
- "license": "BSD"
-}
diff --git a/examples/mapreduce/person.js b/examples/mapreduce/person.js
deleted file mode 100644
index 3f1a651a3da..00000000000
--- a/examples/mapreduce/person.js
+++ /dev/null
@@ -1,18 +0,0 @@
-
-// import the necessary modules
-'use strict';
-
-const mongoose = require('../../lib');
-const Schema = mongoose.Schema;
-
-// create an export function to encapsulate the model creation
-module.exports = function() {
- // define schema
- const PersonSchema = new Schema({
- name: String,
- age: Number,
- birthday: Date,
- gender: String
- });
- mongoose.model('Person', PersonSchema);
-};
diff --git a/index.js b/index.js
index 7320766f0e5..6ebbd5fd5d3 100644
--- a/index.js
+++ b/index.js
@@ -28,6 +28,7 @@ module.exports.modelNames = mongoose.modelNames;
module.exports.plugin = mongoose.plugin;
module.exports.connections = mongoose.connections;
module.exports.version = mongoose.version;
+module.exports.Aggregate = mongoose.Aggregate;
module.exports.Mongoose = mongoose.Mongoose;
module.exports.Schema = mongoose.Schema;
module.exports.SchemaType = mongoose.SchemaType;
@@ -35,7 +36,6 @@ module.exports.SchemaTypes = mongoose.SchemaTypes;
module.exports.VirtualType = mongoose.VirtualType;
module.exports.Types = mongoose.Types;
module.exports.Query = mongoose.Query;
-module.exports.Promise = mongoose.Promise;
module.exports.Model = mongoose.Model;
module.exports.Document = mongoose.Document;
module.exports.ObjectId = mongoose.ObjectId;
@@ -47,6 +47,7 @@ module.exports.Mixed = mongoose.Mixed;
module.exports.Date = mongoose.Date;
module.exports.Number = mongoose.Number;
module.exports.Error = mongoose.Error;
+module.exports.MongooseError = mongoose.MongooseError;
module.exports.now = mongoose.now;
module.exports.CastError = mongoose.CastError;
module.exports.SchemaTypeOptions = mongoose.SchemaTypeOptions;
diff --git a/index.pug b/index.pug
index 7976e086b0a..14cba69a0ff 100644
--- a/index.pug
+++ b/index.pug
@@ -5,31 +5,22 @@ html(lang='en')
meta(name="viewport", content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no")
title Mongoose ODM v#{package.version}
link(href="//fonts.googleapis.com/css?family=Anonymous+Pro:400,700|Droid+Sans+Mono|Open+Sans:400,700|Linden+Hill|Quattrocento:400,700|News+Cycle:400,700|Antic+Slab|Cabin+Condensed:400,700", rel="stylesheet", type="text/css")
- link(href="docs/css/default.css", rel="stylesheet")
link(href="docs/css/style.css", rel="stylesheet")
- link(href="/docs/css/github.css", rel="stylesheet")
-
- link(rel='apple-touch-icon', sizes='57x57', href='docs/images/favicon/apple-icon-57x57.png')
- link(rel='apple-touch-icon', sizes='60x60', href='docs/images/favicon/apple-icon-60x60.png')
- link(rel='apple-touch-icon', sizes='72x72', href='docs/images/favicon/apple-icon-72x72.png')
- link(rel='apple-touch-icon', sizes='76x76', href='docs/images/favicon/apple-icon-76x76.png')
- link(rel='apple-touch-icon', sizes='114x114', href='docs/images/favicon/apple-icon-114x114.png')
- link(rel='apple-touch-icon', sizes='120x120', href='docs/images/favicon/apple-icon-120x120.png')
- link(rel='apple-touch-icon', sizes='144x144', href='docs/images/favicon/apple-icon-144x144.png')
- link(rel='apple-touch-icon', sizes='152x152', href='docs/images/favicon/apple-icon-152x152.png')
- link(rel='apple-touch-icon', sizes='180x180', href='docs/images/favicon/apple-icon-180x180.png')
- link(rel='icon', type='image/png', sizes='192x192', href='docs/images/favicon/android-icon-192x192.png')
- link(rel='icon', type='image/png', sizes='32x32', href='docs/images/favicon/favicon-32x32.png')
- link(rel='icon', type='image/png', sizes='96x96', href='docs/images/favicon/favicon-96x96.png')
- link(rel='icon', type='image/png', sizes='16x16', href='docs/images/favicon/favicon-16x16.png')
- link(rel='manifest', href='docs/images/favicon/manifest.json')
+ link(href="docs/css/github.css", rel="stylesheet")
+ link(href="docs/css/carbonads.css", rel="stylesheet")
+
+ include ./docs/includes/favicon
+
meta(name='msapplication-TileColor', content='#ffffff')
meta(name='msapplication-TileImage', content='docs/images/favicon/ms-icon-144x144.png')
meta(name='theme-color', content='#ffffff')
+ link(rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-fork-ribbon-css/0.2.3/gh-fork-ribbon.min.css")
+
style.
- code {
+ pre {
font-size: 1em;
+ line-height: 1;
}
.sponsor {
@@ -47,37 +38,6 @@ html(lang='en')
text-decoration: none;
}
- .carbonad{
- margin-top:0!important;
- margin-bottom:-3rem!important
- }
-
- #carbonads {
- position:fixed;
- right: 0px;
- bottom: 0px;
- display:block;
- width:200px;
- padding:15px 15px 15px 160px;
- overflow:hidden;
- font-size:13px;
- line-height:1.4;
- text-align:left;
- background-color: #fafafa;
- }
-
- @media (max-width: 1160px) {
- #carbonads {
- display: none !important;
- }
- }
-
- #carbonads a{color:#333;text-decoration:none}
-
- .carbon-img{float:left;margin-left:-145px}
-
- .carbon-poweredby{display:block;color:#777!important}
-
img.sponsor {
margin-right: 10px;
border: 1px dotted #dfdfdf;
@@ -85,8 +45,8 @@ html(lang='en')
}
body
- a#forkbanner(href="http://github.com/Automattic/mongoose")
- img(style="position: absolute; top: 0; right: 0; border: 0;", src="https://s3.amazonaws.com/github/ribbons/forkme_right_gray_6d6d6d.png", alt="Fork me on GitHub")
+ a(class="github-fork-ribbon" href="/Automattic/mongoose" data-ribbon="Fork me on GitHub" title="Fork me on GitHub" target="_blank").
+ Fork me on GitHub
#wrap.homepage
#header
h1
diff --git a/lib/aggregate.js b/lib/aggregate.js
index 36f81c2810d..1d4a0d18e22 100644
--- a/lib/aggregate.js
+++ b/lib/aggregate.js
@@ -4,12 +4,13 @@
* Module dependencies
*/
-const AggregationCursor = require('./cursor/AggregationCursor');
+const AggregationCursor = require('./cursor/aggregationCursor');
+const MongooseError = require('./error/mongooseError');
const Query = require('./query');
const { applyGlobalMaxTimeMS, applyGlobalDiskUse } = require('./helpers/query/applyGlobalOption');
+const clone = require('./helpers/clone');
const getConstructorName = require('./helpers/getConstructorName');
const prepareDiscriminatorPipeline = require('./helpers/aggregate/prepareDiscriminatorPipeline');
-const promiseOrCallback = require('./helpers/promiseOrCallback');
const stringifyFunctionOperators = require('./helpers/aggregate/stringifyFunctionOperators');
const utils = require('./utils');
const read = Query.prototype.read;
@@ -19,7 +20,7 @@ const validRedactStringValues = new Set(['$$DESCEND', '$$PRUNE', '$$KEEP']);
/**
* Aggregate constructor used for building aggregation pipelines. Do not
- * instantiate this class directly, use [Model.aggregate()](/docs/api/model.html#model_Model-aggregate) instead.
+ * instantiate this class directly, use [Model.aggregate()](https://mongoosejs.com/docs/api/model.html#Model.aggregate()) instead.
*
* #### Example:
*
@@ -31,7 +32,7 @@ const validRedactStringValues = new Set(['$$DESCEND', '$$PRUNE', '$$KEEP']);
* Model.
* aggregate([{ $match: { age: { $gte: 21 }}}]).
* unwind('tags').
- * exec(callback);
+ * exec();
*
* #### Note:
*
@@ -63,20 +64,20 @@ function Aggregate(pipeline, model) {
* Contains options passed down to the [aggregate command](https://www.mongodb.com/docs/manual/reference/command/aggregate/).
* Supported options are:
*
- * - [`allowDiskUse`](#aggregate_Aggregate-allowDiskUse)
+ * - [`allowDiskUse`](https://mongoosejs.com/docs/api/aggregate.html#Aggregate.prototype.allowDiskUse())
* - `bypassDocumentValidation`
- * - [`collation`](#aggregate_Aggregate-collation)
+ * - [`collation`](https://mongoosejs.com/docs/api/aggregate.html#Aggregate.prototype.collation())
* - `comment`
- * - [`cursor`](#aggregate_Aggregate-cursor)
- * - [`explain`](#aggregate_Aggregate-explain)
+ * - [`cursor`](https://mongoosejs.com/docs/api/aggregate.html#Aggregate.prototype.cursor())
+ * - [`explain`](https://mongoosejs.com/docs/api/aggregate.html#Aggregate.prototype.explain())
* - `fieldsAsRaw`
- * - [`hint`](#aggregate_Aggregate-hint)
+ * - [`hint`](https://mongoosejs.com/docs/api/aggregate.html#Aggregate.prototype.hint())
* - `let`
* - `maxTimeMS`
* - `raw`
- * - [`readConcern`](#aggregate_Aggregate-readConcern)
+ * - [`readConcern`](https://mongoosejs.com/docs/api/aggregate.html#Aggregate.prototype.readConcern())
* - `readPreference`
- * - [`session`](#aggregate_Aggregate-session)
+ * - [`session`](https://mongoosejs.com/docs/api/aggregate.html#Aggregate.prototype.session())
* - `writeConcern`
*
* @property options
@@ -86,6 +87,24 @@ function Aggregate(pipeline, model) {
Aggregate.prototype.options;
+/**
+ * Returns default options for this aggregate.
+ *
+ * @param {Model} model
+ * @api private
+ */
+
+Aggregate.prototype._optionsForExec = function() {
+ const options = this.options || {};
+
+ const asyncLocalStorage = this.model()?.db?.base.transactionAsyncLocalStorage?.getStore();
+ if (!options.hasOwnProperty('session') && asyncLocalStorage?.session != null) {
+ options.session = asyncLocalStorage.session;
+ }
+
+ return options;
+};
+
/**
* Get/set the model that this aggregation will execute on.
*
@@ -186,7 +205,7 @@ Aggregate.prototype.addFields = function(arg) {
/**
* Appends a new $project operator to this aggregate pipeline.
*
- * Mongoose query [selection syntax](#query_Query-select) is also supported.
+ * Mongoose query [selection syntax](https://mongoosejs.com/docs/api/query.html#Query.prototype.select()) is also supported.
*
* #### Example:
*
@@ -664,7 +683,7 @@ Aggregate.prototype.unionWith = function(options) {
* await Model.aggregate(pipeline).read('primaryPreferred');
*
* @param {String|ReadPreference} pref one of the listed preference options or their aliases
- * @param {Array} [tags] optional tags for this query. DEPRECATED
+ * @param {Array} [tags] optional tags for this query.
* @return {Aggregate} this
* @api public
* @see mongodb https://www.mongodb.com/docs/manual/applications/replication/#read-preference
@@ -746,67 +765,69 @@ Aggregate.prototype.redact = function(expression, thenExpr, elseExpr) {
*
* #### Example:
*
- * Model.aggregate(..).explain(callback)
+ * Model.aggregate(..).explain()
*
* @param {String} [verbosity]
- * @param {Function} [callback] The callback function to call, if not specified, will return a Promise instead.
- * @return {Promise} Returns a promise if no "callback" is given
+ * @return {Promise}
*/
-Aggregate.prototype.explain = function(verbosity, callback) {
- const model = this._model;
- if (typeof verbosity === 'function') {
- callback = verbosity;
- verbosity = null;
+Aggregate.prototype.explain = async function explain(verbosity) {
+ if (typeof verbosity === 'function' || typeof arguments[1] === 'function') {
+ throw new MongooseError('Aggregate.prototype.explain() no longer accepts a callback');
}
+ const model = this._model;
- return promiseOrCallback(callback, cb => {
- if (!this._pipeline.length) {
- const err = new Error('Aggregate has empty pipeline');
- return cb(err);
- }
+ if (!this._pipeline.length) {
+ throw new Error('Aggregate has empty pipeline');
+ }
- prepareDiscriminatorPipeline(this._pipeline, this._model.schema);
+ prepareDiscriminatorPipeline(this._pipeline, this._model.schema);
+ await new Promise((resolve, reject) => {
model.hooks.execPre('aggregate', this, error => {
if (error) {
const _opts = { error: error };
return model.hooks.execPost('aggregate', this, [null], _opts, error => {
- cb(error);
+ reject(error);
});
+ } else {
+ resolve();
}
+ });
+ });
- model.collection.aggregate(this._pipeline, this.options, (error, cursor) => {
- if (error != null) {
- const _opts = { error: error };
- return model.hooks.execPost('aggregate', this, [null], _opts, error => {
- cb(error);
- });
- }
- if (verbosity != null) {
- cursor.explain(verbosity, (error, result) => {
- const _opts = { error: error };
- return model.hooks.execPost('aggregate', this, [result], _opts, error => {
- if (error) {
- return cb(error);
- }
- return cb(null, result);
- });
- });
- } else {
- cursor.explain((error, result) => {
- const _opts = { error: error };
- return model.hooks.execPost('aggregate', this, [result], _opts, error => {
- if (error) {
- return cb(error);
- }
- return cb(null, result);
- });
- });
+ const cursor = model.collection.aggregate(this._pipeline, this.options);
+
+ if (verbosity == null) {
+ verbosity = true;
+ }
+
+ let result = null;
+ try {
+ result = await cursor.explain(verbosity);
+ } catch (error) {
+ await new Promise((resolve, reject) => {
+ const _opts = { error: error };
+ model.hooks.execPost('aggregate', this, [null], _opts, error => {
+ if (error) {
+ return reject(error);
}
+ return resolve();
});
});
- }, model.events);
+ }
+
+ const _opts = { error: null };
+ await new Promise((resolve, reject) => {
+ model.hooks.execPost('aggregate', this, [result], _opts, error => {
+ if (error) {
+ return reject(error);
+ }
+ return resolve();
+ });
+ });
+
+ return result;
};
/**
@@ -831,7 +852,7 @@ Aggregate.prototype.allowDiskUse = function(value) {
*
* #### Example:
*
- * Model.aggregate(..).hint({ qty: 1, category: 1 }).exec(callback)
+ * Model.aggregate(..).hint({ qty: 1, category: 1 }).exec();
*
* @param {Object|String} value a hint object or the index name
* @return {Aggregate} this
@@ -844,7 +865,7 @@ Aggregate.prototype.hint = function(value) {
};
/**
- * Sets the session for this aggregation. Useful for [transactions](/docs/transactions.html).
+ * Sets the session for this aggregation. Useful for [transactions](https://mongoosejs.com/docs/transactions.html).
*
* #### Example:
*
@@ -876,8 +897,8 @@ Aggregate.prototype.session = function(session) {
* @param {Object} options keys to merge into current options
* @param {Number} [options.maxTimeMS] number limits the time this aggregation will run, see [MongoDB docs on `maxTimeMS`](https://www.mongodb.com/docs/manual/reference/operator/meta/maxTimeMS/)
* @param {Boolean} [options.allowDiskUse] boolean if true, the MongoDB server will use the hard drive to store data during this aggregation
- * @param {Object} [options.collation] object see [`Aggregate.prototype.collation()`](#aggregate_Aggregate-collation)
- * @param {ClientSession} [options.session] ClientSession see [`Aggregate.prototype.session()`](#aggregate_Aggregate-session)
+ * @param {Object} [options.collation] object see [`Aggregate.prototype.collation()`](https://mongoosejs.com/docs/api/aggregate.html#Aggregate.prototype.collation())
+ * @param {ClientSession} [options.session] ClientSession see [`Aggregate.prototype.session()`](https://mongoosejs.com/docs/api/aggregate.html#Aggregate.prototype.session())
* @see mongodb https://www.mongodb.com/docs/manual/reference/command/aggregate/
* @return {Aggregate} this
* @api public
@@ -911,6 +932,7 @@ Aggregate.prototype.option = function(value) {
*/
Aggregate.prototype.cursor = function(options) {
+ this._optionsForExec();
this.options.cursor = options || {};
return new AggregationCursor(this); // return this;
};
@@ -1000,66 +1022,81 @@ Aggregate.prototype.pipeline = function() {
* Executes the aggregate pipeline on the currently bound Model.
*
* #### Example:
- *
- * aggregate.exec(callback);
- *
- * // Because a promise is returned, the `callback` is optional.
* const result = await aggregate.exec();
*
- * @param {Function} [callback]
- * @return {Promise} Returns a Promise if no "callback" is given.
+ * @return {Promise}
* @api public
*/
-Aggregate.prototype.exec = function(callback) {
+Aggregate.prototype.exec = async function exec() {
if (!this._model) {
throw new Error('Aggregate not bound to any Model');
}
+ if (typeof arguments[0] === 'function') {
+ throw new MongooseError('Aggregate.prototype.exec() no longer accepts a callback');
+ }
const model = this._model;
const collection = this._model.collection;
- applyGlobalMaxTimeMS(this.options, model);
- applyGlobalDiskUse(this.options, model);
+ applyGlobalMaxTimeMS(this.options, model.db.options, model.base.options);
+ applyGlobalDiskUse(this.options, model.db.options, model.base.options);
+
+ this._optionsForExec();
if (this.options && this.options.cursor) {
return new AggregationCursor(this);
}
- return promiseOrCallback(callback, cb => {
- prepareDiscriminatorPipeline(this._pipeline, this._model.schema);
- stringifyFunctionOperators(this._pipeline);
+ prepareDiscriminatorPipeline(this._pipeline, this._model.schema);
+ stringifyFunctionOperators(this._pipeline);
+ await new Promise((resolve, reject) => {
model.hooks.execPre('aggregate', this, error => {
if (error) {
const _opts = { error: error };
return model.hooks.execPost('aggregate', this, [null], _opts, error => {
- cb(error);
+ reject(error);
});
+ } else {
+ resolve();
}
- if (!this._pipeline.length) {
- return cb(new Error('Aggregate has empty pipeline'));
- }
+ });
+ });
- const options = utils.clone(this.options || {});
+ if (!this._pipeline.length) {
+ throw new MongooseError('Aggregate has empty pipeline');
+ }
- collection.aggregate(this._pipeline, options, (err, cursor) => {
- if (err != null) {
- return cb(err);
+ const options = clone(this.options || {});
+
+ let result;
+ try {
+ const cursor = await collection.aggregate(this._pipeline, options);
+ result = await cursor.toArray();
+ } catch (error) {
+ await new Promise((resolve, reject) => {
+ const _opts = { error: error };
+ model.hooks.execPost('aggregate', this, [null], _opts, (error) => {
+ if (error) {
+ return reject(error);
}
- cursor.toArray((error, result) => {
- const _opts = { error: error };
- model.hooks.execPost('aggregate', this, [result], _opts, (error, result) => {
- if (error) {
- return cb(error);
- }
-
- cb(null, result);
- });
- });
+ resolve();
});
});
- }, model.events);
+ }
+
+ const _opts = { error: null };
+ await new Promise((resolve, reject) => {
+ model.hooks.execPost('aggregate', this, [result], _opts, error => {
+ if (error) {
+ return reject(error);
+ }
+ return resolve();
+ });
+ });
+
+ return result;
};
/**
@@ -1079,9 +1116,9 @@ Aggregate.prototype.then = function(resolve, reject) {
};
/**
- * Executes the query returning a `Promise` which will be
+ * Executes the aggregation returning a `Promise` which will be
* resolved with either the doc(s) or rejected with the error.
- * Like [`.then()`](#query_Query-then), but only takes a rejection handler.
+ * Like [`.then()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.then), but only takes a rejection handler.
* Compatible with `await`.
*
* @param {Function} [reject]
@@ -1093,6 +1130,21 @@ Aggregate.prototype.catch = function(reject) {
return this.exec().then(null, reject);
};
+/**
+ * Executes the aggregate returning a `Promise` which will be
+ * resolved with `.finally()` chained.
+ *
+ * More about [Promise `finally()` in JavaScript](https://thecodebarbarian.com/using-promise-finally-in-node-js.html).
+ *
+ * @param {Function} [onFinally]
+ * @return {Promise}
+ * @api public
+ */
+
+Aggregate.prototype.finally = function(onFinally) {
+ return this.exec().finally(onFinally);
+};
+
/**
* Returns an asyncIterator for use with [`for/await/of` loops](https://thecodebarbarian.com/getting-started-with-async-iterators-in-node-js)
* You do not need to call this function explicitly, the JavaScript runtime
diff --git a/lib/browser.js b/lib/browser.js
index 12664eb03ca..12b0cbde653 100644
--- a/lib/browser.js
+++ b/lib/browser.js
@@ -4,38 +4,12 @@
require('./driver').set(require('./drivers/browser'));
-const DocumentProvider = require('./document_provider.js');
-const PromiseProvider = require('./promise_provider');
+const DocumentProvider = require('./documentProvider.js');
DocumentProvider.setBrowser(true);
/**
- * The Mongoose [Promise](#promise_Promise) constructor.
- *
- * @method Promise
- * @api public
- */
-
-Object.defineProperty(exports, 'Promise', {
- get: function() {
- return PromiseProvider.get();
- },
- set: function(lib) {
- PromiseProvider.set(lib);
- }
-});
-
-/**
- * Storage layer for mongoose promises
- *
- * @method PromiseProvider
- * @api public
- */
-
-exports.PromiseProvider = PromiseProvider;
-
-/**
- * The [MongooseError](#error_MongooseError) constructor.
+ * The [MongooseError](https://mongoosejs.com/docs/api/error.html#Error()) constructor.
*
* @method Error
* @api public
@@ -44,7 +18,7 @@ exports.PromiseProvider = PromiseProvider;
exports.Error = require('./error/index');
/**
- * The Mongoose [Schema](#schema_Schema) constructor
+ * The Mongoose [Schema](https://mongoosejs.com/docs/api/schema.html#Schema()) constructor
*
* #### Example:
*
@@ -68,14 +42,14 @@ exports.Schema = require('./schema');
*
* #### Types:
*
- * - [Array](/docs/schematypes.html#arrays)
- * - [Buffer](/docs/schematypes.html#buffers)
- * - [Embedded](/docs/schematypes.html#schemas)
- * - [DocumentArray](/docs/api/documentarraypath.html)
- * - [Decimal128](/docs/api/mongoose.html#mongoose_Mongoose-Decimal128)
- * - [ObjectId](/docs/schematypes.html#objectids)
- * - [Map](/docs/schematypes.html#maps)
- * - [Subdocument](/docs/schematypes.html#schemas)
+ * - [Array](https://mongoosejs.com/docs/schematypes.html#arrays)
+ * - [Buffer](https://mongoosejs.com/docs/schematypes.html#buffers)
+ * - [Embedded](https://mongoosejs.com/docs/schematypes.html#schemas)
+ * - [DocumentArray](https://mongoosejs.com/docs/api/documentarraypath.html)
+ * - [Decimal128](https://mongoosejs.com/docs/api/decimal128.html#Decimal128())
+ * - [ObjectId](https://mongoosejs.com/docs/schematypes.html#objectids)
+ * - [Map](https://mongoosejs.com/docs/schematypes.html#maps)
+ * - [Subdocument](https://mongoosejs.com/docs/schematypes.html#schemas)
*
* Using this exposed access to the `ObjectId` type, we can construct ids on demand.
*
@@ -88,12 +62,12 @@ exports.Schema = require('./schema');
exports.Types = require('./types');
/**
- * The Mongoose [VirtualType](#virtualtype_VirtualType) constructor
+ * The Mongoose [VirtualType](https://mongoosejs.com/docs/api/virtualtype.html#VirtualType()) constructor
*
* @method VirtualType
* @api public
*/
-exports.VirtualType = require('./virtualtype');
+exports.VirtualType = require('./virtualType');
/**
* The various Mongoose SchemaTypes.
@@ -103,11 +77,11 @@ exports.VirtualType = require('./virtualtype');
* _Alias of mongoose.Schema.Types for backwards compatibility._
*
* @property SchemaTypes
- * @see Schema.SchemaTypes #schema_Schema-Types
+ * @see Schema.SchemaTypes https://mongoosejs.com/docs/api/schema.html#Schema.Types
* @api public
*/
-exports.SchemaType = require('./schematype.js');
+exports.SchemaType = require('./schemaType.js');
/**
* Internal utils
diff --git a/lib/cast.js b/lib/cast.js
index 8a5bb696999..03cbb3415c2 100644
--- a/lib/cast.js
+++ b/lib/cast.js
@@ -8,14 +8,15 @@ const CastError = require('./error/cast');
const StrictModeError = require('./error/strict');
const Types = require('./schema/index');
const cast$expr = require('./helpers/query/cast$expr');
+const castString = require('./cast/string');
const castTextSearch = require('./schema/operators/text');
const get = require('./helpers/get');
-const getConstructorName = require('./helpers/getConstructorName');
const getSchemaDiscriminatorByValue = require('./helpers/discriminator/getSchemaDiscriminatorByValue');
const isOperator = require('./helpers/query/isOperator');
const util = require('util');
const isObject = require('./helpers/isObject');
const isMongooseObject = require('./helpers/isMongooseObject');
+const utils = require('./utils');
const ALLOWED_GEOWITHIN_GEOJSON_TYPES = ['Polygon', 'MultiPolygon'];
@@ -27,6 +28,7 @@ const ALLOWED_GEOWITHIN_GEOJSON_TYPES = ['Polygon', 'MultiPolygon'];
* @param {Object} [options] the query options
* @param {Boolean|"throw"} [options.strict] Wheter to enable all strict options
* @param {Boolean|"throw"} [options.strictQuery] Enable strict Queries
+ * @param {Boolean} [options.sanitizeFilter] avoid adding implict query selectors ($in)
* @param {Boolean} [options.upsert]
* @param {Query} [context] passed to setters
* @api private
@@ -64,11 +66,27 @@ module.exports = function cast(schema, obj, options, context) {
if (!Array.isArray(val)) {
throw new CastError('Array', val, path);
}
- for (let k = 0; k < val.length; ++k) {
+ for (let k = val.length - 1; k >= 0; k--) {
if (val[k] == null || typeof val[k] !== 'object') {
throw new CastError('Object', val[k], path + '.' + k);
}
- val[k] = cast(schema, val[k], options, context);
+ const beforeCastKeysLength = Object.keys(val[k]).length;
+ const discriminatorValue = val[k][schema.options.discriminatorKey];
+ if (discriminatorValue == null) {
+ val[k] = cast(schema, val[k], options, context);
+ } else {
+ const discriminatorSchema = getSchemaDiscriminatorByValue(context.schema, discriminatorValue);
+ val[k] = cast(discriminatorSchema ? discriminatorSchema : schema, val[k], options, context);
+ }
+
+ if (Object.keys(val[k]).length === 0 && beforeCastKeysLength !== 0) {
+ val.splice(k, 1);
+ }
+ }
+
+ // delete empty: {$or: []} -> {}
+ if (val.length === 0) {
+ delete obj[path];
}
} else if (path === '$where') {
type = typeof val;
@@ -89,6 +107,9 @@ module.exports = function cast(schema, obj, options, context) {
val = cast(schema, val, options, context);
} else if (path === '$text') {
val = castTextSearch(val, path);
+ } else if (path === '$comment' && !schema.paths.hasOwnProperty('$comment')) {
+ val = castString(val, path);
+ obj[path] = val;
} else {
if (!schema) {
// no casting for Mixed types
@@ -118,9 +139,17 @@ module.exports = function cast(schema, obj, options, context) {
discriminatorKey != null &&
pathLastHalf !== discriminatorKey) {
const discriminatorVal = get(obj, pathFirstHalf + '.' + discriminatorKey);
- if (discriminatorVal != null) {
- schematype = _schematype.schema.discriminators[discriminatorVal].
- path(pathLastHalf);
+ const discriminators = _schematype.schema.discriminators;
+ if (typeof discriminatorVal === 'string' && discriminators[discriminatorVal] != null) {
+
+ schematype = discriminators[discriminatorVal].path(pathLastHalf);
+ } else if (discriminatorVal != null &&
+ Object.keys(discriminatorVal).length === 1 &&
+ Array.isArray(discriminatorVal.$in) &&
+ discriminatorVal.$in.length === 1 &&
+ typeof discriminatorVal.$in[0] === 'string' &&
+ discriminators[discriminatorVal.$in[0]] != null) {
+ schematype = discriminators[discriminatorVal.$in[0]].path(pathLastHalf);
}
}
}
@@ -185,16 +214,18 @@ module.exports = function cast(schema, obj, options, context) {
let value = val[geo];
if (val.$maxDistance != null) {
- val.$maxDistance = numbertype.castForQueryWrapper({
- val: val.$maxDistance,
- context: context
- });
+ val.$maxDistance = numbertype.castForQuery(
+ null,
+ val.$maxDistance,
+ context
+ );
}
if (val.$minDistance != null) {
- val.$minDistance = numbertype.castForQueryWrapper({
- val: val.$minDistance,
- context: context
- });
+ val.$minDistance = numbertype.castForQuery(
+ null,
+ val.$minDistance,
+ context
+ );
}
if (geo === '$within') {
@@ -216,16 +247,18 @@ module.exports = function cast(schema, obj, options, context) {
value.$geometry && typeof value.$geometry.type === 'string' &&
Array.isArray(value.$geometry.coordinates)) {
if (value.$maxDistance != null) {
- value.$maxDistance = numbertype.castForQueryWrapper({
- val: value.$maxDistance,
- context: context
- });
+ value.$maxDistance = numbertype.castForQuery(
+ null,
+ value.$maxDistance,
+ context
+ );
}
if (value.$minDistance != null) {
- value.$minDistance = numbertype.castForQueryWrapper({
- val: value.$minDistance,
- context: context
- });
+ value.$minDistance = numbertype.castForQuery(
+ null,
+ value.$minDistance,
+ context
+ );
}
if (isMongooseObject(value.$geometry)) {
value.$geometry = value.$geometry.toObject({
@@ -279,70 +312,86 @@ module.exports = function cast(schema, obj, options, context) {
}
} else if (val == null) {
continue;
- } else if (getConstructorName(val) === 'Object') {
+ } else if (utils.isPOJO(val)) {
any$conditionals = Object.keys(val).some(isOperator);
if (!any$conditionals) {
- obj[path] = schematype.castForQueryWrapper({
- val: val,
- context: context
- });
+ obj[path] = schematype.castForQuery(
+ null,
+ val,
+ context
+ );
} else {
const ks = Object.keys(val);
let $cond;
-
let k = ks.length;
while (k--) {
$cond = ks[k];
nested = val[$cond];
-
- if ($cond === '$not') {
+ if ($cond === '$elemMatch') {
+ if (nested && schematype != null && schematype.schema != null) {
+ cast(schematype.schema, nested, options, context);
+ } else if (nested && schematype != null && schematype.$isMongooseArray) {
+ if (utils.isPOJO(nested) && nested.$not != null) {
+ cast(schema, nested, options, context);
+ } else {
+ val[$cond] = schematype.castForQuery(
+ $cond,
+ nested,
+ context
+ );
+ }
+ }
+ } else if ($cond === '$not') {
if (nested && schematype) {
_keys = Object.keys(nested);
if (_keys.length && isOperator(_keys[0])) {
for (const key in nested) {
- nested[key] = schematype.castForQueryWrapper({
- $conditional: key,
- val: nested[key],
- context: context
- });
+ nested[key] = schematype.castForQuery(
+ key,
+ nested[key],
+ context
+ );
}
} else {
- val[$cond] = schematype.castForQueryWrapper({
- $conditional: $cond,
- val: nested,
- context: context
- });
+ val[$cond] = schematype.castForQuery(
+ $cond,
+ nested,
+ context
+ );
}
continue;
}
} else {
- val[$cond] = schematype.castForQueryWrapper({
- $conditional: $cond,
- val: nested,
- context: context
- });
+ val[$cond] = schematype.castForQuery(
+ $cond,
+ nested,
+ context
+ );
}
+
}
}
- } else if (Array.isArray(val) && ['Buffer', 'Array'].indexOf(schematype.instance) === -1) {
+ } else if (Array.isArray(val) && ['Buffer', 'Array'].indexOf(schematype.instance) === -1 && !options.sanitizeFilter) {
const casted = [];
const valuesArray = val;
for (const _val of valuesArray) {
- casted.push(schematype.castForQueryWrapper({
- val: _val,
- context: context
- }));
+ casted.push(schematype.castForQuery(
+ null,
+ _val,
+ context
+ ));
}
obj[path] = { $in: casted };
} else {
- obj[path] = schematype.castForQueryWrapper({
- val: val,
- context: context
- });
+ obj[path] = schematype.castForQuery(
+ null,
+ val,
+ context
+ );
}
}
}
@@ -356,7 +405,7 @@ function _cast(val, numbertype, context) {
if (Array.isArray(item) || isObject(item)) {
return _cast(item, numbertype, context);
}
- val[i] = numbertype.castForQueryWrapper({ val: item, context: context });
+ val[i] = numbertype.castForQuery(null, item, context);
});
} else {
const nearKeys = Object.keys(val);
@@ -378,15 +427,9 @@ function getStrictQuery(queryOptions, schemaUserProvidedOptions, schemaOptions,
if ('strictQuery' in queryOptions) {
return queryOptions.strictQuery;
}
- if ('strict' in queryOptions) {
- return queryOptions.strict;
- }
if ('strictQuery' in schemaUserProvidedOptions) {
return schemaUserProvidedOptions.strictQuery;
}
- if ('strict' in schemaUserProvidedOptions) {
- return schemaUserProvidedOptions.strict;
- }
const mongooseOptions = context &&
context.mongooseCollection &&
context.mongooseCollection.conn &&
@@ -396,9 +439,6 @@ function getStrictQuery(queryOptions, schemaUserProvidedOptions, schemaOptions,
if ('strictQuery' in mongooseOptions) {
return mongooseOptions.strictQuery;
}
- if ('strict' in mongooseOptions) {
- return mongooseOptions.strict;
- }
}
return schemaOptions.strictQuery;
}
diff --git a/lib/cast/bigint.js b/lib/cast/bigint.js
new file mode 100644
index 00000000000..20e01ae58b1
--- /dev/null
+++ b/lib/cast/bigint.js
@@ -0,0 +1,36 @@
+'use strict';
+
+const assert = require('assert');
+const { Long } = require('bson');
+
+/**
+ * Given a value, cast it to a BigInt, or throw an `Error` if the value
+ * cannot be casted. `null` and `undefined` are considered valid.
+ *
+ * @param {Any} value
+ * @return {Number}
+ * @throws {Error} if `value` is not one of the allowed values
+ * @api private
+ */
+
+module.exports = function castBigInt(val) {
+ if (val == null) {
+ return val;
+ }
+ if (val === '') {
+ return null;
+ }
+ if (typeof val === 'bigint') {
+ return val;
+ }
+
+ if (val instanceof Long) {
+ return val.toBigInt();
+ }
+
+ if (typeof val === 'string' || typeof val === 'number') {
+ return BigInt(val);
+ }
+
+ assert.ok(false);
+};
diff --git a/lib/cast/decimal128.js b/lib/cast/decimal128.js
index 2cd9208af93..2bec9975c67 100644
--- a/lib/cast/decimal128.js
+++ b/lib/cast/decimal128.js
@@ -20,7 +20,10 @@ module.exports = function castDecimal128(value) {
return Decimal128Type.fromString(value);
}
- if (Buffer.isBuffer(value)) {
+ if (typeof Buffer === 'function' && Buffer.isBuffer(value)) {
+ return new Decimal128Type(value);
+ }
+ if (typeof Uint8Array === 'function' && value instanceof Uint8Array) {
return new Decimal128Type(value);
}
diff --git a/lib/cast/double.js b/lib/cast/double.js
new file mode 100644
index 00000000000..5dfc6c1a797
--- /dev/null
+++ b/lib/cast/double.js
@@ -0,0 +1,50 @@
+'use strict';
+
+const assert = require('assert');
+const BSON = require('bson');
+const isBsonType = require('../helpers/isBsonType');
+
+/**
+ * Given a value, cast it to a IEEE 754-2008 floating point, or throw an `Error` if the value
+ * cannot be casted. `null`, `undefined`, and `NaN` are considered valid inputs.
+ *
+ * @param {Any} value
+ * @return {Number}
+ * @throws {Error} if `value` does not represent a IEEE 754-2008 floating point. If casting from a string, see [BSON Double.fromString API documentation](https://mongodb.github.io/node-mongodb-native/Next/classes/BSON.Double.html#fromString)
+ * @api private
+ */
+
+module.exports = function castDouble(val) {
+ if (val == null || val === '') {
+ return null;
+ }
+
+ let coercedVal;
+ if (isBsonType(val, 'Long')) {
+ coercedVal = val.toNumber();
+ } else if (typeof val === 'string') {
+ try {
+ coercedVal = BSON.Double.fromString(val);
+ return coercedVal;
+ } catch {
+ assert.ok(false);
+ }
+ } else if (typeof val === 'object') {
+ const tempVal = val.valueOf() ?? val.toString();
+ // ex: { a: 'im an object, valueOf: () => 'helloworld' } // throw an error
+ if (typeof tempVal === 'string') {
+ try {
+ coercedVal = BSON.Double.fromString(val);
+ return coercedVal;
+ } catch {
+ assert.ok(false);
+ }
+ } else {
+ coercedVal = Number(tempVal);
+ }
+ } else {
+ coercedVal = Number(val);
+ }
+
+ return new BSON.Double(coercedVal);
+};
diff --git a/lib/cast/int32.js b/lib/cast/int32.js
new file mode 100644
index 00000000000..34eeae8565f
--- /dev/null
+++ b/lib/cast/int32.js
@@ -0,0 +1,36 @@
+'use strict';
+
+const isBsonType = require('../helpers/isBsonType');
+const assert = require('assert');
+
+/**
+ * Given a value, cast it to a Int32, or throw an `Error` if the value
+ * cannot be casted. `null` and `undefined` are considered valid.
+ *
+ * @param {Any} value
+ * @return {Number}
+ * @throws {Error} if `value` does not represent an integer, or is outside the bounds of an 32-bit integer.
+ * @api private
+ */
+
+module.exports = function castInt32(val) {
+ if (val == null) {
+ return val;
+ }
+ if (val === '') {
+ return null;
+ }
+
+ const coercedVal = isBsonType(val, 'Long') ? val.toNumber() : Number(val);
+
+ const INT32_MAX = 0x7FFFFFFF;
+ const INT32_MIN = -0x80000000;
+
+ if (coercedVal === (coercedVal | 0) &&
+ coercedVal >= INT32_MIN &&
+ coercedVal <= INT32_MAX
+ ) {
+ return coercedVal;
+ }
+ assert.ok(false);
+};
diff --git a/lib/cast/objectid.js b/lib/cast/objectid.js
index 7321c0cccf7..095a0a10e49 100644
--- a/lib/cast/objectid.js
+++ b/lib/cast/objectid.js
@@ -1,19 +1,19 @@
'use strict';
const isBsonType = require('../helpers/isBsonType');
-const ObjectId = require('../driver').get().ObjectId;
+const ObjectId = require('../types/objectid');
module.exports = function castObjectId(value) {
if (value == null) {
return value;
}
- if (isBsonType(value, 'ObjectID')) {
+ if (isBsonType(value, 'ObjectId')) {
return value;
}
if (value._id) {
- if (isBsonType(value._id, 'ObjectID')) {
+ if (isBsonType(value._id, 'ObjectId')) {
return value._id;
}
if (value._id.toString instanceof Function) {
diff --git a/lib/collection.js b/lib/collection.js
index 35633a6e2d3..117a8c69551 100644
--- a/lib/collection.js
+++ b/lib/collection.js
@@ -5,7 +5,7 @@
*/
const EventEmitter = require('events').EventEmitter;
-const STATES = require('./connectionstate');
+const STATES = require('./connectionState');
const immediate = require('./helpers/immediate');
/**
@@ -236,24 +236,40 @@ Collection.prototype.save = function() {
* Abstract method that drivers must implement.
*/
-Collection.prototype.update = function() {
- throw new Error('Collection#update unimplemented by driver');
+Collection.prototype.updateOne = function() {
+ throw new Error('Collection#updateOne unimplemented by driver');
};
/**
* Abstract method that drivers must implement.
*/
-Collection.prototype.getIndexes = function() {
- throw new Error('Collection#getIndexes unimplemented by driver');
+Collection.prototype.updateMany = function() {
+ throw new Error('Collection#updateMany unimplemented by driver');
+};
+
+/**
+ * Abstract method that drivers must implement.
+ */
+
+Collection.prototype.deleteOne = function() {
+ throw new Error('Collection#deleteOne unimplemented by driver');
};
/**
* Abstract method that drivers must implement.
*/
-Collection.prototype.mapReduce = function() {
- throw new Error('Collection#mapReduce unimplemented by driver');
+Collection.prototype.deleteMany = function() {
+ throw new Error('Collection#deleteMany unimplemented by driver');
+};
+
+/**
+ * Abstract method that drivers must implement.
+ */
+
+Collection.prototype.getIndexes = function() {
+ throw new Error('Collection#getIndexes unimplemented by driver');
};
/**
diff --git a/lib/connection.js b/lib/connection.js
index 1b6fcbae40f..dbb10b1ef78 100644
--- a/lib/connection.js
+++ b/lib/connection.js
@@ -4,24 +4,25 @@
* Module dependencies.
*/
-const ChangeStream = require('./cursor/ChangeStream');
+const ChangeStream = require('./cursor/changeStream');
const EventEmitter = require('events').EventEmitter;
const Schema = require('./schema');
-const STATES = require('./connectionstate');
+const STATES = require('./connectionState');
+const MongooseBulkWriteError = require('./error/bulkWriteError');
const MongooseError = require('./error/index');
-const DisconnectedError = require('./error/disconnected');
-const SyncIndexesError = require('./error/syncIndexes');
-const PromiseProvider = require('./promise_provider');
const ServerSelectionError = require('./error/serverSelection');
+const SyncIndexesError = require('./error/syncIndexes');
const applyPlugins = require('./helpers/schema/applyPlugins');
+const clone = require('./helpers/clone');
const driver = require('./driver');
-const promiseOrCallback = require('./helpers/promiseOrCallback');
const get = require('./helpers/get');
+const getDefaultBulkwriteResult = require('./helpers/getDefaultBulkwriteResult');
const immediate = require('./helpers/immediate');
-const mongodb = require('mongodb');
-const pkg = require('../package.json');
const utils = require('./utils');
-const processConnectionOptions = require('./helpers/processConnectionOptions');
+const CreateCollectionsError = require('./error/createCollectionsError');
+const castBulkWrite = require('./helpers/model/castBulkWrite');
+const { modelSymbol } = require('./helpers/symbols');
+const isPromise = require('./helpers/isPromise');
const arrayAtomicsSymbol = require('./helpers/symbols').arrayAtomicsSymbol;
const sessionNewDocuments = require('./helpers/symbols').sessionNewDocuments;
@@ -51,7 +52,8 @@ const noPasswordAuthMechanisms = [
* @event `close`: Emitted after we `disconnected` and `onClose` executed on all of this connection's models.
* @event `reconnected`: Emitted after we `connected` and subsequently `disconnected`, followed by successfully another successful connection.
* @event `error`: Emitted when an error occurs on this connection.
- * @event `fullsetup`: Emitted after the driver has connected to primary and all secondaries if specified in the connection string.
+ * @event `operation-start`: Emitted when a call to the MongoDB Node.js driver, like a `find()` or `insertOne()`, happens on any collection tied to this connection.
+ * @event `operation-end`: Emitted when a call to the MongoDB Node.js driver, like a `find()` or `insertOne()`, either succeeds or errors.
* @api public
*/
@@ -74,6 +76,9 @@ function Connection(base) {
} else {
this.id = base.nextConnectionId;
}
+
+ // Internal queue of objects `{ fn, ctx, args }` that Mongoose calls when this connection is successfully
+ // opened. In `onOpen()`, Mongoose calls every entry in `_queue` and empties the queue.
this._queue = [];
}
@@ -106,6 +111,18 @@ Object.setPrototypeOf(Connection.prototype, EventEmitter.prototype);
Object.defineProperty(Connection.prototype, 'readyState', {
get: function() {
+ // If connection thinks it is connected, but we haven't received a heartbeat in 2 heartbeat intervals,
+ // that likely means the connection is stale (potentially due to frozen AWS Lambda container)
+ if (
+ this._readyState === STATES.connected &&
+ this._lastHeartbeatAt != null &&
+ // LoadBalanced topology (behind haproxy, including Atlas serverless instances) don't use heartbeats,
+ // so we can't use this check in that case.
+ this.client?.topology?.s?.description?.type !== 'LoadBalanced' &&
+ typeof this.client?.topology?.s?.description?.heartbeatFrequencyMS === 'number' &&
+ Date.now() - this._lastHeartbeatAt >= this.client.topology.s.description.heartbeatFrequencyMS * 2) {
+ return STATES.disconnected;
+ }
return this._readyState;
},
set: function(val) {
@@ -141,7 +158,7 @@ Object.defineProperty(Connection.prototype, 'readyState', {
* @api public
*/
-Connection.prototype.get = function(key) {
+Connection.prototype.get = function getOption(key) {
if (this.config.hasOwnProperty(key)) {
return this.config[key];
}
@@ -154,7 +171,7 @@ Connection.prototype.get = function(key) {
*
* Supported options include:
*
- * - `maxTimeMS`: Set [`maxTimeMS`](/docs/api/query.html#query_Query-maxTimeMS) for all queries on this connection.
+ * - `maxTimeMS`: Set [`maxTimeMS`](https://mongoosejs.com/docs/api/query.html#Query.prototype.maxTimeMS()) for all queries on this connection.
* - 'debug': If `true`, prints the operations mongoose sends to MongoDB to the console. If a writable stream is passed, it will log to that stream, without colorization. If a callback function is passed, it will receive the collection name, the method name, then all arugments passed to the method. For example, if you wanted to replicate the default logging, you could output from the callback `Mongoose: ${collectionName}.${methodName}(${methodArgs.join(', ')})`.
*
* #### Example:
@@ -169,7 +186,7 @@ Connection.prototype.get = function(key) {
* @api public
*/
-Connection.prototype.set = function(key, val) {
+Connection.prototype.set = function setOption(key, val) {
if (this.config.hasOwnProperty(key)) {
this.config[key] = val;
return val;
@@ -209,7 +226,7 @@ Connection.prototype.name;
/**
* A [POJO](https://masteringjs.io/tutorials/fundamentals/pojo) containing
* a map from model names to models. Contains all models that have been
- * added to this connection using [`Connection#model()`](/docs/api/connection.html#connection_Connection-model).
+ * added to this connection using [`Connection#model()`](https://mongoosejs.com/docs/api/connection.html#Connection.prototype.model()).
*
* #### Example:
*
@@ -229,7 +246,7 @@ Connection.prototype.models;
/**
* A number identifier for this connection. Used for debugging when
- * you have [multiple connections](/docs/connections.html#multiple_connections).
+ * you have [multiple connections](https://mongoosejs.com/docs/connections.html#multiple_connections).
*
* #### Example:
*
@@ -393,18 +410,249 @@ Connection.prototype.config;
* @method createCollection
* @param {string} collection The collection to create
* @param {Object} [options] see [MongoDB driver docs](https://mongodb.github.io/node-mongodb-native/4.9/classes/Db.html#createCollection)
- * @param {Function} [callback]
- * @return {Promise} Returns a Promise if no `callback` is given.
+ * @return {Promise}
* @api public
*/
-Connection.prototype.createCollection = _wrapConnHelper(function createCollection(collection, options, cb) {
- if (typeof options === 'function') {
- cb = options;
- options = {};
+Connection.prototype.createCollection = async function createCollection(collection, options) {
+ if (typeof options === 'function' || (arguments.length >= 3 && typeof arguments[2] === 'function')) {
+ throw new MongooseError('Connection.prototype.createCollection() no longer accepts a callback');
+ }
+
+ await this._waitForConnect();
+
+ return this.db.createCollection(collection, options);
+};
+
+/**
+ * _Requires MongoDB Server 8.0 or greater_. Executes bulk write operations across multiple models in a single operation.
+ * You must specify the `model` for each operation: Mongoose will use `model` for casting and validation, as well as
+ * determining which collection to apply the operation to.
+ *
+ * #### Example:
+ * const Test = mongoose.model('Test', new Schema({ name: String }));
+ *
+ * await db.bulkWrite([
+ * { model: Test, name: 'insertOne', document: { name: 'test1' } }, // Can specify model as a Model class...
+ * { model: 'Test', name: 'insertOne', document: { name: 'test2' } } // or as a model name
+ * ], { ordered: false });
+ *
+ * @method bulkWrite
+ * @param {Array} ops
+ * @param {Object} [options]
+ * @param {Boolean} [options.ordered] If false, perform unordered operations. If true, perform ordered operations.
+ * @param {Session} [options.session] The session to use for the operation.
+ * @return {Promise}
+ * @see MongoDB https://www.mongodb.com/docs/manual/reference/command/bulkWrite/#mongodb-dbcommand-dbcmd.bulkWrite
+ * @api public
+ */
+
+
+Connection.prototype.bulkWrite = async function bulkWrite(ops, options) {
+ await this._waitForConnect();
+ options = options || {};
+
+ const ordered = options.ordered == null ? true : options.ordered;
+ const asyncLocalStorage = this.base.transactionAsyncLocalStorage?.getStore();
+ if ((!options || !options.hasOwnProperty('session')) && asyncLocalStorage?.session != null) {
+ options = { ...options, session: asyncLocalStorage.session };
+ }
+
+ const now = this.base.now();
+
+ let res = null;
+ if (ordered) {
+ const opsToSend = [];
+ for (const op of ops) {
+ if (typeof op.model !== 'string' && !op.model?.[modelSymbol]) {
+ throw new MongooseError('Must specify model in Connection.prototype.bulkWrite() operations');
+ }
+ const Model = op.model[modelSymbol] ? op.model : this.model(op.model);
+
+ if (op.name == null) {
+ throw new MongooseError('Must specify operation name in Connection.prototype.bulkWrite()');
+ }
+ if (!castBulkWrite.cast.hasOwnProperty(op.name)) {
+ throw new MongooseError(`Unrecognized bulkWrite() operation name ${op.name}`);
+ }
+
+ await castBulkWrite.cast[op.name](Model, op, options, now);
+ opsToSend.push({ ...op, namespace: Model.namespace() });
+ }
+
+ res = await this.client.bulkWrite(opsToSend, options);
+ } else {
+ const validOps = [];
+ const validOpIndexes = [];
+ let validationErrors = [];
+ const asyncValidations = [];
+ const results = [];
+ for (let i = 0; i < ops.length; ++i) {
+ const op = ops[i];
+ if (typeof op.model !== 'string' && !op.model?.[modelSymbol]) {
+ const error = new MongooseError('Must specify model in Connection.prototype.bulkWrite() operations');
+ validationErrors.push({ index: i, error: error });
+ results[i] = error;
+ continue;
+ }
+ let Model;
+ try {
+ Model = op.model[modelSymbol] ? op.model : this.model(op.model);
+ } catch (error) {
+ validationErrors.push({ index: i, error: error });
+ continue;
+ }
+ if (op.name == null) {
+ const error = new MongooseError('Must specify operation name in Connection.prototype.bulkWrite()');
+ validationErrors.push({ index: i, error: error });
+ results[i] = error;
+ continue;
+ }
+ if (!castBulkWrite.cast.hasOwnProperty(op.name)) {
+ const error = new MongooseError(`Unrecognized bulkWrite() operation name ${op.name}`);
+ validationErrors.push({ index: i, error: error });
+ results[i] = error;
+ continue;
+ }
+
+ let maybePromise = null;
+ try {
+ maybePromise = castBulkWrite.cast[op.name](Model, op, options, now);
+ } catch (error) {
+ validationErrors.push({ index: i, error: error });
+ results[i] = error;
+ continue;
+ }
+ if (isPromise(maybePromise)) {
+ asyncValidations.push(
+ maybePromise.then(
+ () => {
+ validOps.push({ ...op, namespace: Model.namespace() });
+ validOpIndexes.push(i);
+ },
+ error => {
+ validationErrors.push({ index: i, error: error });
+ results[i] = error;
+ }
+ )
+ );
+ } else {
+ validOps.push({ ...op, namespace: Model.namespace() });
+ validOpIndexes.push(i);
+ }
+ }
+
+ if (asyncValidations.length > 0) {
+ await Promise.all(asyncValidations);
+ }
+
+ validationErrors = validationErrors.
+ sort((v1, v2) => v1.index - v2.index).
+ map(v => v.error);
+
+ if (validOps.length === 0) {
+ if (options.throwOnValidationError && validationErrors.length) {
+ throw new MongooseBulkWriteError(
+ validationErrors,
+ results,
+ res,
+ 'bulkWrite'
+ );
+ }
+ return getDefaultBulkwriteResult();
+ }
+
+ let error;
+ [res, error] = await this.client.bulkWrite(validOps, options).
+ then(res => ([res, null])).
+ catch(err => ([null, err]));
+
+ if (error) {
+ if (validationErrors.length > 0) {
+ error.mongoose = error.mongoose || {};
+ error.mongoose.validationErrors = validationErrors;
+ }
+ }
+
+ for (let i = 0; i < validOpIndexes.length; ++i) {
+ results[validOpIndexes[i]] = null;
+ }
+ if (validationErrors.length > 0) {
+ if (options.throwOnValidationError) {
+ throw new MongooseBulkWriteError(
+ validationErrors,
+ results,
+ res,
+ 'bulkWrite'
+ );
+ } else {
+ res.mongoose = res.mongoose || {};
+ res.mongoose.validationErrors = validationErrors;
+ res.mongoose.results = results;
+ }
+ }
+ }
+
+ return res;
+};
+
+/**
+ * Calls `createCollection()` on a models in a series.
+ *
+ * @method createCollections
+ * @param {Boolean} continueOnError When true, will continue to create collections and create a new error class for the collections that errored.
+ * @returns {Promise}
+ * @api public
+ */
+
+Connection.prototype.createCollections = async function createCollections(options = {}) {
+ const result = {};
+ const errorsMap = { };
+
+ const { continueOnError } = options;
+ delete options.continueOnError;
+ for (const model of Object.values(this.models)) {
+ try {
+ result[model.modelName] = await model.createCollection({});
+ } catch (err) {
+ if (!continueOnError) {
+ errorsMap[model.modelName] = err;
+ break;
+ } else {
+ result[model.modelName] = err;
+ }
+ }
}
- this.db.createCollection(collection, options, cb);
-});
+
+ if (!continueOnError && Object.keys(errorsMap).length) {
+ const message = Object.entries(errorsMap).map(([modelName, err]) => `${modelName}: ${err.message}`).join(', ');
+ const createCollectionsError = new CreateCollectionsError(message, errorsMap);
+ throw createCollectionsError;
+ }
+ return result;
+};
+
+/**
+ * A convenience wrapper for `connection.client.withSession()`.
+ *
+ * #### Example:
+ *
+ * await conn.withSession(async session => {
+ * const doc = await TestModel.findOne().session(session);
+ * });
+ *
+ * @method withSession
+ * @param {Function} executor called with 1 argument: a `ClientSession` instance
+ * @return {Promise} resolves to the return value of the executor function
+ * @api public
+ */
+
+Connection.prototype.withSession = async function withSession(executor) {
+ if (arguments.length === 0) {
+ throw new Error('Please provide an executor function');
+ }
+ return await this.client.withSession(executor);
+};
/**
* _Requires MongoDB >= 3.6.0._ Starts a [MongoDB session](https://www.mongodb.com/docs/manual/release-notes/3.6/#client-sessions)
@@ -415,7 +663,7 @@ Connection.prototype.createCollection = _wrapConnHelper(function createCollectio
*
* const session = await conn.startSession();
* let doc = await Person.findOne({ name: 'Ned Stark' }, null, { session });
- * await doc.remove();
+ * await doc.deleteOne();
* // `doc` will always be null, even if reading from a replica set
* // secondary. Without causal consistency, it is possible to
* // get a doc back from the below query if the query reads from a
@@ -426,19 +674,20 @@ Connection.prototype.createCollection = _wrapConnHelper(function createCollectio
* @method startSession
* @param {Object} [options] see the [mongodb driver options](https://mongodb.github.io/node-mongodb-native/4.9/classes/MongoClient.html#startSession)
* @param {Boolean} [options.causalConsistency=true] set to false to disable causal consistency
- * @param {Function} [callback]
* @return {Promise} promise that resolves to a MongoDB driver `ClientSession`
* @api public
*/
-Connection.prototype.startSession = _wrapConnHelper(function startSession(options, cb) {
- if (typeof options === 'function') {
- cb = options;
- options = null;
+Connection.prototype.startSession = async function startSession(options) {
+ if (arguments.length >= 2 && typeof arguments[1] === 'function') {
+ throw new MongooseError('Connection.prototype.startSession() no longer accepts a callback');
}
+
+ await this._waitForConnect();
+
const session = this.client.startSession(options);
- cb(null, session);
-});
+ return session;
+};
/**
* _Requires MongoDB >= 3.6.0._ Executes the wrapped async function
@@ -475,63 +724,146 @@ Connection.prototype.startSession = _wrapConnHelper(function startSession(option
Connection.prototype.transaction = function transaction(fn, options) {
return this.startSession().then(session => {
session[sessionNewDocuments] = new Map();
- return session.withTransaction(() => fn(session), options).
+ return session.withTransaction(() => _wrapUserTransaction(fn, session, this.base), options).
then(res => {
delete session[sessionNewDocuments];
return res;
}).
catch(err => {
- // If transaction was aborted, we need to reset newly
- // inserted documents' `isNew`.
- for (const doc of session[sessionNewDocuments].keys()) {
- const state = session[sessionNewDocuments].get(doc);
- if (state.hasOwnProperty('isNew')) {
- doc.$isNew = state.$isNew;
- }
- if (state.hasOwnProperty('versionKey')) {
- doc.set(doc.schema.options.versionKey, state.versionKey);
- }
-
- if (state.modifiedPaths.length > 0 && doc.$__.activePaths.states.modify == null) {
- doc.$__.activePaths.states.modify = {};
- }
- for (const path of state.modifiedPaths) {
- doc.$__.activePaths.paths[path] = 'modify';
- doc.$__.activePaths.states.modify[path] = true;
- }
-
- for (const path of state.atomics.keys()) {
- const val = doc.$__getValue(path);
- if (val == null) {
- continue;
- }
- val[arrayAtomicsSymbol] = state.atomics.get(path);
- }
- }
delete session[sessionNewDocuments];
throw err;
- })
- .finally(() => {
- session.endSession()
- .catch(() => {});
+ }).
+ finally(() => {
+ session.endSession().catch(() => {});
});
});
};
+/*!
+ * Reset document state in between transaction retries re: gh-13698
+ */
+
+async function _wrapUserTransaction(fn, session, mongoose) {
+ try {
+ const res = mongoose.transactionAsyncLocalStorage == null
+ ? await fn(session)
+ : await new Promise(resolve => {
+ mongoose.transactionAsyncLocalStorage.run(
+ { session },
+ () => resolve(fn(session))
+ );
+ });
+ return res;
+ } catch (err) {
+ _resetSessionDocuments(session);
+ throw err;
+ }
+}
+
+/*!
+ * If transaction was aborted, we need to reset newly inserted documents' `isNew`.
+ */
+function _resetSessionDocuments(session) {
+ for (const doc of session[sessionNewDocuments].keys()) {
+ const state = session[sessionNewDocuments].get(doc);
+ if (state.hasOwnProperty('isNew')) {
+ doc.$isNew = state.isNew;
+ }
+ if (state.hasOwnProperty('versionKey')) {
+ doc.set(doc.schema.options.versionKey, state.versionKey);
+ }
+
+ if (state.modifiedPaths.length > 0 && doc.$__.activePaths.states.modify == null) {
+ doc.$__.activePaths.states.modify = {};
+ }
+ for (const path of state.modifiedPaths) {
+ const currentState = doc.$__.activePaths.paths[path];
+ if (currentState != null) {
+ delete doc.$__.activePaths[currentState][path];
+ }
+ doc.$__.activePaths.paths[path] = 'modify';
+ doc.$__.activePaths.states.modify[path] = true;
+ }
+
+ for (const path of state.atomics.keys()) {
+ const val = doc.$__getValue(path);
+ if (val == null) {
+ continue;
+ }
+ val[arrayAtomicsSymbol] = state.atomics.get(path);
+ }
+ }
+}
+
/**
* Helper for `dropCollection()`. Will delete the given collection, including
* all documents and indexes.
*
* @method dropCollection
* @param {string} collection The collection to delete
- * @param {Function} [callback]
- * @return {Promise} Returns a Promise if no `callback` is given.
+ * @return {Promise}
* @api public
*/
-Connection.prototype.dropCollection = _wrapConnHelper(function dropCollection(collection, cb) {
- this.db.dropCollection(collection, cb);
-});
+Connection.prototype.dropCollection = async function dropCollection(collection) {
+ if (arguments.length >= 2 && typeof arguments[1] === 'function') {
+ throw new MongooseError('Connection.prototype.dropCollection() no longer accepts a callback');
+ }
+
+ await this._waitForConnect();
+
+ return this.db.dropCollection(collection);
+};
+
+/**
+ * Waits for connection to be established, so the connection has a `client`
+ *
+ * @return Promise
+ * @api private
+ */
+
+Connection.prototype._waitForConnect = async function _waitForConnect() {
+ if ((this.readyState === STATES.connecting || this.readyState === STATES.disconnected) && this._shouldBufferCommands()) {
+ await new Promise(resolve => {
+ this._queue.push({ fn: resolve });
+ });
+ }
+};
+
+/**
+ * Helper for MongoDB Node driver's `listCollections()`.
+ * Returns an array of collection objects.
+ *
+ * @method listCollections
+ * @return {Promise}
+ * @api public
+ */
+
+Connection.prototype.listCollections = async function listCollections() {
+ await this._waitForConnect();
+
+ const cursor = this.db.listCollections();
+ return await cursor.toArray();
+};
+
+/**
+ * Helper for MongoDB Node driver's `listDatabases()`.
+ * Returns an object with a `databases` property that contains an
+ * array of database objects.
+ *
+ * #### Example:
+ * const { databases } = await mongoose.connection.listDatabases();
+ * databases; // [{ name: 'mongoose_test', sizeOnDisk: 0, empty: false }]
+ *
+ * @method listCollections
+ * @return {Promise<{ databases: Array<{ name: string }> }>}
+ * @api public
+ */
+
+Connection.prototype.listDatabases = async function listDatabases() {
+ // Implemented in `lib/drivers/node-mongodb-native/connection.js`
+ throw new MongooseError('listDatabases() not implemented by driver');
+};
/**
* Helper for `dropDatabase()`. Deletes the given database, including all
@@ -544,12 +876,17 @@ Connection.prototype.dropCollection = _wrapConnHelper(function dropCollection(co
* await conn.dropDatabase();
*
* @method dropDatabase
- * @param {Function} [callback]
- * @return {Promise} Returns a Promise if no `callback` is given.
+ * @return {Promise}
* @api public
*/
-Connection.prototype.dropDatabase = _wrapConnHelper(function dropDatabase(cb) {
+Connection.prototype.dropDatabase = async function dropDatabase() {
+ if (arguments.length >= 1 && typeof arguments[0] === 'function') {
+ throw new MongooseError('Connection.prototype.dropDatabase() no longer accepts a callback');
+ }
+
+ await this._waitForConnect();
+
// 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
@@ -557,38 +894,9 @@ Connection.prototype.dropDatabase = _wrapConnHelper(function dropDatabase(cb) {
for (const model of Object.values(this.models)) {
delete model.$init;
}
- this.db.dropDatabase(cb);
-});
-/*!
- * ignore
- */
-
-function _wrapConnHelper(fn) {
- return function() {
- const cb = arguments.length > 0 ? arguments[arguments.length - 1] : null;
- const argsWithoutCb = typeof cb === 'function' ?
- Array.prototype.slice.call(arguments, 0, arguments.length - 1) :
- Array.prototype.slice.call(arguments);
- const disconnectedError = new DisconnectedError(this.id, fn.name);
-
- return promiseOrCallback(cb, cb => {
- immediate(() => {
- if ((this.readyState === STATES.connecting || this.readyState === STATES.disconnected) && this._shouldBufferCommands()) {
- this._queue.push({ fn: fn, ctx: this, args: argsWithoutCb.concat([cb]) });
- } else if (this.readyState === STATES.disconnected && this.db == null) {
- cb(disconnectedError);
- } else {
- try {
- fn.apply(this, argsWithoutCb.concat([cb]));
- } catch (err) {
- return cb(err);
- }
- }
- });
- });
- };
-}
+ return this.db.dropDatabase();
+};
/*!
* ignore
@@ -617,7 +925,7 @@ Connection.prototype._shouldBufferCommands = function _shouldBufferCommands() {
* @api private
*/
-Connection.prototype.error = function(err, callback) {
+Connection.prototype.error = function error(err, callback) {
if (callback) {
callback(err);
return null;
@@ -631,6 +939,7 @@ Connection.prototype.error = function(err, callback) {
/**
* Called when the connection is opened
*
+ * @emits "open"
* @api private
*/
@@ -669,273 +978,154 @@ Connection.prototype.onOpen = function() {
* @param {Number} [options.heartbeatFrequencyMS] If `useUnifiedTopology = true`, the MongoDB driver sends a heartbeat every `heartbeatFrequencyMS` to check on the status of the connection. A heartbeat is subject to `serverSelectionTimeoutMS`, so the MongoDB driver will retry failed heartbeats for up to 30 seconds by default. Mongoose only emits a `'disconnected'` event after a heartbeat has failed, so you may want to decrease this setting to reduce the time between when your server goes down and when Mongoose emits `'disconnected'`. We recommend you do **not** set this setting below 1000, too many heartbeats can lead to performance degradation.
* @param {Boolean} [options.autoIndex=true] Mongoose-specific option. Set to false to disable automatic index creation for all models associated with this connection.
* @param {Class} [options.promiseLibrary] Sets the [underlying driver's promise library](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/MongoClientOptions.html#promiseLibrary).
- * @param {Number} [options.connectTimeoutMS=30000] How long the MongoDB driver will wait before killing a socket due to inactivity _during initial connection_. Defaults to 30000. This option is passed transparently to [Node.js' `socket#setTimeout()` function](https://nodejs.org/api/net.html#net_socket_settimeout_timeout_callback).
- * @param {Number} [options.socketTimeoutMS=30000] How long the MongoDB driver will wait before killing a socket due to inactivity _after initial connection_. A socket may be inactive because of either no activity or a long-running operation. This is set to `30000` by default, you should set this to 2-3x your longest running operation if you expect some of your database operations to run longer than 20 seconds. This option is passed to [Node.js `socket#setTimeout()` function](https://nodejs.org/api/net.html#net_socket_settimeout_timeout_callback) after the MongoDB driver successfully completes.
+ * @param {Number} [options.socketTimeoutMS=0] How long the MongoDB driver will wait before killing a socket due to inactivity _after initial connection_. A socket may be inactive because of either no activity or a long-running operation. `socketTimeoutMS` defaults to 0, which means Node.js will not time out the socket due to inactivity. This option is passed to [Node.js `socket#setTimeout()` function](https://nodejs.org/api/net.html#net_socket_settimeout_timeout_callback) after the MongoDB driver successfully completes.
* @param {Number} [options.family=0] Passed transparently to [Node.js' `dns.lookup()`](https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback) function. May be either `0, `4`, or `6`. `4` means use IPv4 only, `6` means use IPv6 only, `0` means try both.
* @param {Boolean} [options.autoCreate=false] Set to `true` to make Mongoose automatically call `createCollection()` on every model created on this connection.
- * @param {Function} [callback]
- * @returns {Promise} Returns a Promise if no `callback` is given.
+ * @returns {Promise}
* @api public
*/
-Connection.prototype.openUri = function(uri, options, callback) {
- if (typeof options === 'function') {
- callback = options;
- options = null;
- }
-
- if (['string', 'number'].indexOf(typeof options) !== -1) {
- throw new MongooseError('Mongoose 5.x no longer supports ' +
- '`mongoose.connect(host, dbname, port)` or ' +
- '`mongoose.createConnection(host, dbname, port)`. See ' +
- 'https://mongoosejs.com/docs/connections.html for supported connection syntax');
- }
-
- if (typeof uri !== 'string') {
- throw new MongooseError('The `uri` parameter to `openUri()` must be a ' +
- `string, got "${typeof uri}". Make sure the first parameter to ` +
- '`mongoose.connect()` or `mongoose.createConnection()` is a string.');
- }
-
- if (callback != null && typeof callback !== 'function') {
- throw new MongooseError('3rd parameter to `mongoose.connect()` or ' +
- '`mongoose.createConnection()` must be a function, got "' +
- typeof callback + '"');
- }
-
- if (this._destroyCalled) {
- const error = 'Connection has been closed and destroyed, and cannot be used for re-opening the connection. ' +
- 'Please create a new connection with `mongoose.createConnection()` or `mongoose.connect()`.';
- if (typeof callback === 'function') {
- callback(error);
- return;
- }
- else {
- throw new MongooseError(error);
- }
- }
-
+Connection.prototype.openUri = async function openUri(uri, options) {
if (this.readyState === STATES.connecting || this.readyState === STATES.connected) {
- if (this._connectionString !== uri) {
- throw new MongooseError('Can\'t call `openUri()` on an active connection with ' +
- 'different connection strings. Make sure you aren\'t calling `mongoose.connect()` ' +
- 'multiple times. See: https://mongoosejs.com/docs/connections.html#multiple_connections');
- }
-
- if (typeof callback === 'function') {
- this.$initialConnection = this.$initialConnection.then(
- () => callback(null, this),
- err => callback(err)
- );
+ if (this._connectionString === uri) {
+ return this;
}
- return this;
}
- this._connectionString = uri;
- this.readyState = STATES.connecting;
this._closeCalled = false;
- const Promise = PromiseProvider.get();
- const _this = this;
-
- options = processConnectionOptions(uri, options);
-
- if (options) {
- options = utils.clone(options);
-
- const autoIndex = options.config && options.config.autoIndex != null ?
- options.config.autoIndex :
- options.autoIndex;
- if (autoIndex != null) {
- this.config.autoIndex = autoIndex !== false;
- delete options.config;
- delete options.autoIndex;
- }
-
- if ('autoCreate' in options) {
- this.config.autoCreate = !!options.autoCreate;
- delete options.autoCreate;
- }
-
- if ('sanitizeFilter' in options) {
- this.config.sanitizeFilter = options.sanitizeFilter;
- delete options.sanitizeFilter;
- }
-
- // Backwards compat
- if (options.user || options.pass) {
- options.auth = options.auth || {};
- options.auth.username = options.user;
- options.auth.password = options.pass;
-
- this.user = options.user;
- this.pass = options.pass;
- }
- delete options.user;
- delete options.pass;
-
- if (options.bufferCommands != null) {
- this.config.bufferCommands = options.bufferCommands;
- delete options.bufferCommands;
- }
- } else {
- options = {};
- }
-
- this._connectionOptions = options;
- const dbName = options.dbName;
- if (dbName != null) {
- this.$dbName = dbName;
- }
- delete options.dbName;
-
- if (!utils.hasUserDefinedProperty(options, 'driverInfo')) {
- options.driverInfo = {
- name: 'Mongoose',
- version: pkg.version
- };
+ // Internal option to skip `await this.$initialConnection` in
+ // this function for `createConnection()`. Because otherwise
+ // `createConnection()` would have an uncatchable error.
+ let _fireAndForget = false;
+ if (options && '_fireAndForget' in options) {
+ _fireAndForget = options._fireAndForget;
+ delete options._fireAndForget;
}
- const promise = new Promise((resolve, reject) => {
- let client;
- try {
- client = new mongodb.MongoClient(uri, options);
- } catch (error) {
- _this.readyState = STATES.disconnected;
- return reject(error);
+ try {
+ _validateArgs.apply(arguments);
+ } catch (err) {
+ if (_fireAndForget) {
+ throw err;
}
- _this.client = client;
-
- client.setMaxListeners(0);
- client.connect((error) => {
- if (error) {
- return reject(error);
- }
-
- _setClient(_this, client, options, dbName);
-
- for (const db of this.otherDbs) {
- _setClient(db, client, {}, db.name);
- }
-
- resolve(_this);
- });
- });
+ this.$initialConnection = Promise.reject(err);
+ throw err;
+ }
- const serverSelectionError = new ServerSelectionError();
- this.$initialConnection = promise.
+ this.$initialConnection = this.createClient(uri, options).
then(() => this).
catch(err => {
this.readyState = STATES.disconnected;
- if (err != null && err.name === 'MongoServerSelectionError') {
- err = serverSelectionError.assimilateError(err);
- }
-
if (this.listeners('error').length > 0) {
immediate(() => this.emit('error', err));
}
throw err;
});
- if (callback != null) {
- this.$initialConnection = this.$initialConnection.then(
- () => { callback(null, this); return this; },
- err => callback(err)
- );
- }
-
for (const model of Object.values(this.models)) {
// Errors handled internally, so safe to ignore error
- model.init(function $modelInitNoop() {});
+ model.init().catch(function $modelInitNoop() {});
+ }
+
+ // `createConnection()` calls this `openUri()` function without
+ // awaiting on the result, so we set this option to rely on
+ // `asPromise()` to handle any errors.
+ if (_fireAndForget) {
+ return this;
}
- return this.$initialConnection;
+ try {
+ await this.$initialConnection;
+ } catch (err) {
+ throw _handleConnectionErrors(err);
+ }
+
+ return this;
};
-/*!
- * ignore
+/**
+ * Listen to events in the Connection
+ *
+ * @param {String} event The event to listen on
+ * @param {Function} callback
+ * @see Connection#readyState https://mongoosejs.com/docs/api/connection.html#Connection.prototype.readyState
+ *
+ * @method on
+ * @instance
+ * @memberOf Connection
+ * @api public
*/
-function _setClient(conn, client, options, dbName) {
- const db = dbName != null ? client.db(dbName) : client.db();
- conn.db = db;
- conn.client = client;
- conn.host = client &&
- client.s &&
- client.s.options &&
- client.s.options.hosts &&
- client.s.options.hosts[0] &&
- client.s.options.hosts[0].host || void 0;
- conn.port = client &&
- client.s &&
- client.s.options &&
- client.s.options.hosts &&
- client.s.options.hosts[0] &&
- client.s.options.hosts[0].port || void 0;
- conn.name = dbName != null ? dbName : client && client.s && client.s.options && client.s.options.dbName || void 0;
- conn._closeCalled = client._closeCalled;
-
- const _handleReconnect = () => {
- // If we aren't disconnected, we assume this reconnect is due to a
- // socket timeout. If there's no activity on a socket for
- // `socketTimeoutMS`, the driver will attempt to reconnect and emit
- // this event.
- if (conn.readyState !== STATES.connected) {
- conn.readyState = STATES.connected;
- conn.emit('reconnect');
- conn.emit('reconnected');
- conn.onOpen();
- }
- };
+// Treat `on('error')` handlers as handling the initialConnection promise
+// to avoid uncaught exceptions when using `on('error')`. See gh-14377.
+Connection.prototype.on = function on(event, callback) {
+ if (event === 'error' && this.$initialConnection) {
+ this.$initialConnection.catch(() => {});
+ }
+ return EventEmitter.prototype.on.call(this, event, callback);
+};
- const type = client &&
- client.topology &&
- client.topology.description &&
- client.topology.description.type || '';
+/**
+ * Listen to a event once in the Connection
+ *
+ * @param {String} event The event to listen on
+ * @param {Function} callback
+ * @see Connection#readyState https://mongoosejs.com/docs/api/connection.html#Connection.prototype.readyState
+ *
+ * @method once
+ * @instance
+ * @memberOf Connection
+ * @api public
+ */
- if (type === 'Single') {
- client.on('serverDescriptionChanged', ev => {
- const newDescription = ev.newDescription;
- if (newDescription.type === 'Unknown') {
- conn.readyState = STATES.disconnected;
- } else {
- _handleReconnect();
- }
- });
- } else if (type.startsWith('ReplicaSet')) {
- client.on('topologyDescriptionChanged', ev => {
- // Emit disconnected if we've lost connectivity to the primary
- const description = ev.newDescription;
- if (conn.readyState === STATES.connected && description.type !== 'ReplicaSetWithPrimary') {
- // Implicitly emits 'disconnected'
- conn.readyState = STATES.disconnected;
- } else if (conn.readyState === STATES.disconnected && description.type === 'ReplicaSetWithPrimary') {
- _handleReconnect();
- }
- });
+// Treat `on('error')` handlers as handling the initialConnection promise
+// to avoid uncaught exceptions when using `on('error')`. See gh-14377.
+Connection.prototype.once = function on(event, callback) {
+ if (event === 'error' && this.$initialConnection) {
+ this.$initialConnection.catch(() => {});
+ }
+ return EventEmitter.prototype.once.call(this, event, callback);
+};
+
+/*!
+ * ignore
+ */
+
+function _validateArgs(uri, options, callback) {
+ if (typeof options === 'function' && callback == null) {
+ throw new MongooseError('Connection.prototype.openUri() no longer accepts a callback');
+ } else if (typeof callback === 'function') {
+ throw new MongooseError('Connection.prototype.openUri() no longer accepts a callback');
}
+}
- conn.onOpen();
+/*!
+ * ignore
+ */
- for (const i in conn.collections) {
- if (utils.object.hasOwnProperty(conn.collections, i)) {
- conn.collections[i].onOpen();
- }
+function _handleConnectionErrors(err) {
+ if (err?.name === 'MongoServerSelectionError') {
+ const originalError = err;
+ err = new ServerSelectionError();
+ err.assimilateError(originalError);
}
+
+ return err;
}
/**
- * Destory the connection (not just a alias of [`.close`](#connection_Connection-close))
+ * Destroy the connection. Similar to [`.close`](https://mongoosejs.com/docs/api/connection.html#Connection.prototype.close()),
+ * but also removes the connection from Mongoose's `connections` list and prevents the
+ * connection from ever being re-opened.
*
* @param {Boolean} [force]
- * @param {Function} [callback]
- * @returns {Promise} Returns a Promise if no `callback` is given.
+ * @returns {Promise}
*/
-Connection.prototype.destroy = function(force, callback) {
- if (typeof force === 'function') {
- callback = force;
- force = false;
+Connection.prototype.destroy = async function destroy(force) {
+ if (typeof force === 'function' || (arguments.length === 2 && typeof arguments[1] === 'function')) {
+ throw new MongooseError('Connection.prototype.destroy() no longer accepts a callback');
}
if (force != null && typeof force === 'object') {
@@ -944,24 +1134,20 @@ Connection.prototype.destroy = function(force, callback) {
this.$wasForceClosed = !!force;
}
- return promiseOrCallback(callback, cb => {
- this._close(force, true, cb);
- });
+ return this._close(force, true);
};
/**
* Closes the connection
*
* @param {Boolean} [force] optional
- * @param {Function} [callback] optional
- * @return {Promise} Returns a Promise if no `callback` is given.
+ * @return {Promise}
* @api public
*/
-Connection.prototype.close = function(force, callback) {
- if (typeof force === 'function') {
- callback = force;
- force = false;
+Connection.prototype.close = async function close(force) {
+ if (typeof force === 'function' || (arguments.length === 2 && typeof arguments[1] === 'function')) {
+ throw new MongooseError('Connection.prototype.close() no longer accepts a callback');
}
if (force != null && typeof force === 'object') {
@@ -977,9 +1163,7 @@ Connection.prototype.close = function(force, callback) {
delete model.$init;
}
- return promiseOrCallback(callback, cb => {
- this._close(force, false, cb);
- });
+ return this._close(force, false);
};
/**
@@ -987,11 +1171,10 @@ Connection.prototype.close = function(force, callback) {
*
* @param {Boolean} force
* @param {Boolean} destroy
- * @param {Function} [callback]
* @returns {Connection} this
* @api private
*/
-Connection.prototype._close = function(force, destroy, callback) {
+Connection.prototype._close = async function _close(force, destroy) {
const _this = this;
const closeCalled = this._closeCalled;
this._closeCalled = true;
@@ -1007,47 +1190,46 @@ Connection.prototype._close = function(force, destroy, callback) {
if (destroy && this.base.connections.indexOf(conn) !== -1) {
this.base.connections.splice(this.base.connections.indexOf(conn), 1);
}
- if (closeCalled) {
- callback();
- } else {
- this.doClose(force, function(err) {
- if (err) {
- return callback(err);
- }
- _this.onClose(force);
- callback(null);
- });
+ if (!closeCalled) {
+ await this.doClose(force);
+ this.onClose(force);
}
break;
case STATES.connected:
this.readyState = STATES.disconnecting;
- this.doClose(force, function(err) {
- if (err) {
- return callback(err);
- }
- if (destroy && _this.base.connections.indexOf(conn) !== -1) {
- _this.base.connections.splice(_this.base.connections.indexOf(conn), 1);
- }
- _this.onClose(force);
- callback(null);
- });
+ await this.doClose(force);
+ if (destroy && _this.base.connections.indexOf(conn) !== -1) {
+ this.base.connections.splice(this.base.connections.indexOf(conn), 1);
+ }
+ this.onClose(force);
break;
case STATES.connecting:
- this.once('open', function() {
- destroy ? _this.destroy(force, callback) : _this.close(force, callback);
+ return new Promise((resolve, reject) => {
+ const _rerunClose = () => {
+ this.removeListener('open', _rerunClose);
+ this.removeListener('error', _rerunClose);
+ if (destroy) {
+ this.destroy(force).then(resolve, reject);
+ } else {
+ this.close(force).then(resolve, reject);
+ }
+ };
+
+ this.once('open', _rerunClose);
+ this.once('error', _rerunClose);
});
- break;
case STATES.disconnecting:
- this.once('close', function() {
- if (destroy && _this.base.connections.indexOf(conn) !== -1) {
- _this.base.connections.splice(_this.base.connections.indexOf(conn), 1);
- }
- callback();
+ return new Promise(resolve => {
+ this.once('close', () => {
+ if (destroy && this.base.connections.indexOf(conn) !== -1) {
+ this.base.connections.splice(this.base.connections.indexOf(conn), 1);
+ }
+ resolve();
+ });
});
- break;
}
return this;
@@ -1059,17 +1241,18 @@ Connection.prototype._close = function(force, destroy, callback) {
* @api private
*/
-Connection.prototype.doClose = function() {
+Connection.prototype.doClose = function doClose() {
throw new Error('Connection#doClose unimplemented by driver');
};
/**
* Called when the connection closes
*
+ * @emits "close"
* @api private
*/
-Connection.prototype.onClose = function(force) {
+Connection.prototype.onClose = function onClose(force) {
this.readyState = STATES.disconnected;
// avoid having the collection subscribe to our event emitter
@@ -1088,9 +1271,10 @@ Connection.prototype.onClose = function(force) {
};
/**
- * Retrieves a collection, creating it if not cached.
- *
- * Not typically needed by applications. Just talk to your collection through your model.
+ * Retrieves a raw collection instance, creating it if not cached.
+ * This method returns a thin wrapper around a [MongoDB Node.js driver collection]([MongoDB Node.js driver collection](https://mongodb.github.io/node-mongodb-native/Next/classes/Collection.html)).
+ * Using a Collection bypasses Mongoose middleware, validation, and casting,
+ * letting you use [MongoDB Node.js driver](https://mongodb.github.io/node-mongodb-native/) functionality directly.
*
* @param {String} name of the collection
* @param {Object} [options] optional collection options
@@ -1101,9 +1285,10 @@ Connection.prototype.onClose = function(force) {
Connection.prototype.collection = function(name, options) {
const defaultOptions = {
autoIndex: this.config.autoIndex != null ? this.config.autoIndex : this.base.options.autoIndex,
- autoCreate: this.config.autoCreate != null ? this.config.autoCreate : this.base.options.autoCreate
+ autoCreate: this.config.autoCreate != null ? this.config.autoCreate : this.base.options.autoCreate,
+ autoSearchIndex: this.config.autoSearchIndex != null ? this.config.autoSearchIndex : this.base.options.autoSearchIndex
};
- options = Object.assign({}, defaultOptions, options ? utils.clone(options) : {});
+ options = Object.assign({}, defaultOptions, options ? clone(options) : {});
options.$wasForceClosed = this.$wasForceClosed;
const Collection = this.base && this.base.__driver && this.base.__driver.Collection || driver.get().Collection;
if (!(name in this.collections)) {
@@ -1128,7 +1313,7 @@ Connection.prototype.collection = function(name, options) {
* @param {Function} fn plugin callback
* @param {Object} [opts] optional options
* @return {Connection} this
- * @see plugins /docs/plugins.html
+ * @see plugins https://mongoosejs.com/docs/plugins.html
* @api public
*/
@@ -1146,7 +1331,7 @@ Connection.prototype.plugin = function(fn, opts) {
* const Ticket = db.model('Ticket', new Schema(..));
* const Venue = db.model('Venue');
*
- * _When no `collection` argument is passed, Mongoose produces a collection name by passing the model `name` to the [utils.toCollectionName](#utils_exports-toCollectionName) method. This method pluralizes the name. If you don't like this behavior, either pass a collection name or set your schemas collection name option._
+ * _When no `collection` argument is passed, Mongoose produces a collection name by passing the model `name` to the `utils.toCollectionName` method. This method pluralizes the name. If you don't like this behavior, either pass a collection name or set your schemas collection name option._
*
* #### Example:
*
@@ -1166,12 +1351,12 @@ Connection.prototype.plugin = function(fn, opts) {
* @param {String} [collection] name of mongodb collection (optional) if not given it will be induced from model name
* @param {Object} [options]
* @param {Boolean} [options.overwriteModels=false] If true, overwrite existing models with the same name to avoid `OverwriteModelError`
- * @see Mongoose#model #index_Mongoose-model
+ * @see Mongoose#model https://mongoosejs.com/docs/api/mongoose.html#Mongoose.prototype.model()
* @return {Model} The compiled model
* @api public
*/
-Connection.prototype.model = function(name, schema, collection, options) {
+Connection.prototype.model = function model(name, schema, collection, options) {
if (!(this instanceof Connection)) {
throw new MongooseError('`connection.model()` should not be run with ' +
'`new`. If you are doing `new db.model(foo)(bar)`, use ' +
@@ -1227,7 +1412,7 @@ Connection.prototype.model = function(name, schema, collection, options) {
}
// Errors handled internally, so safe to ignore error
- model.init(function $modelInitNoop() {});
+ model.init().catch(function $modelInitNoop() {});
return model;
}
@@ -1291,7 +1476,7 @@ Connection.prototype.model = function(name, schema, collection, options) {
* @return {Connection} this
*/
-Connection.prototype.deleteModel = function(name) {
+Connection.prototype.deleteModel = function deleteModel(name) {
if (typeof name === 'string') {
const model = this.model(name);
if (model == null) {
@@ -1320,7 +1505,7 @@ Connection.prototype.deleteModel = function(name) {
/**
* Watches the entire underlying database for changes. Similar to
- * [`Model.watch()`](/docs/api/model.html#model_Model-watch).
+ * [`Model.watch()`](https://mongoosejs.com/docs/api/model.html#Model.watch()).
*
* This function does **not** trigger any middleware. In particular, it
* does **not** trigger aggregate middleware.
@@ -1347,9 +1532,7 @@ Connection.prototype.deleteModel = function(name) {
* @return {ChangeStream} mongoose-specific change stream wrapper, inherits from EventEmitter
*/
-Connection.prototype.watch = function(pipeline, options) {
- const disconnectedError = new DisconnectedError(this.id, 'watch');
-
+Connection.prototype.watch = function watch(pipeline, options) {
const changeStreamThunk = cb => {
immediate(() => {
if (this.readyState === STATES.connecting) {
@@ -1357,8 +1540,6 @@ Connection.prototype.watch = function(pipeline, options) {
const driverChangeStream = this.db.watch(pipeline, options);
cb(null, driverChangeStream);
});
- } else if (this.readyState === STATES.disconnected && this.db == null) {
- cb(disconnectedError);
} else {
const driverChangeStream = this.db.watch(pipeline, options);
cb(null, driverChangeStream);
@@ -1385,8 +1566,13 @@ Connection.prototype.watch = function(pipeline, options) {
* @return {Promise}
*/
-Connection.prototype.asPromise = function asPromise() {
- return this.$initialConnection;
+Connection.prototype.asPromise = async function asPromise() {
+ try {
+ await this.$initialConnection;
+ return this;
+ } catch (err) {
+ throw _handleConnectionErrors(err);
+ }
};
/**
@@ -1395,7 +1581,7 @@ Connection.prototype.asPromise = function asPromise() {
* @return {String[]}
*/
-Connection.prototype.modelNames = function() {
+Connection.prototype.modelNames = function modelNames() {
return Object.keys(this.models);
};
@@ -1407,7 +1593,7 @@ Connection.prototype.modelNames = function() {
* @api private
* @return {Boolean} true if the connection should be authenticated after it is opened, otherwise false.
*/
-Connection.prototype.shouldAuthenticate = function() {
+Connection.prototype.shouldAuthenticate = function shouldAuthenticate() {
return this.user != null &&
(this.pass != null || this.authMechanismDoesNotRequirePassword());
};
@@ -1420,7 +1606,7 @@ Connection.prototype.shouldAuthenticate = function() {
* @return {Boolean} true if the authentication mechanism specified in the options object requires
* a password, otherwise false.
*/
-Connection.prototype.authMechanismDoesNotRequirePassword = function() {
+Connection.prototype.authMechanismDoesNotRequirePassword = function authMechanismDoesNotRequirePassword() {
if (this.options && this.options.auth) {
return noPasswordAuthMechanisms.indexOf(this.options.auth.authMechanism) >= 0;
}
@@ -1438,7 +1624,7 @@ Connection.prototype.authMechanismDoesNotRequirePassword = function() {
* @return {Boolean} true if the provided options object provides enough data to authenticate with,
* otherwise false.
*/
-Connection.prototype.optionsProvideAuthenticationData = function(options) {
+Connection.prototype.optionsProvideAuthenticationData = function optionsProvideAuthenticationData(options) {
return (options) &&
(options.user) &&
((options.pass) || this.authMechanismDoesNotRequirePassword());
@@ -1482,26 +1668,16 @@ Connection.prototype.getClient = function getClient() {
* @return {Connection} this
*/
-Connection.prototype.setClient = function setClient(client) {
- if (!(client instanceof mongodb.MongoClient)) {
- throw new MongooseError('Must call `setClient()` with an instance of MongoClient');
- }
- if (this.readyState !== STATES.disconnected) {
- throw new MongooseError('Cannot call `setClient()` on a connection that is already connected.');
- }
- if (client.topology == null) {
- throw new MongooseError('Cannot call `setClient()` with a MongoClient that you have not called `connect()` on yet.');
- }
-
- this._connectionString = client.s.url;
- _setClient(this, client, {}, client.s.options.dbName);
+Connection.prototype.setClient = function setClient() {
+ throw new MongooseError('Connection#setClient not implemented by driver');
+};
- for (const model of Object.values(this.models)) {
- // Errors handled internally, so safe to ignore error
- model.init(function $modelInitNoop() {});
- }
+/*!
+ * Called internally by `openUri()` to create a MongoClient instance.
+ */
- return this;
+Connection.prototype.createClient = function createClient() {
+ throw new MongooseError('Connection#createClient not implemented by driver');
};
/**
@@ -1541,7 +1717,7 @@ Connection.prototype.syncIndexes = async function syncIndexes(options = {}) {
};
/**
- * Switches to a different database using the same connection pool.
+ * Switches to a different database using the same [connection pool](https://mongoosejs.com/docs/api/connectionshtml#connection_pools).
*
* Returns a new connection object, with the new db.
*
@@ -1566,6 +1742,29 @@ Connection.prototype.syncIndexes = async function syncIndexes(options = {}) {
* @api public
*/
+/**
+ * Removes the database connection with the given name created with with `useDb()`.
+ *
+ * Throws an error if the database connection was not found.
+ *
+ * #### Example:
+ *
+ * // Connect to `initialdb` first
+ * const conn = await mongoose.createConnection('mongodb://127.0.0.1:27017/initialdb').asPromise();
+ *
+ * // Creates an un-cached connection to `mydb`
+ * const db = conn.useDb('mydb');
+ *
+ * // Closes `db`, and removes `db` from `conn.relatedDbs` and `conn.otherDbs`
+ * await conn.removeDb('mydb');
+ *
+ * @method removeDb
+ * @memberOf Connection
+ * @param {String} name The database name
+ * @return {Connection} this
+ * @api public
+ */
+
/*!
* Module exports.
*/
diff --git a/lib/connectionstate.js b/lib/connectionState.js
similarity index 100%
rename from lib/connectionstate.js
rename to lib/connectionState.js
diff --git a/lib/constants.js b/lib/constants.js
new file mode 100644
index 00000000000..3a03bd502fc
--- /dev/null
+++ b/lib/constants.js
@@ -0,0 +1,73 @@
+'use strict';
+
+/*!
+ * ignore
+ */
+
+const queryOperations = Object.freeze([
+ // Read
+ 'countDocuments',
+ 'distinct',
+ 'estimatedDocumentCount',
+ 'find',
+ 'findOne',
+ // Update
+ 'findOneAndReplace',
+ 'findOneAndUpdate',
+ 'replaceOne',
+ 'updateMany',
+ 'updateOne',
+ // Delete
+ 'deleteMany',
+ 'deleteOne',
+ 'findOneAndDelete'
+]);
+
+exports.queryOperations = queryOperations;
+
+/*!
+ * ignore
+ */
+
+const queryMiddlewareFunctions = queryOperations.concat([
+ 'validate'
+]);
+
+exports.queryMiddlewareFunctions = queryMiddlewareFunctions;
+
+/*!
+ * ignore
+ */
+
+const aggregateMiddlewareFunctions = [
+ 'aggregate'
+];
+
+exports.aggregateMiddlewareFunctions = aggregateMiddlewareFunctions;
+
+/*!
+ * ignore
+ */
+
+const modelMiddlewareFunctions = [
+ 'bulkWrite',
+ 'createCollection',
+ 'insertMany'
+];
+
+exports.modelMiddlewareFunctions = modelMiddlewareFunctions;
+
+/*!
+ * ignore
+ */
+
+const documentMiddlewareFunctions = [
+ 'validate',
+ 'save',
+ 'remove',
+ 'updateOne',
+ 'deleteOne',
+ 'init'
+];
+
+exports.documentMiddlewareFunctions = documentMiddlewareFunctions;
diff --git a/lib/cursor/ChangeStream.js b/lib/cursor/ChangeStream.js
deleted file mode 100644
index 72b844ac67d..00000000000
--- a/lib/cursor/ChangeStream.js
+++ /dev/null
@@ -1,151 +0,0 @@
-'use strict';
-
-/*!
- * Module dependencies.
- */
-
-const EventEmitter = require('events').EventEmitter;
-
-/*!
- * ignore
- */
-
-class ChangeStream extends EventEmitter {
- constructor(changeStreamThunk, pipeline, options) {
- super();
-
- this.driverChangeStream = null;
- this.closed = false;
- this.bindedEvents = false;
- this.pipeline = pipeline;
- this.options = options;
-
- if (options && options.hydrate && !options.model) {
- throw new Error(
- 'Cannot create change stream with `hydrate: true` ' +
- 'unless calling `Model.watch()`'
- );
- }
-
- // This wrapper is necessary because of buffering.
- changeStreamThunk((err, driverChangeStream) => {
- if (err != null) {
- this.emit('error', err);
- return;
- }
-
- this.driverChangeStream = driverChangeStream;
- this.emit('ready');
- });
- }
-
- _bindEvents() {
- if (this.bindedEvents) {
- return;
- }
-
- this.bindedEvents = true;
-
- if (this.driverChangeStream == null) {
- this.once('ready', () => {
- this.driverChangeStream.on('close', () => {
- this.closed = true;
- });
-
- ['close', 'change', 'end', 'error'].forEach(ev => {
- this.driverChangeStream.on(ev, data => {
- // Sometimes Node driver still polls after close, so
- // avoid any uncaught exceptions due to closed change streams
- // See tests for gh-7022
- if (ev === 'error' && this.closed) {
- return;
- }
- if (data != null && data.fullDocument != null && this.options && this.options.hydrate) {
- data.fullDocument = this.options.model.hydrate(data.fullDocument);
- }
- this.emit(ev, data);
- });
- });
- });
-
- return;
- }
-
- this.driverChangeStream.on('close', () => {
- this.closed = true;
- });
-
- ['close', 'change', 'end', 'error'].forEach(ev => {
- this.driverChangeStream.on(ev, data => {
- // Sometimes Node driver still polls after close, so
- // avoid any uncaught exceptions due to closed change streams
- // See tests for gh-7022
- if (ev === 'error' && this.closed) {
- return;
- }
- this.emit(ev, data);
- });
- });
- }
-
- hasNext(cb) {
- return this.driverChangeStream.hasNext(cb);
- }
-
- next(cb) {
- if (this.options && this.options.hydrate) {
- if (cb != null) {
- const originalCb = cb;
- cb = (err, data) => {
- if (err != null) {
- return originalCb(err);
- }
- if (data.fullDocument != null) {
- data.fullDocument = this.options.model.hydrate(data.fullDocument);
- }
- return originalCb(null, data);
- };
- }
-
- let maybePromise = this.driverChangeStream.next(cb);
- if (maybePromise && typeof maybePromise.then === 'function') {
- maybePromise = maybePromise.then(data => {
- if (data.fullDocument != null) {
- data.fullDocument = this.options.model.hydrate(data.fullDocument);
- }
- return data;
- });
- }
- return maybePromise;
- }
-
- return this.driverChangeStream.next(cb);
- }
-
- on(event, handler) {
- this._bindEvents();
- return super.on(event, handler);
- }
-
- once(event, handler) {
- this._bindEvents();
- return super.once(event, handler);
- }
-
- _queue(cb) {
- this.once('ready', () => cb());
- }
-
- close() {
- this.closed = true;
- if (this.driverChangeStream) {
- this.driverChangeStream.close();
- }
- }
-}
-
-/*!
- * ignore
- */
-
-module.exports = ChangeStream;
diff --git a/lib/cursor/AggregationCursor.js b/lib/cursor/aggregationCursor.js
similarity index 77%
rename from lib/cursor/AggregationCursor.js
rename to lib/cursor/aggregationCursor.js
index 05b68b62ab1..2cff8bb8e1a 100644
--- a/lib/cursor/AggregationCursor.js
+++ b/lib/cursor/aggregationCursor.js
@@ -6,7 +6,6 @@
const MongooseError = require('../error/mongooseError');
const Readable = require('stream').Readable;
-const promiseOrCallback = require('../helpers/promiseOrCallback');
const eachAsync = require('../helpers/cursor/eachAsync');
const immediate = require('../helpers/immediate');
const util = require('util');
@@ -23,7 +22,7 @@ const util = require('util');
* but **not** the model's post aggregate hooks.
*
* Unless you're an advanced user, do **not** instantiate this class directly.
- * Use [`Aggregate#cursor()`](/docs/api/aggregate.html#aggregate_Aggregate-cursor) instead.
+ * Use [`Aggregate#cursor()`](https://mongoosejs.com/docs/api/aggregate.html#Aggregate.prototype.cursor()) instead.
*
* @param {Aggregate} agg
* @inherits Readable https://nodejs.org/api/stream.html#class-streamreadable
@@ -58,12 +57,20 @@ util.inherits(AggregationCursor, Readable);
function _init(model, c, agg) {
if (!model.collection.buffer) {
model.hooks.execPre('aggregate', agg, function() {
+ if (typeof agg.options?.cursor?.transform === 'function') {
+ c._transforms.push(agg.options.cursor.transform);
+ }
+
c.cursor = model.collection.aggregate(agg._pipeline, agg.options || {});
c.emit('cursor', c.cursor);
});
} else {
model.collection.emitter.once('queue', function() {
model.hooks.execPre('aggregate', agg, function() {
+ if (typeof agg.options?.cursor?.transform === 'function') {
+ c._transforms.push(agg.options.cursor.transform);
+ }
+
c.cursor = model.collection.aggregate(agg._pipeline, agg.options || {});
c.emit('cursor', c.cursor);
});
@@ -168,40 +175,77 @@ AggregationCursor.prototype._markError = function(error) {
* Marks this cursor as closed. Will stop streaming and subsequent calls to
* `next()` will error.
*
- * @param {Function} callback
* @return {Promise}
* @api public
* @method close
- * @emits close
+ * @emits "close"
* @see AggregationCursor.close https://mongodb.github.io/node-mongodb-native/4.9/classes/AggregationCursor.html#close
*/
-AggregationCursor.prototype.close = function(callback) {
- return promiseOrCallback(callback, cb => {
- this.cursor.close(error => {
- if (error) {
- cb(error);
- return this.listeners('error').length > 0 && this.emit('error', error);
- }
- this.emit('close');
- cb(null);
+AggregationCursor.prototype.close = async function close() {
+ if (typeof arguments[0] === 'function') {
+ throw new MongooseError('AggregationCursor.prototype.close() no longer accepts a callback');
+ }
+ try {
+ await this.cursor.close();
+ } catch (error) {
+ this.listeners('error').length > 0 && this.emit('error', error);
+ throw error;
+ }
+ this.emit('close');
+};
+
+/**
+ * Marks this cursor as destroyed. Will stop streaming and subsequent calls to
+ * `next()` will error.
+ *
+ * @return {this}
+ * @api private
+ * @method _destroy
+ */
+
+AggregationCursor.prototype._destroy = function _destroy(_err, callback) {
+ let waitForCursor = null;
+ if (!this.cursor) {
+ waitForCursor = new Promise((resolve) => {
+ this.once('cursor', resolve);
});
- });
+ } else {
+ waitForCursor = Promise.resolve();
+ }
+
+ waitForCursor
+ .then(() => this.cursor.close())
+ .then(() => {
+ this._closed = true;
+ callback();
+ })
+ .catch(error => {
+ callback(error);
+ });
+ return this;
};
/**
* Get the next document from this cursor. Will return `null` when there are
* no documents left.
*
- * @param {Function} callback
* @return {Promise}
* @api public
* @method next
*/
-AggregationCursor.prototype.next = function(callback) {
- return promiseOrCallback(callback, cb => {
- _next(this, cb);
+AggregationCursor.prototype.next = async function next() {
+ if (typeof arguments[0] === 'function') {
+ throw new MongooseError('AggregationCursor.prototype.next() no longer accepts a callback');
+ }
+ return new Promise((resolve, reject) => {
+ _next(this, (err, res) => {
+ if (err != null) {
+ return reject(err);
+ }
+ resolve(res);
+ });
});
};
@@ -213,21 +257,24 @@ AggregationCursor.prototype.next = function(callback) {
* @param {Function} fn
* @param {Object} [options]
* @param {Number} [options.parallel] the number of promises to execute in parallel. Defaults to 1.
- * @param {Function} [callback] executed when all docs have been processed
+ * @param {Number} [options.batchSize=null] if set, Mongoose will call `fn` with an array of at most `batchSize` documents, instead of a single document
+ * @param {Boolean} [options.continueOnError=false] if true, `eachAsync()` iterates through all docs even if `fn` throws an error. If false, `eachAsync()` throws an error immediately if the given function `fn()` throws an error.
* @return {Promise}
* @api public
* @method eachAsync
*/
-AggregationCursor.prototype.eachAsync = function(fn, opts, callback) {
+AggregationCursor.prototype.eachAsync = function(fn, opts) {
+ if (typeof arguments[2] === 'function') {
+ throw new MongooseError('AggregationCursor.prototype.eachAsync() no longer accepts a callback');
+ }
const _this = this;
if (typeof opts === 'function') {
- callback = opts;
opts = {};
}
opts = opts || {};
- return eachAsync(function(cb) { return _next(_this, cb); }, fn, opts, callback);
+ return eachAsync(function(cb) { return _next(_this, cb); }, fn, opts);
};
/**
@@ -360,16 +407,16 @@ function _next(ctx, cb) {
}
if (ctx.cursor) {
- return ctx.cursor.next(function(error, doc) {
- if (error) {
- return callback(error);
- }
- if (!doc) {
- return callback(null, null);
- }
+ return ctx.cursor.next().then(
+ doc => {
+ if (!doc) {
+ return callback(null, null);
+ }
- callback(null, doc);
- });
+ callback(null, doc);
+ },
+ err => callback(err)
+ );
} else {
ctx.once('cursor', function() {
_next(ctx, cb);
diff --git a/lib/cursor/changeStream.js b/lib/cursor/changeStream.js
new file mode 100644
index 00000000000..b41e2379e83
--- /dev/null
+++ b/lib/cursor/changeStream.js
@@ -0,0 +1,198 @@
+'use strict';
+
+/*!
+ * Module dependencies.
+ */
+
+const EventEmitter = require('events').EventEmitter;
+const MongooseError = require('../error/mongooseError');
+
+/*!
+ * ignore
+ */
+
+const driverChangeStreamEvents = ['close', 'change', 'end', 'error', 'resumeTokenChanged'];
+
+/*!
+ * ignore
+ */
+
+class ChangeStream extends EventEmitter {
+ constructor(changeStreamThunk, pipeline, options) {
+ super();
+
+ this.driverChangeStream = null;
+ this.closed = false;
+ this.bindedEvents = false;
+ this.pipeline = pipeline;
+ this.options = options;
+ this.errored = false;
+
+ if (options && options.hydrate && !options.model) {
+ throw new Error(
+ 'Cannot create change stream with `hydrate: true` ' +
+ 'unless calling `Model.watch()`'
+ );
+ }
+
+ let syncError = null;
+ this.$driverChangeStreamPromise = new Promise((resolve, reject) => {
+ // This wrapper is necessary because of buffering.
+ try {
+ changeStreamThunk((err, driverChangeStream) => {
+ if (err != null) {
+ this.errored = true;
+ this.emit('error', err);
+ return reject(err);
+ }
+
+ this.driverChangeStream = driverChangeStream;
+ this.emit('ready');
+ resolve();
+ });
+ } catch (err) {
+ syncError = err;
+ this.errored = true;
+ this.emit('error', err);
+ reject(err);
+ }
+ });
+
+ // Because a ChangeStream is an event emitter, there's no way to register an 'error' handler
+ // that catches errors which occur in the constructor, unless we force sync errors into async
+ // errors with setImmediate(). For cleaner stack trace, we just immediately throw any synchronous
+ // errors that occurred with changeStreamThunk().
+ if (syncError != null) {
+ throw syncError;
+ }
+ }
+
+ _bindEvents() {
+ if (this.bindedEvents) {
+ return;
+ }
+
+ this.bindedEvents = true;
+
+ if (this.driverChangeStream == null) {
+ this.$driverChangeStreamPromise.then(
+ () => {
+ this.driverChangeStream.on('close', () => {
+ this.closed = true;
+ });
+
+ driverChangeStreamEvents.forEach(ev => {
+ this.driverChangeStream.on(ev, data => {
+ if (data != null && data.fullDocument != null && this.options && this.options.hydrate) {
+ data.fullDocument = this.options.model.hydrate(data.fullDocument);
+ }
+ this.emit(ev, data);
+ });
+ });
+ },
+ () => {} // No need to register events if opening change stream failed
+ );
+
+ return;
+ }
+
+ this.driverChangeStream.on('close', () => {
+ this.closed = true;
+ });
+
+ driverChangeStreamEvents.forEach(ev => {
+ this.driverChangeStream.on(ev, data => {
+ if (data != null && data.fullDocument != null && this.options && this.options.hydrate) {
+ data.fullDocument = this.options.model.hydrate(data.fullDocument);
+ }
+ this.emit(ev, data);
+ });
+ });
+ }
+
+ hasNext(cb) {
+ if (this.errored) {
+ throw new MongooseError('Cannot call hasNext() on errored ChangeStream');
+ }
+ return this.driverChangeStream.hasNext(cb);
+ }
+
+ next(cb) {
+ if (this.errored) {
+ throw new MongooseError('Cannot call next() on errored ChangeStream');
+ }
+ if (this.options && this.options.hydrate) {
+ if (cb != null) {
+ const originalCb = cb;
+ cb = (err, data) => {
+ if (err != null) {
+ return originalCb(err);
+ }
+ if (data.fullDocument != null) {
+ data.fullDocument = this.options.model.hydrate(data.fullDocument);
+ }
+ return originalCb(null, data);
+ };
+ }
+
+ let maybePromise = this.driverChangeStream.next(cb);
+ if (maybePromise && typeof maybePromise.then === 'function') {
+ maybePromise = maybePromise.then(data => {
+ if (data.fullDocument != null) {
+ data.fullDocument = this.options.model.hydrate(data.fullDocument);
+ }
+ return data;
+ });
+ }
+ return maybePromise;
+ }
+
+ return this.driverChangeStream.next(cb);
+ }
+
+ addListener(event, handler) {
+ if (this.errored) {
+ throw new MongooseError('Cannot call addListener() on errored ChangeStream');
+ }
+ this._bindEvents();
+ return super.addListener(event, handler);
+ }
+
+ on(event, handler) {
+ if (this.errored) {
+ throw new MongooseError('Cannot call on() on errored ChangeStream');
+ }
+ this._bindEvents();
+ return super.on(event, handler);
+ }
+
+ once(event, handler) {
+ if (this.errored) {
+ throw new MongooseError('Cannot call once() on errored ChangeStream');
+ }
+ this._bindEvents();
+ return super.once(event, handler);
+ }
+
+ _queue(cb) {
+ this.once('ready', () => cb());
+ }
+
+ close() {
+ this.closed = true;
+ if (this.driverChangeStream) {
+ return this.driverChangeStream.close();
+ } else {
+ return this.$driverChangeStreamPromise.then(
+ () => this.driverChangeStream.close(),
+ () => {} // No need to close if opening the change stream failed
+ );
+ }
+ }
+}
+
+/*!
+ * ignore
+ */
+
+module.exports = ChangeStream;
diff --git a/lib/cursor/QueryCursor.js b/lib/cursor/queryCursor.js
similarity index 67%
rename from lib/cursor/QueryCursor.js
rename to lib/cursor/queryCursor.js
index 96fbc80dd67..f25a06f2fd1 100644
--- a/lib/cursor/QueryCursor.js
+++ b/lib/cursor/queryCursor.js
@@ -4,11 +4,13 @@
'use strict';
+const MongooseError = require('../error/mongooseError');
const Readable = require('stream').Readable;
-const promiseOrCallback = require('../helpers/promiseOrCallback');
const eachAsync = require('../helpers/cursor/eachAsync');
-const helpers = require('../queryhelpers');
+const helpers = require('../queryHelpers');
+const kareem = require('kareem');
const immediate = require('../helpers/immediate');
+const { once } = require('events');
const util = require('util');
/**
@@ -21,7 +23,7 @@ const util = require('util');
* from MongoDB, and the model's post `find` hooks after loading each document.
*
* Unless you're an advanced user, do **not** instantiate this class directly.
- * Use [`Query#cursor()`](/docs/api/query.html#query_Query-cursor) instead.
+ * Use [`Query#cursor()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.cursor()) instead.
*
* @param {Query} query
* @param {Object} options query options passed to `.find()`
@@ -33,59 +35,82 @@ const util = require('util');
* @api public
*/
-function QueryCursor(query, options) {
+function QueryCursor(query) {
// set autoDestroy=true because on node 12 it's by default false
// gh-10902 need autoDestroy to destroy correctly and emit 'close' event
Readable.call(this, { autoDestroy: true, objectMode: true });
this.cursor = null;
+ this.skipped = false;
this.query = query;
- const _this = this;
+ this._closed = false;
const model = query.model;
this._mongooseOptions = {};
this._transforms = [];
this.model = model;
- this.options = options || {};
-
+ this.options = {};
model.hooks.execPre('find', query, (err) => {
if (err != null) {
- _this._markError(err);
- _this.listeners('error').length > 0 && _this.emit('error', err);
+ if (err instanceof kareem.skipWrappedFunction) {
+ const resultValue = err.args[0];
+ if (resultValue != null && (!Array.isArray(resultValue) || resultValue.length)) {
+ const err = new MongooseError(
+ 'Cannot `skipMiddlewareFunction()` with a value when using ' +
+ '`.find().cursor()`, value must be nullish or empty array, got "' +
+ util.inspect(resultValue) +
+ '".'
+ );
+ this._markError(err);
+ this.listeners('error').length > 0 && this.emit('error', err);
+ return;
+ }
+ this.skipped = true;
+ this.emit('cursor', null);
+ return;
+ }
+ this._markError(err);
+ this.listeners('error').length > 0 && this.emit('error', err);
return;
}
+ Object.assign(this.options, query._optionsForExec());
this._transforms = this._transforms.concat(query._transforms.slice());
if (this.options.transform) {
- this._transforms.push(options.transform);
+ this._transforms.push(this.options.transform);
}
// Re: gh-8039, you need to set the `cursor.batchSize` option, top-level
// `batchSize` option doesn't work.
if (this.options.batchSize) {
- this.options.cursor = options.cursor || {};
- this.options.cursor.batchSize = options.batchSize;
-
// Max out the number of documents we'll populate in parallel at 5000.
this.options._populateBatchSize = Math.min(this.options.batchSize, 5000);
}
- Object.assign(this.options, query._optionsForExec());
- model.collection.find(query._conditions, this.options, (err, cursor) => {
- if (err != null) {
- _this._markError(err);
- _this.listeners('error').length > 0 && _this.emit('error', _this._error);
- return;
- }
- if (_this._error) {
- cursor.close(function() {});
- _this.listeners('error').length > 0 && _this.emit('error', _this._error);
- }
- _this.cursor = cursor;
- _this.emit('cursor', cursor);
- });
+ if (model.collection._shouldBufferCommands() && model.collection.buffer) {
+ model.collection.queue.push([
+ () => _getRawCursor(query, this)
+ ]);
+ } else {
+ _getRawCursor(query, this);
+ }
});
}
util.inherits(QueryCursor, Readable);
+/*!
+ * ignore
+ */
+
+function _getRawCursor(query, queryCursor) {
+ try {
+ const cursor = query.model.collection.find(query._conditions, queryCursor.options);
+ queryCursor.cursor = cursor;
+ queryCursor.emit('cursor', cursor);
+ } catch (err) {
+ queryCursor._markError(err);
+ queryCursor.listeners('error').length > 0 && queryCursor.emit('error', queryCursor._error);
+ }
+}
+
/**
* Necessary to satisfy the Readable API
* @method _read
@@ -95,24 +120,42 @@ util.inherits(QueryCursor, Readable);
*/
QueryCursor.prototype._read = function() {
- const _this = this;
- _next(this, function(error, doc) {
+ _next(this, (error, doc) => {
if (error) {
- return _this.emit('error', error);
+ return this.emit('error', error);
}
if (!doc) {
- _this.push(null);
- _this.cursor.close(function(error) {
+ this.push(null);
+ this.cursor.close(function(error) {
if (error) {
- return _this.emit('error', error);
+ return this.emit('error', error);
}
});
return;
}
- _this.push(doc);
+ this.push(doc);
});
};
+/**
+ * Returns the underlying cursor from the MongoDB Node driver that this cursor uses.
+ *
+ * @method getDriverCursor
+ * @memberOf QueryCursor
+ * @returns {Cursor} MongoDB Node driver cursor instance
+ * @instance
+ * @api public
+ */
+
+QueryCursor.prototype.getDriverCursor = async function getDriverCursor() {
+ if (this.cursor) {
+ return this.cursor;
+ }
+
+ await once(this, 'cursor');
+ return this.cursor;
+};
+
/**
* Registers a transform function which subsequently maps documents retrieved
* via the streams interface or `.next()`
@@ -174,7 +217,6 @@ QueryCursor.prototype._markError = function(error) {
* Marks this cursor as closed. Will stop streaming and subsequent calls to
* `next()` will error.
*
- * @param {Function} callback
* @return {Promise}
* @api public
* @method close
@@ -182,17 +224,51 @@ QueryCursor.prototype._markError = function(error) {
* @see AggregationCursor.close https://mongodb.github.io/node-mongodb-native/4.9/classes/AggregationCursor.html#close
*/
-QueryCursor.prototype.close = function(callback) {
- return promiseOrCallback(callback, cb => {
- this.cursor.close(error => {
- if (error) {
- cb(error);
- return this.listeners('error').length > 0 && this.emit('error', error);
- }
- this.emit('close');
- cb(null);
+QueryCursor.prototype.close = async function close() {
+ if (typeof arguments[0] === 'function') {
+ throw new MongooseError('QueryCursor.prototype.close() no longer accepts a callback');
+ }
+ try {
+ await this.cursor.close();
+ this._closed = true;
+ this.emit('close');
+ } catch (error) {
+ this.listeners('error').length > 0 && this.emit('error', error);
+ throw error;
+ }
+};
+
+/**
+ * Marks this cursor as destroyed. Will stop streaming and subsequent calls to
+ * `next()` will error.
+ *
+ * @return {this}
+ * @api private
+ * @method _destroy
+ */
+
+QueryCursor.prototype._destroy = function _destroy(_err, callback) {
+ let waitForCursor = null;
+ if (!this.cursor) {
+ waitForCursor = new Promise((resolve) => {
+ this.once('cursor', resolve);
+ });
+ } else {
+ waitForCursor = Promise.resolve();
+ }
+
+ waitForCursor
+ .then(() => {
+ this.cursor.close();
+ })
+ .then(() => {
+ this._closed = true;
+ callback();
+ })
+ .catch(error => {
+ callback(error);
});
- }, this.model.events);
+ return this;
};
/**
@@ -206,9 +282,8 @@ QueryCursor.prototype.close = function(callback) {
*/
QueryCursor.prototype.rewind = function() {
- const _this = this;
- _waitForCursor(this, function() {
- _this.cursor.rewind();
+ _waitForCursor(this, () => {
+ this.cursor.rewind();
});
return this;
};
@@ -217,21 +292,26 @@ QueryCursor.prototype.rewind = function() {
* Get the next document from this cursor. Will return `null` when there are
* no documents left.
*
- * @param {Function} callback
* @return {Promise}
* @api public
* @method next
*/
-QueryCursor.prototype.next = function(callback) {
- return promiseOrCallback(callback, cb => {
+QueryCursor.prototype.next = async function next() {
+ if (typeof arguments[0] === 'function') {
+ throw new MongooseError('QueryCursor.prototype.next() no longer accepts a callback');
+ }
+ if (this._closed) {
+ throw new MongooseError('Cannot call `next()` on a closed cursor');
+ }
+ return new Promise((resolve, reject) => {
_next(this, function(error, doc) {
if (error) {
- return cb(error);
+ return reject(error);
}
- cb(null, doc);
+ resolve(doc);
});
- }, this.model.events);
+ });
};
/**
@@ -255,21 +335,21 @@ QueryCursor.prototype.next = function(callback) {
* @param {Number} [options.parallel] the number of promises to execute in parallel. Defaults to 1.
* @param {Number} [options.batchSize] if set, will call `fn()` with arrays of documents with length at most `batchSize`
* @param {Boolean} [options.continueOnError=false] if true, `eachAsync()` iterates through all docs even if `fn` throws an error. If false, `eachAsync()` throws an error immediately if the given function `fn()` throws an error.
- * @param {Function} [callback] executed when all docs have been processed
* @return {Promise}
* @api public
* @method eachAsync
*/
-QueryCursor.prototype.eachAsync = function(fn, opts, callback) {
- const _this = this;
+QueryCursor.prototype.eachAsync = function(fn, opts) {
+ if (typeof arguments[2] === 'function') {
+ throw new MongooseError('QueryCursor.prototype.eachAsync() no longer accepts a callback');
+ }
if (typeof opts === 'function') {
- callback = opts;
opts = {};
}
opts = opts || {};
- return eachAsync(function(cb) { return _next(_this, cb); }, fn, opts, callback);
+ return eachAsync((cb) => _next(this, cb), fn, opts);
};
/**
@@ -293,9 +373,8 @@ QueryCursor.prototype.options;
*/
QueryCursor.prototype.addCursorFlag = function(flag, value) {
- const _this = this;
- _waitForCursor(this, function() {
- _this.cursor.addCursorFlag(flag, value);
+ _waitForCursor(this, () => {
+ this.cursor.addCursorFlag(flag, value);
});
return this;
};
@@ -393,6 +472,9 @@ function _next(ctx, cb) {
callback(ctx._error);
});
}
+ if (ctx.skipped) {
+ return immediate(() => callback(null, null));
+ }
if (ctx.cursor) {
if (ctx.query._mongooseOptions.populate && !ctx._pop) {
@@ -410,28 +492,37 @@ function _next(ctx, cb) {
} else {
// Request as many docs as batchSize, to populate them also in batch
ctx._batchDocs = [];
- return ctx.cursor.next(_onNext.bind({ ctx, callback }));
+ ctx.cursor.next().then(
+ res => { _onNext.call({ ctx, callback }, null, res); },
+ err => { _onNext.call({ ctx, callback }, err); }
+ );
+ return;
}
} else {
- return ctx.cursor.next(function(error, doc) {
- if (error) {
- return callback(error);
- }
- if (!doc) {
- return callback(null, null);
- }
-
- if (!ctx.query._mongooseOptions.populate) {
- return _nextDoc(ctx, doc, null, callback);
- }
+ return ctx.cursor.next().then(
+ doc => {
+ if (!doc) {
+ callback(null, null);
+ return;
+ }
- ctx.query.model.populate(doc, ctx._pop, function(err, doc) {
- if (err) {
- return callback(err);
+ if (!ctx.query._mongooseOptions.populate) {
+ return _nextDoc(ctx, doc, null, callback);
}
- return _nextDoc(ctx, doc, ctx._pop, callback);
- });
- });
+
+ ctx.query.model.populate(doc, ctx._pop).then(
+ doc => {
+ _nextDoc(ctx, doc, ctx._pop, callback);
+ },
+ err => {
+ callback(err);
+ }
+ );
+ },
+ error => {
+ callback(error);
+ }
+ );
}
} else {
ctx.once('error', cb);
@@ -439,6 +530,9 @@ function _next(ctx, cb) {
ctx.once('cursor', function(cursor) {
ctx.removeListener('error', cb);
if (cursor == null) {
+ if (ctx.skipped) {
+ return cb(null, null);
+ }
return;
}
_next(ctx, cb);
@@ -463,11 +557,11 @@ function _onNext(error, doc) {
if (this.ctx._batchDocs.length < this.ctx.options._populateBatchSize) {
// If both `batchSize` and `_populateBatchSize` are huge, calling `next()` repeatedly may
- // cause a stack overflow. So make sure we clear the stack regularly.
- if (this.ctx._batchDocs.length > 0 && this.ctx._batchDocs.length % 1000 === 0) {
- return immediate(() => this.ctx.cursor.next(_onNext.bind(this)));
- }
- this.ctx.cursor.next(_onNext.bind(this));
+ // cause a stack overflow. So make sure we clear the stack.
+ immediate(() => this.ctx.cursor.next().then(
+ res => { _onNext.call(this, null, res); },
+ err => { _onNext.call(this, err); }
+ ));
} else {
_populateBatch.call(this);
}
@@ -481,14 +575,14 @@ function _populateBatch() {
if (!this.ctx._batchDocs.length) {
return this.callback(null, null);
}
- const _this = this;
- this.ctx.query.model.populate(this.ctx._batchDocs, this.ctx._pop, function(err) {
- if (err) {
- return _this.callback(err);
+ this.ctx.query.model.populate(this.ctx._batchDocs, this.ctx._pop).then(
+ () => {
+ _nextDoc(this.ctx, this.ctx._batchDocs.shift(), this.ctx._pop, this.callback);
+ },
+ err => {
+ this.callback(err);
}
-
- _nextDoc(_this.ctx, _this.ctx._batchDocs.shift(), _this.ctx._pop, _this.callback);
- });
+ );
}
/*!
diff --git a/lib/document.js b/lib/document.js
index d1909489137..3b02ed475a5 100644
--- a/lib/document.js
+++ b/lib/document.js
@@ -4,10 +4,13 @@
* Module dependencies.
*/
+const DivergentArrayError = require('./error/divergentArray');
const EventEmitter = require('events').EventEmitter;
const InternalCache = require('./internal');
+const MongooseBuffer = require('./types/buffer');
const MongooseError = require('./error/index');
const MixedSchema = require('./schema/mixed');
+const ModifiedPathsSnapshot = require('./modifiedPathsSnapshot');
const ObjectExpectedError = require('./error/objectExpected');
const ObjectParameterError = require('./error/objectParameter');
const ParallelValidateError = require('./error/parallelValidate');
@@ -16,29 +19,32 @@ const StrictModeError = require('./error/strict');
const ValidationError = require('./error/validation');
const ValidatorError = require('./error/validator');
const $__hasIncludedChildren = require('./helpers/projection/hasIncludedChildren');
-const promiseOrCallback = require('./helpers/promiseOrCallback');
const applyDefaults = require('./helpers/document/applyDefaults');
const cleanModifiedSubpaths = require('./helpers/document/cleanModifiedSubpaths');
+const clone = require('./helpers/clone');
const compile = require('./helpers/document/compile').compile;
const defineKey = require('./helpers/document/compile').defineKey;
+const firstKey = require('./helpers/firstKey');
const flatten = require('./helpers/common').flatten;
-const flattenObjectWithDottedPaths = require('./helpers/path/flattenObjectWithDottedPaths');
-const get = require('./helpers/get');
const getEmbeddedDiscriminatorPath = require('./helpers/document/getEmbeddedDiscriminatorPath');
const getKeysInSchemaOrder = require('./helpers/schema/getKeysInSchemaOrder');
+const getSubdocumentStrictValue = require('./helpers/schema/getSubdocumentStrictValue');
const handleSpreadDoc = require('./helpers/document/handleSpreadDoc');
const immediate = require('./helpers/immediate');
+const isBsonType = require('./helpers/isBsonType');
const isDefiningProjection = require('./helpers/projection/isDefiningProjection');
const isExclusive = require('./helpers/projection/isExclusive');
+const isPathExcluded = require('./helpers/projection/isPathExcluded');
const inspect = require('util').inspect;
const internalToObjectOptions = require('./options').internalToObjectOptions;
const markArraySubdocsPopulated = require('./helpers/populate/markArraySubdocsPopulated');
+const minimize = require('./helpers/minimize');
const mpath = require('mpath');
-const queryhelpers = require('./queryhelpers');
+const parentPaths = require('./helpers/path/parentPaths');
+const queryhelpers = require('./queryHelpers');
const utils = require('./utils');
const isPromise = require('./helpers/isPromise');
-const clone = utils.clone;
const deepEqual = utils.deepEqual;
const isMongooseObject = utils.isMongooseObject;
@@ -49,10 +55,12 @@ const documentIsModified = require('./helpers/symbols').documentIsModified;
const documentModifiedPaths = require('./helpers/symbols').documentModifiedPaths;
const documentSchemaSymbol = require('./helpers/symbols').documentSchemaSymbol;
const getSymbol = require('./helpers/symbols').getSymbol;
+const modelSymbol = require('./helpers/symbols').modelSymbol;
const populateModelSymbol = require('./helpers/symbols').populateModelSymbol;
const scopeSymbol = require('./helpers/symbols').scopeSymbol;
const schemaMixedSymbol = require('./schema/symbols').schemaMixedSymbol;
-const parentPaths = require('./helpers/path/parentPaths');
+const getDeepestSubdocumentForPath = require('./helpers/document/getDeepestSubdocumentForPath');
+const sessionNewDocuments = require('./helpers/symbols').sessionNewDocuments;
let DocumentArray;
let MongooseArray;
@@ -60,6 +68,10 @@ let Embedded;
const specialProperties = utils.specialProperties;
+const VERSION_WHERE = 1;
+const VERSION_INC = 2;
+const VERSION_ALL = VERSION_WHERE | VERSION_INC;
+
/**
* The core Mongoose document constructor. You should not call this directly,
* the Mongoose [Model constructor](./api/model.html#Model) calls this for you.
@@ -153,7 +165,9 @@ function Document(obj, fields, skipId, options) {
// By default, defaults get applied **before** setting initial values
// Re: gh-6155
if (defaults) {
- applyDefaults(this, fields, exclude, hasIncludedChildren, true, null);
+ applyDefaults(this, fields, exclude, hasIncludedChildren, true, null, {
+ skipParentChangeTracking: true
+ });
}
}
if (obj) {
@@ -212,7 +226,7 @@ Document.prototype.$isMongooseDocumentPrototype = true;
* await user.save(); // Sends an `insertOne` to MongoDB
*
* On the other hand, if you load an existing document from the database
- * using `findOne()` or another [query operation](/docs/queries.html),
+ * using `findOne()` or another [query operation](https://mongoosejs.com/docs/queries.html),
* `$isNew` will be false.
*
* #### Example:
@@ -376,7 +390,7 @@ Object.defineProperty(Document.prototype, '$locals', {
* @api public
* @property isNew
* @memberOf Document
- * @see $isNew #document_Document-$isNew
+ * @see $isNew https://mongoosejs.com/docs/api/document.html#Document.prototype.$isNew
* @instance
*/
@@ -409,12 +423,12 @@ Object.defineProperty(Document.prototype, '$where', {
*
* #### Note:
*
- * This getter exists on all documents by default. The getter can be disabled by setting the `id` [option](/docs/guide.html#id) of its `Schema` to false at construction time.
+ * This getter exists on all documents by default. The getter can be disabled by setting the `id` [option](https://mongoosejs.com/docs/guide.html#id) of its `Schema` to false at construction time.
*
* new Schema({ name: String }, { id: false });
*
* @api public
- * @see Schema options /docs/guide.html#options
+ * @see Schema options https://mongoosejs.com/docs/guide.html#options
* @property id
* @memberOf Document
* @instance
@@ -472,8 +486,6 @@ function $applyDefaultsToNested(val, path, doc) {
return;
}
- flattenObjectWithDottedPaths(val);
-
const paths = Object.keys(doc.$__schema.paths);
const plen = paths.length;
@@ -613,16 +625,17 @@ Document.prototype.toBSON = function() {
};
/**
- * Initializes the document without setters or marking anything modified.
+ * Hydrates this document with the data in `doc`. Does not run setters or mark any paths modified.
*
- * Called internally after a document is returned from mongodb. Normally,
+ * Called internally after a document is returned from MongoDB. Normally,
* you do **not** need to call this function on your own.
*
- * This function triggers `init` [middleware](/docs/middleware.html).
- * Note that `init` hooks are [synchronous](/docs/middleware.html#synchronous).
+ * This function triggers `init` [middleware](https://mongoosejs.com/docs/middleware.html).
+ * Note that `init` hooks are [synchronous](https://mongoosejs.com/docs/middleware.html#synchronous).
*
- * @param {Object} doc document returned by mongo
+ * @param {Object} doc raw document returned by mongo
* @param {Object} [opts]
+ * @param {Boolean} [opts.hydratedPopulatedDocs=false] If true, hydrate and mark as populated any paths that are populated in the raw document
* @param {Function} [fn]
* @api public
* @memberOf Document
@@ -645,7 +658,7 @@ Document.prototype.init = function(doc, opts, fn) {
};
/**
- * Alias for [`.init`](#document_Document-init)
+ * Alias for [`.init`](https://mongoosejs.com/docs/api/document.html#Document.prototype.init())
*
* @api public
*/
@@ -694,7 +707,6 @@ Document.prototype.$__init = function(doc, opts) {
init(this, doc, this._doc, opts);
markArraySubdocsPopulated(this, opts.populated);
-
this.$emit('init', this);
this.constructor.emit('init', this);
@@ -703,7 +715,6 @@ Document.prototype.$__init = function(doc, opts) {
null;
applyDefaults(this, this.$__.selected, this.$__.exclude, hasIncludedChildren, false, this.$__.skipDefaults);
-
return this;
};
@@ -744,9 +755,8 @@ function init(self, obj, doc, opts, prefix) {
if (i === '__proto__' || i === 'constructor') {
return;
}
- path = prefix + i;
+ path = prefix ? prefix + i : i;
schemaType = docSchema.path(path);
-
// Should still work if not a model-level discriminator, but should not be
// necessary. This is *only* to catch the case where we queried using the
// base model and the discriminated model has a projection
@@ -754,7 +764,8 @@ function init(self, obj, doc, opts, prefix) {
return;
}
- if (!schemaType && utils.isPOJO(obj[i])) {
+ const value = obj[i];
+ if (!schemaType && utils.isPOJO(value)) {
// assume nested object
if (!doc[i]) {
doc[i] = {};
@@ -762,30 +773,30 @@ function init(self, obj, doc, opts, prefix) {
self[i] = doc[i];
}
}
- init(self, obj[i], doc[i], opts, path + '.');
+ init(self, value, doc[i], opts, path + '.');
} else if (!schemaType) {
- doc[i] = obj[i];
+ doc[i] = value;
if (!strict && !prefix) {
- self[i] = obj[i];
+ self[i] = value;
}
} else {
// Retain order when overwriting defaults
- if (doc.hasOwnProperty(i) && obj[i] !== void 0) {
+ if (doc.hasOwnProperty(i) && value !== void 0 && !opts.hydratedPopulatedDocs) {
delete doc[i];
}
- if (obj[i] === null) {
+ if (value === null) {
doc[i] = schemaType._castNullish(null);
- } else if (obj[i] !== undefined) {
- const wasPopulated = obj[i].$__ == null ? null : obj[i].$__.wasPopulated;
+ } else if (value !== undefined) {
+ const wasPopulated = value.$__ == null ? null : value.$__.wasPopulated;
- if (schemaType && !wasPopulated) {
+ if (schemaType && !wasPopulated && !opts.hydratedPopulatedDocs) {
try {
if (opts && opts.setters) {
// Call applySetters with `init = false` because otherwise setters are a noop
const overrideInit = false;
- doc[i] = schemaType.applySetters(obj[i], self, overrideInit);
+ doc[i] = schemaType.applySetters(value, self, overrideInit);
} else {
- doc[i] = schemaType.cast(obj[i], self, true);
+ doc[i] = schemaType.cast(value, self, true);
}
} catch (e) {
self.invalidate(e.path, new ValidatorError({
@@ -796,8 +807,16 @@ function init(self, obj, doc, opts, prefix) {
reason: e
}));
}
+ } else if (opts.hydratedPopulatedDocs) {
+ doc[i] = schemaType.cast(value, self, true);
+
+ if (doc[i] && doc[i].$__ && doc[i].$__.wasPopulated) {
+ self.$populated(path, doc[i].$__.wasPopulated.value, doc[i].$__.wasPopulated.options);
+ } else if (Array.isArray(doc[i]) && doc[i].length && doc[i][0]?.$__?.wasPopulated) {
+ self.$populated(path, doc[i].map(populatedDoc => populatedDoc?.$__?.wasPopulated?.value).filter(val => val != null), doc[i][0].$__.wasPopulated.options);
+ }
} else {
- doc[i] = obj[i];
+ doc[i] = value;
}
}
// mark as hydrated
@@ -808,59 +827,23 @@ function init(self, obj, doc, opts, prefix) {
}
}
-/**
- * Sends an update command with this document `_id` as the query selector.
- *
- * #### Example:
- *
- * weirdCar.update({ $inc: { wheels:1 } }, { w: 1 }, callback);
- *
- * #### Valid options:
- *
- * - same as in [Model.update](#model_Model-update)
- *
- * @see Model.update #model_Model-update
- * @param {...Object} ops
- * @param {Object} [options]
- * @param {Function} [callback]
- * @return {Query} this
- * @api public
- * @memberOf Document
- * @instance
- */
-
-Document.prototype.update = function update() {
- const args = [...arguments];
- args.unshift({ _id: this._id });
- const query = this.constructor.update.apply(this.constructor, args);
-
- if (this.$session() != null) {
- if (!('session' in query.options)) {
- query.options.session = this.$session();
- }
- }
-
- return query;
-};
-
/**
* Sends an updateOne command with this document `_id` as the query selector.
*
* #### Example:
*
- * weirdCar.updateOne({$inc: {wheels:1}}, { w: 1 }, callback);
+ * weirdCar.updateOne({$inc: {wheels:1}}, { w: 1 });
*
* #### Valid options:
*
- * - same as in [Model.updateOne](#model_Model-updateOne)
+ * - same as in [Model.updateOne](https://mongoosejs.com/docs/api/model.html#Model.updateOne)
*
- * @see Model.updateOne #model_Model-updateOne
+ * @see Model.updateOne https://mongoosejs.com/docs/api/model.html#Model.updateOne
* @param {Object} doc
- * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#query_Query-setOptions)
- * @param {Object} [options.lean] if truthy, mongoose will return the document as a plain JavaScript object rather than a mongoose document. See [`Query.lean()`](/docs/api/query.html#query_Query-lean) and the [Mongoose lean tutorial](/docs/tutorials/lean.html).
+ * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions())
+ * @param {Object} [options.lean] if truthy, mongoose will return the document as a plain JavaScript object rather than a mongoose document. See [`Query.lean()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.lean()) and the [Mongoose lean tutorial](https://mongoosejs.com/docs/tutorials/lean.html).
* @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict)
- * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Note that this allows you to overwrite timestamps. Does nothing if schema-level timestamps are not set.
- * @param {Function} [callback]
+ * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Note that this allows you to overwrite timestamps. Does nothing if schema-level timestamps are not set.
* @return {Query}
* @api public
* @memberOf Document
@@ -868,7 +851,7 @@ Document.prototype.update = function update() {
*/
Document.prototype.updateOne = function updateOne(doc, options, callback) {
- const query = this.constructor.updateOne({ _id: this._id }, doc, options);
+ const query = this.constructor.updateOne({ _id: this._doc._id }, doc, options);
const self = this;
query.pre(function queryPreUpdateOne(cb) {
self.constructor._middleware.execPre('updateOne', self, [self], cb);
@@ -895,9 +878,9 @@ Document.prototype.updateOne = function updateOne(doc, options, callback) {
*
* #### Valid options:
*
- * - same as in [Model.replaceOne](https://mongoosejs.com/docs/api/model.html#model_Model-replaceOne)
+ * - same as in [Model.replaceOne](https://mongoosejs.com/docs/api/model.html#Model.replaceOne())
*
- * @see Model.replaceOne #model_Model-replaceOne
+ * @see Model.replaceOne https://mongoosejs.com/docs/api/model.html#Model.replaceOne()
* @param {Object} doc
* @param {Object} [options]
* @param {Function} [callback]
@@ -909,7 +892,7 @@ Document.prototype.updateOne = function updateOne(doc, options, callback) {
Document.prototype.replaceOne = function replaceOne() {
const args = [...arguments];
- args.unshift({ _id: this._id });
+ args.unshift({ _id: this._doc._id });
return this.constructor.replaceOne.apply(this.constructor, args);
};
@@ -1048,7 +1031,7 @@ Document.prototype.overwrite = function overwrite(obj) {
* @param {Any} val the value to set
* @param {Schema|String|Number|Buffer|*} [type] optionally specify a type for "on-the-fly" attributes
* @param {Object} [options] optionally specify options that modify the behavior of the set
- * @param {Boolean} [options.merge=false] if true, setting a [nested path](/docs/subdocs.html#subdocuments-versus-nested-paths) will merge existing values rather than overwrite the whole object. So `doc.set('nested', { a: 1, b: 2 })` becomes `doc.set('nested.a', 1); doc.set('nested.b', 2);`
+ * @param {Boolean} [options.merge=false] if true, setting a [nested path](https://mongoosejs.com/docs/subdocs.html#subdocuments-versus-nested-paths) will merge existing values rather than overwrite the whole object. So `doc.set('nested', { a: 1, b: 2 })` becomes `doc.set('nested.a', 1); doc.set('nested.b', 2);`
* @return {Document} this
* @method $set
* @memberOf Document
@@ -1072,7 +1055,8 @@ Document.prototype.$set = function $set(path, val, type, options) {
let key;
let prefix;
- const strict = options && 'strict' in options
+ const userSpecifiedStrict = options && 'strict' in options;
+ let strict = userSpecifiedStrict
? options.strict
: this.$__.strictMode;
@@ -1089,7 +1073,11 @@ Document.prototype.$set = function $set(path, val, type, options) {
if (path.$__isNested) {
path = path.toObject();
} else {
- path = path._doc;
+ // This ternary is to support gh-7898 (copying virtuals if same schema)
+ // while not breaking gh-10819, which for some reason breaks if we use toObject()
+ path = path.$__schema === this.$__schema
+ ? applyVirtuals(path, { ...path._doc })
+ : path._doc;
}
}
if (path == null) {
@@ -1112,9 +1100,11 @@ Document.prototype.$set = function $set(path, val, type, options) {
return this;
}
+ options = Object.assign({}, options, { _skipMinimizeTopLevel: false });
+
for (let i = 0; i < len; ++i) {
key = keys[i];
- const pathName = prefix + key;
+ const pathName = prefix ? prefix + key : key;
pathtype = this.$__schema.pathType(pathName);
const valForKey = path[key];
@@ -1126,20 +1116,15 @@ Document.prototype.$set = function $set(path, val, type, options) {
pathtype === 'nested' &&
this._doc[key] != null) {
delete this._doc[key];
- // Make sure we set `{}` back even if we minimize re: gh-8565
- options = Object.assign({}, options, { _skipMinimizeTopLevel: true });
- } else {
- // Make sure we set `{_skipMinimizeTopLevel: false}` if don't have overwrite: gh-10441
- options = Object.assign({}, options, { _skipMinimizeTopLevel: false });
}
if (utils.isNonBuiltinObject(valForKey) && pathtype === 'nested') {
- this.$set(prefix + key, path[key], constructing, Object.assign({}, options, { _skipMarkModified: true }));
- $applyDefaultsToNested(this.$get(prefix + key), prefix + key, this);
+ this.$set(pathName, valForKey, constructing, Object.assign({}, options, { _skipMarkModified: true }));
+ $applyDefaultsToNested(this.$get(pathName), pathName, this);
continue;
} else if (strict) {
// Don't overwrite defaults with undefined keys (gh-3981) (gh-9039)
- if (constructing && path[key] === void 0 &&
+ if (constructing && valForKey === void 0 &&
this.$get(pathName) !== void 0) {
continue;
}
@@ -1149,20 +1134,21 @@ Document.prototype.$set = function $set(path, val, type, options) {
}
if (pathtype === 'real' || pathtype === 'virtual') {
- const p = path[key];
- this.$set(prefix + key, p, constructing, options);
- } else if (pathtype === 'nested' && path[key] instanceof Document) {
- this.$set(prefix + key,
- path[key].toObject({ transform: false }), constructing, options);
+ this.$set(pathName, valForKey, constructing, options);
+ } else if (pathtype === 'nested' && valForKey instanceof Document) {
+ this.$set(pathName,
+ valForKey.toObject({ transform: false }), constructing, options);
} else if (strict === 'throw') {
if (pathtype === 'nested') {
- throw new ObjectExpectedError(key, path[key]);
+ throw new ObjectExpectedError(key, valForKey);
} else {
throw new StrictModeError(key);
}
+ } else if (pathtype === 'nested' && valForKey == null) {
+ this.$set(pathName, valForKey, constructing, options);
}
- } else if (path[key] !== void 0) {
- this.$set(prefix + key, path[key], constructing, options);
+ } else if (valForKey !== void 0) {
+ this.$set(pathName, valForKey, constructing, options);
}
}
@@ -1180,13 +1166,25 @@ Document.prototype.$set = function $set(path, val, type, options) {
}
let pathType = this.$__schema.pathType(path);
+ let parts = null;
if (pathType === 'adhocOrUndefined') {
- pathType = getEmbeddedDiscriminatorPath(this, path, { typeOnly: true });
+ parts = path.indexOf('.') === -1 ? [path] : path.split('.');
+ pathType = getEmbeddedDiscriminatorPath(this, parts, { typeOnly: true });
+ }
+ if (pathType === 'adhocOrUndefined' && !userSpecifiedStrict) {
+ // May be path underneath non-strict schema
+ if (parts == null) {
+ parts = path.indexOf('.') === -1 ? [path] : path.split('.');
+ }
+ const subdocStrict = getSubdocumentStrictValue(this.$__schema, parts);
+ if (subdocStrict !== undefined) {
+ strict = subdocStrict;
+ }
}
// Assume this is a Mongoose document that was copied into a POJO using
// `Object.assign()` or `{...doc}`
- val = handleSpreadDoc(val);
+ val = handleSpreadDoc(val, true);
// if this doc is being constructed we should not trigger getters
const priorVal = (() => {
@@ -1208,6 +1206,7 @@ Document.prototype.$set = function $set(path, val, type, options) {
this.invalidate(path, new MongooseError.CastError('Object', val, path));
return this;
}
+ const wasModified = this.$isModified(path);
const hasInitialVal = this.$__.savedState != null && this.$__.savedState.hasOwnProperty(path);
if (this.$__.savedState != null && !this.$isNew && !this.$__.savedState.hasOwnProperty(path)) {
const initialVal = this.$__getValue(path);
@@ -1223,16 +1222,18 @@ Document.prototype.$set = function $set(path, val, type, options) {
this.$__setValue(path, null);
cleanModifiedSubpaths(this, path);
} else {
- return this.$set(val, path, constructing);
+ return this.$set(val, path, constructing, options);
}
const keys = getKeysInSchemaOrder(this.$__schema, val, path);
this.$__setValue(path, {});
for (const key of keys) {
- this.$set(path + '.' + key, val[key], constructing, options);
+ this.$set(path + '.' + key, val[key], constructing, { ...options, _skipMarkModified: true });
}
- if (priorVal != null && utils.deepEqual(hasInitialVal ? this.$__.savedState[path] : priorVal, val)) {
+ if (priorVal != null &&
+ (!wasModified || hasInitialVal) &&
+ utils.deepEqual(hasInitialVal ? this.$__.savedState[path] : priorVal, val)) {
this.unmarkModified(path);
} else {
this.markModified(path);
@@ -1244,7 +1245,9 @@ Document.prototype.$set = function $set(path, val, type, options) {
}
let schema;
- const parts = path.indexOf('.') === -1 ? [path] : path.split('.');
+ if (parts == null) {
+ parts = path.indexOf('.') === -1 ? [path] : path.split('.');
+ }
// Might need to change path for top-level alias
if (typeof this.$__schema.aliases[parts[0]] === 'string') {
@@ -1273,6 +1276,11 @@ Document.prototype.$set = function $set(path, val, type, options) {
// allow changes to sub paths of mixed types
mixed = true;
break;
+ } else if (schema.$isSchemaMap && schema.$__schemaType instanceof MixedSchema && i < parts.length - 1) {
+ // Map of mixed and not the last element in the path resolves to mixed
+ mixed = true;
+ schema = schema.$__schemaType;
+ break;
}
}
@@ -1379,7 +1387,9 @@ Document.prototype.$set = function $set(path, val, type, options) {
const model = val.constructor;
// Check ref
- const ref = schema.options.ref;
+ const refOpt = typeof schema.options.ref === 'function' && !schema.options.ref[modelSymbol] ? schema.options.ref.call(this, this) : schema.options.ref;
+
+ const ref = refOpt?.modelName || refOpt;
if (ref != null && (ref === model.modelName || ref === model.baseModelName)) {
return true;
}
@@ -1394,8 +1404,8 @@ Document.prototype.$set = function $set(path, val, type, options) {
})();
let didPopulate = false;
- 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;
+ if (refMatches && val instanceof Document && (!val.$__.wasPopulated || utils.deepEqual(val.$__.wasPopulated.value, val._doc._id))) {
+ const unpopulatedValue = (schema && schema.$isSingleNested) ? schema.cast(val, this) : val._doc._id;
this.$populated(path, unpopulatedValue, { [populateModelSymbol]: val.constructor });
val.$__.wasPopulated = { value: unpopulatedValue };
didPopulate = true;
@@ -1406,26 +1416,31 @@ Document.prototype.$set = function $set(path, val, type, options) {
if (schema.options &&
Array.isArray(schema.options[typeKey]) &&
schema.options[typeKey].length &&
+ schema.options[typeKey][0] &&
schema.options[typeKey][0].ref &&
_isManuallyPopulatedArray(val, schema.options[typeKey][0].ref)) {
popOpts = { [populateModelSymbol]: val[0].constructor };
- this.$populated(path, val.map(function(v) { return v._id; }), popOpts);
+ this.$populated(path, val.map(function(v) { return v._doc._id; }), popOpts);
for (const doc of val) {
- doc.$__.wasPopulated = { value: doc._id };
+ doc.$__.wasPopulated = { value: doc._doc._id };
}
didPopulate = true;
}
- if (this.$__schema.singleNestedPaths[path] == null && (!refMatches || !schema.$isSingleNested || !val.$__)) {
+ if (!refMatches || !schema.$isSingleNested || !val.$__) {
// If this path is underneath a single nested schema, we'll call the setter
// later in `$__set()` because we don't take `_doc` when we iterate through
// a single nested doc. That's to make sure we get the correct context.
// Otherwise we would double-call the setter, see gh-7196.
+ let setterContext = this;
+ if (this.$__schema.singleNestedPaths[path] != null && parts.length > 1) {
+ setterContext = getDeepestSubdocumentForPath(this, parts, this.schema);
+ }
if (options != null && options.overwriteImmutable) {
- val = schema.applySetters(val, this, false, priorVal, { overwriteImmutable: true });
+ val = schema.applySetters(val, setterContext, false, priorVal, { overwriteImmutable: true });
} else {
- val = schema.applySetters(val, this, false, priorVal);
+ val = schema.applySetters(val, setterContext, false, priorVal);
}
}
@@ -1451,7 +1466,7 @@ Document.prototype.$set = function $set(path, val, type, options) {
if (Array.isArray(val) && this.$__.populated[path]) {
for (let i = 0; i < val.length; ++i) {
if (val[i] instanceof Document) {
- val.set(i, val[i]._id, true);
+ val.set(i, val[i]._doc._id, true);
}
}
}
@@ -1491,7 +1506,16 @@ Document.prototype.$set = function $set(path, val, type, options) {
this.$__set(pathToMark, path, options, constructing, parts, schema, val, priorVal);
- if (savedState != null && savedState.hasOwnProperty(savedStatePath) && utils.deepEqual(val, savedState[savedStatePath])) {
+ const isInTransaction = !!this.$__.session?.transaction;
+ const isModifiedWithinTransaction = this.$__.session &&
+ this.$__.session[sessionNewDocuments] &&
+ this.$__.session[sessionNewDocuments].has(this) &&
+ this.$__.session[sessionNewDocuments].get(this).modifiedPaths &&
+ !this.$__.session[sessionNewDocuments].get(this).modifiedPaths.has(savedStatePath);
+ if (savedState != null &&
+ savedState.hasOwnProperty(savedStatePath) &&
+ (!isInTransaction || isModifiedWithinTransaction) &&
+ utils.deepEqual(val, savedState[savedStatePath])) {
this.unmarkModified(path);
}
}
@@ -1533,7 +1557,7 @@ function _isManuallyPopulatedArray(val, ref) {
/**
* Sets the value of a path, or many paths.
- * Alias for [`.$set`](#document_Document-$set).
+ * Alias for [`.$set`](https://mongoosejs.com/docs/api/document.html#Document.prototype.$set()).
*
* #### Example:
*
@@ -1600,13 +1624,6 @@ Document.prototype.$__shouldModify = function(pathToMark, path, options, constru
return true;
}
- // Re: the note about gh-7196, `val` is the raw value without casting or
- // setters if the full path is under a single nested subdoc because we don't
- // want to double run setters. So don't set it as modified. See gh-7264.
- if (this.$__schema.singleNestedPaths[path] != null) {
- return false;
- }
-
if (val === void 0 && !this.$__isSelected(path)) {
// when a path is not selected in a query, its initial
// value will be undefined.
@@ -1622,7 +1639,7 @@ Document.prototype.$__shouldModify = function(pathToMark, path, options, constru
// if they have the same _id
if (this.$populated(path) &&
val instanceof Document &&
- deepEqual(val._id, priorVal)) {
+ deepEqual(val._doc._id, priorVal)) {
return false;
}
@@ -1660,7 +1677,7 @@ Document.prototype.$__shouldModify = function(pathToMark, path, options, constru
*/
Document.prototype.$__set = function(pathToMark, path, options, constructing, parts, schema, val, priorVal) {
- Embedded = Embedded || require('./types/ArraySubdocument');
+ Embedded = Embedded || require('./types/arraySubdocument');
const shouldModify = this.$__shouldModify(pathToMark, path, options, constructing, parts,
schema, val, priorVal);
@@ -1690,7 +1707,11 @@ Document.prototype.$__set = function(pathToMark, path, options, constructing, pa
val[arrayAtomicsSymbol] = priorVal[arrayAtomicsSymbol];
val[arrayAtomicsBackupSymbol] = priorVal[arrayAtomicsBackupSymbol];
if (utils.isMongooseDocumentArray(val)) {
- val.forEach(doc => { doc.isNew = false; });
+ val.forEach(doc => {
+ if (doc != null) {
+ doc.$isNew = false;
+ }
+ });
}
}
@@ -1710,21 +1731,40 @@ Document.prototype.$__set = function(pathToMark, path, options, constructing, pa
if (last) {
if (obj instanceof Map) {
obj.set(parts[i], val);
+ } else if (obj.$isSingleNested) {
+ if (!(parts[i] in obj)) {
+ obj[parts[i]] = val;
+ obj._doc[parts[i]] = val;
+ } else {
+ obj._doc[parts[i]] = val;
+ }
+ if (shouldModify) {
+ obj.markModified(parts[i]);
+ }
} else {
obj[parts[i]] = val;
}
} else {
- if (utils.isPOJO(obj[parts[i]])) {
- obj = obj[parts[i]];
- } else if (obj[parts[i]] && obj[parts[i]] instanceof Embedded) {
- obj = obj[parts[i]];
- } else if (obj[parts[i]] && !Array.isArray(obj[parts[i]]) && obj[parts[i]].$isSingleNested) {
- obj = obj[parts[i]];
- } else if (obj[parts[i]] && Array.isArray(obj[parts[i]])) {
- obj = obj[parts[i]];
+ const isMap = obj instanceof Map;
+ let value = isMap ? obj.get(parts[i]) : obj[parts[i]];
+ if (utils.isPOJO(value)) {
+ obj = value;
+ } else if (value && value instanceof Embedded) {
+ obj = value;
+ } else if (value && !Array.isArray(value) && value.$isSingleNested) {
+ obj = value;
+ } else if (value && Array.isArray(value)) {
+ obj = value;
+ } else if (value == null) {
+ value = {};
+ if (isMap) {
+ obj.set(parts[i], value);
+ } else {
+ obj[parts[i]] = value;
+ }
+ obj = value;
} else {
- obj[parts[i]] = obj[parts[i]] || {};
- obj = obj[parts[i]];
+ obj = value;
}
}
}
@@ -1739,6 +1779,11 @@ Document.prototype.$__set = function(pathToMark, path, options, constructing, pa
*/
Document.prototype.$__getValue = function(path) {
+ if (typeof path !== 'string' && !Array.isArray(path)) {
+ throw new TypeError(
+ `Invalid \`path\`. Must be either string or array. Got "${path}" (type ${typeof path})`
+ );
+ }
return utils.getValue(path, this._doc);
};
@@ -1922,7 +1967,7 @@ Document.prototype.get = function(path, type, options) {
obj = schema.applyGetters(obj, this);
} else if (this.$__schema.nested[path] && options.virtuals) {
// Might need to apply virtuals if this is a nested path
- return applyVirtuals(this, utils.clone(obj) || {}, { path: path });
+ return applyVirtuals(this, clone(obj) || {}, { path: path });
}
return obj;
@@ -1993,7 +2038,7 @@ Document.prototype.$__saveInitialState = function $__saveInitialState(path) {
const firstDot = savedStatePath.indexOf('.');
const topLevelPath = firstDot === -1 ? savedStatePath : savedStatePath.slice(0, firstDot);
if (!savedState.hasOwnProperty(topLevelPath)) {
- savedState[topLevelPath] = utils.clone(this.$__getValue(topLevelPath));
+ savedState[topLevelPath] = clone(this.$__getValue(topLevelPath));
}
}
};
@@ -2068,7 +2113,7 @@ Document.prototype.directModifiedPaths = function() {
/**
* Returns true if the given path is nullish or only contains empty objects.
* Useful for determining whether this subdoc will get stripped out by the
- * [minimize option](/docs/guide.html#minimize).
+ * [minimize option](https://mongoosejs.com/docs/guide.html#minimize).
*
* #### Example:
*
@@ -2219,22 +2264,26 @@ Document.prototype[documentModifiedPaths] = Document.prototype.modifiedPaths;
* doc.isDirectModified('documents') // false
*
* @param {String} [path] optional
+ * @param {Object} [options]
+ * @param {Boolean} [options.ignoreAtomics=false] If true, doesn't return true if path is underneath an array that was modified with atomic operations like `push()`
* @return {Boolean}
* @api public
*/
-Document.prototype.isModified = function(paths, modifiedPaths) {
+Document.prototype.isModified = function(paths, options, modifiedPaths) {
if (paths) {
- if (!Array.isArray(paths)) {
- paths = paths.indexOf(' ') === -1 ? [paths] : paths.split(' ');
- }
-
+ const ignoreAtomics = options && options.ignoreAtomics;
const directModifiedPathsObj = this.$__.activePaths.states.modify;
if (directModifiedPathsObj == null) {
return false;
}
+
+ if (typeof paths === 'string') {
+ paths = paths.indexOf(' ') === -1 ? [paths] : paths.split(' ');
+ }
+
for (const path of paths) {
- if (Object.prototype.hasOwnProperty.call(directModifiedPathsObj, path)) {
+ if (directModifiedPathsObj[path] != null) {
return true;
}
}
@@ -2244,7 +2293,16 @@ Document.prototype.isModified = function(paths, modifiedPaths) {
return !!~modified.indexOf(path);
});
- const directModifiedPaths = Object.keys(directModifiedPathsObj);
+ let directModifiedPaths = Object.keys(directModifiedPathsObj);
+ if (ignoreAtomics) {
+ directModifiedPaths = directModifiedPaths.filter(path => {
+ const value = this.$__getValue(path);
+ if (value != null && value[arrayAtomicsSymbol] != null && value[arrayAtomicsSymbol].$set === undefined) {
+ return false;
+ }
+ return true;
+ });
+ }
return isModifiedChild || paths.some(function(path) {
return directModifiedPaths.some(function(mod) {
return mod === path || path.startsWith(mod + '.');
@@ -2256,7 +2314,7 @@ Document.prototype.isModified = function(paths, modifiedPaths) {
};
/**
- * Alias of [`.isModified`](#document_Document-isModified)
+ * Alias of [`.isModified`](https://mongoosejs.com/docs/api/document.html#Document.prototype.isModified())
*
* @method $isModified
* @memberOf Document
@@ -2302,17 +2360,17 @@ Document.prototype.$isDefault = function(path) {
};
/**
- * Getter/setter, determines whether the document was removed or not.
+ * Getter/setter, determines whether the document was deleted. The `Model.prototype.deleteOne()` method sets `$isDeleted` if the delete operation succeeded.
*
* #### Example:
*
- * const product = await product.remove();
+ * const product = await product.deleteOne();
* product.$isDeleted(); // true
- * product.remove(); // no-op, doesn't send anything to the db
+ * product.deleteOne(); // no-op, doesn't send anything to the db
*
* product.$isDeleted(false);
* product.$isDeleted(); // false
- * product.remove(); // will execute a remove against the db
+ * product.deleteOne(); // will execute a remove against the db
*
*
* @param {Boolean} [val] optional, overrides whether mongoose thinks the doc is deleted
@@ -2352,15 +2410,29 @@ Document.prototype.isDirectModified = function(path) {
}
if (typeof path === 'string' && path.indexOf(' ') === -1) {
- return this.$__.activePaths.getStatePaths('modify').hasOwnProperty(path);
+ const res = this.$__.activePaths.getStatePaths('modify').hasOwnProperty(path);
+ if (res || path.indexOf('.') === -1) {
+ return res;
+ }
+
+ const pieces = path.split('.');
+ for (let i = 0; i < pieces.length - 1; ++i) {
+ const subpath = pieces.slice(0, i + 1).join('.');
+ const subdoc = this.$get(subpath);
+ if (subdoc != null && subdoc.$__ != null && subdoc.isDirectModified(pieces.slice(i + 1).join('.'))) {
+ return true;
+ }
+ }
+
+ return false;
}
let paths = path;
- if (!Array.isArray(paths)) {
+ if (typeof paths === 'string') {
paths = paths.split(' ');
}
- return paths.some(path => this.$__.activePaths.getStatePaths('modify').hasOwnProperty(path));
+ return paths.some(path => this.isDirectModified(path));
};
/**
@@ -2462,7 +2534,6 @@ Document.prototype.isSelected = function isSelected(path) {
return inclusive;
}
}
-
return !inclusive;
};
@@ -2536,78 +2607,68 @@ Document.prototype.isDirectSelected = function isDirectSelected(path) {
*
* #### Note:
*
- * This method is called `pre` save and if a validation rule is violated, [save](#model_Model-save) is aborted and the error is returned to your `callback`.
+ * This method is called `pre` save and if a validation rule is violated, [save](https://mongoosejs.com/docs/api/model.html#Model.prototype.save()) is aborted and the error is thrown.
*
* #### Example:
*
- * doc.validate(function (err) {
- * if (err) handleError(err);
- * else // validation passed
- * });
+ * await doc.validate({ validateModifiedOnly: false, pathsToSkip: ['name', 'email']});
*
* @param {Array|String} [pathsToValidate] list of paths to validate. If set, Mongoose will validate only the modified paths that are in the given list.
* @param {Object} [options] internal options
* @param {Boolean} [options.validateModifiedOnly=false] if `true` mongoose validates only modified paths.
* @param {Array|string} [options.pathsToSkip] list of paths to skip. If set, Mongoose will validate every modified path that is not in this list.
- * @param {Function} [callback] optional callback called after validation completes, passing an error if one occurred
- * @return {Promise} Returns a Promise if no `callback` is given.
+ * @return {Promise} Returns a Promise.
* @api public
*/
-Document.prototype.validate = function(pathsToValidate, options, callback) {
+Document.prototype.validate = async function validate(pathsToValidate, options) {
+ if (typeof pathsToValidate === 'function' || typeof options === 'function' || typeof arguments[2] === 'function') {
+ throw new MongooseError('Document.prototype.validate() no longer accepts a callback');
+ }
let parallelValidate;
this.$op = 'validate';
- if (this.$isSubdocument != null) {
- // Skip parallel validate check for subdocuments
- } else if (this.$__.validating) {
- parallelValidate = new ParallelValidateError(this, {
- parentStack: options && options.parentStack,
- conflictStack: this.$__.validating.stack
- });
- } else {
- this.$__.validating = new ParallelValidateError(this, { parentStack: options && options.parentStack });
- }
-
if (arguments.length === 1) {
if (typeof arguments[0] === 'object' && !Array.isArray(arguments[0])) {
options = arguments[0];
- callback = null;
- pathsToValidate = null;
- } else if (typeof arguments[0] === 'function') {
- callback = arguments[0];
- options = null;
pathsToValidate = null;
}
- } else if (typeof pathsToValidate === 'function') {
- callback = pathsToValidate;
- options = null;
- pathsToValidate = null;
- } else if (typeof options === 'function') {
- callback = options;
- options = pathsToValidate;
- pathsToValidate = null;
}
if (options && typeof options.pathsToSkip === 'string') {
const isOnePathOnly = options.pathsToSkip.indexOf(' ') === -1;
options.pathsToSkip = isOnePathOnly ? [options.pathsToSkip] : options.pathsToSkip.split(' ');
}
+ const _skipParallelValidateCheck = options && options._skipParallelValidateCheck;
- return promiseOrCallback(callback, cb => {
- if (parallelValidate != null) {
- return cb(parallelValidate);
- }
+ if (this.$isSubdocument != null) {
+ // Skip parallel validate check for subdocuments
+ } else if (this.$__.validating && !_skipParallelValidateCheck) {
+ parallelValidate = new ParallelValidateError(this, {
+ parentStack: options && options.parentStack,
+ conflictStack: this.$__.validating.stack
+ });
+ } else if (!_skipParallelValidateCheck) {
+ this.$__.validating = new ParallelValidateError(this, { parentStack: options && options.parentStack });
+ }
+
+ if (parallelValidate != null) {
+ throw parallelValidate;
+ }
+ return new Promise((resolve, reject) => {
this.$__validate(pathsToValidate, options, (error) => {
this.$op = null;
this.$__.validating = null;
- cb(error);
+ if (error != null) {
+ return reject(error);
+ }
+ resolve();
});
- }, this.constructor.events);
+ });
};
/**
- * Alias of [`.validate`](#document_Document-validate)
+ * Alias of [`.validate`](https://mongoosejs.com/docs/api/document.html#Document.prototype.validate())
*
* @method $validate
* @memberOf Document
@@ -2644,8 +2705,8 @@ function _evaluateRequiredFunctions(doc) {
* ignore
*/
-function _getPathsToValidate(doc) {
- const skipSchemaValidators = {};
+function _getPathsToValidate(doc, pathsToValidate, pathsToSkip, isNestedValidate) {
+ const doValidateOptions = {};
_evaluateRequiredFunctions(doc);
// only validate required fields when necessary
@@ -2664,26 +2725,41 @@ function _getPathsToValidate(doc) {
Object.keys(doc.$__.activePaths.getStatePaths('default')).forEach(addToPaths);
function addToPaths(p) { paths.add(p); }
- const subdocs = doc.$getAllSubdocs();
- const modifiedPaths = doc.modifiedPaths();
- for (const subdoc of subdocs) {
- if (subdoc.$basePath) {
- // Remove child paths for now, because we'll be validating the whole
- // subdoc
- const fullPathToSubdoc = subdoc.$__fullPathWithIndexes();
-
- for (const p of paths) {
- if (p == null || p.startsWith(fullPathToSubdoc + '.')) {
- paths.delete(p);
+ if (!isNestedValidate) {
+ // If we're validating a subdocument, all this logic will run anyway on the top-level document, so skip for subdocuments
+ const subdocs = doc.$getAllSubdocs({ useCache: true });
+ const modifiedPaths = doc.modifiedPaths();
+ for (const subdoc of subdocs) {
+ if (subdoc.$basePath) {
+ const fullPathToSubdoc = subdoc.$isSingleNested ? subdoc.$__pathRelativeToParent() : subdoc.$__fullPathWithIndexes();
+
+ // Remove child paths for now, because we'll be validating the whole
+ // subdoc.
+ // The following is a faster take on looping through every path in `paths`
+ // and checking if the path starts with `fullPathToSubdoc` re: gh-13191
+ for (const modifiedPath of subdoc.modifiedPaths()) {
+ paths.delete(fullPathToSubdoc + '.' + modifiedPath);
}
- }
- if (doc.$isModified(fullPathToSubdoc, modifiedPaths) &&
- !doc.isDirectModified(fullPathToSubdoc) &&
- !doc.$isDefault(fullPathToSubdoc)) {
- paths.add(fullPathToSubdoc);
+ if (doc.$isModified(fullPathToSubdoc, null, modifiedPaths) &&
+ // Avoid using isDirectModified() here because that does additional checks on whether the parent path
+ // is direct modified, which can cause performance issues re: gh-14897
+ !doc.$__.activePaths.getStatePaths('modify').hasOwnProperty(fullPathToSubdoc) &&
+ !doc.$isDefault(fullPathToSubdoc)) {
+ paths.add(fullPathToSubdoc);
+
+ if (doc.$__.pathsToScopes == null) {
+ doc.$__.pathsToScopes = {};
+ }
+ doc.$__.pathsToScopes[fullPathToSubdoc] = subdoc.$isDocumentArrayElement ?
+ subdoc.__parentArray :
+ subdoc.$parent();
- skipSchemaValidators[fullPathToSubdoc] = true;
+ doValidateOptions[fullPathToSubdoc] = { skipSchemaValidators: true };
+ if (subdoc.$isDocumentArrayElement && subdoc.__index != null) {
+ doValidateOptions[fullPathToSubdoc].index = subdoc.__index;
+ }
+ }
}
}
}
@@ -2704,7 +2780,7 @@ function _getPathsToValidate(doc) {
// Optimization: if primitive path with no validators, or array of primitives
// with no validators, skip validating this path entirely.
- if (!_pathType.caster && _pathType.validators.length === 0) {
+ if (!_pathType.caster && _pathType.validators.length === 0 && !_pathType.$parentSchemaDocArray) {
paths.delete(path);
} else if (_pathType.$isMongooseArray &&
!_pathType.$isMongooseDocumentArray && // Skip document arrays...
@@ -2715,10 +2791,66 @@ function _getPathsToValidate(doc) {
}
}
+
+ if (Array.isArray(pathsToValidate)) {
+ paths = _handlePathsToValidate(paths, pathsToValidate);
+ } else if (Array.isArray(pathsToSkip)) {
+ paths = _handlePathsToSkip(paths, pathsToSkip);
+ }
+
// from here on we're not removing items from paths
// gh-661: if a whole array is modified, make sure to run validation on all
// the children as well
+ _addArrayPathsToValidate(doc, paths);
+
+ const flattenOptions = { skipArrays: true };
+ for (const pathToCheck of paths) {
+ if (doc.$__schema.nested[pathToCheck]) {
+ let _v = doc.$__getValue(pathToCheck);
+ if (isMongooseObject(_v)) {
+ _v = _v.toObject({ transform: false });
+ }
+ const flat = flatten(_v, pathToCheck, flattenOptions, doc.$__schema);
+ // Single nested paths (paths embedded under single nested subdocs) will
+ // be validated on their own when we call `validate()` on the subdoc itself.
+ // Re: gh-8468
+ Object.keys(flat).filter(path => !doc.$__schema.singleNestedPaths.hasOwnProperty(path)).forEach(addToPaths);
+ }
+ }
+
+ for (const path of paths) {
+ const _pathType = doc.$__schema.path(path);
+
+ if (!_pathType) {
+ continue;
+ }
+
+ // If underneath a document array, may need to re-validate the parent
+ // array re: gh-6818. Do this _after_ adding subpaths, because
+ // we don't want to add every array subpath.
+ if (_pathType.$parentSchemaDocArray && typeof _pathType.$parentSchemaDocArray.path === 'string') {
+ paths.add(_pathType.$parentSchemaDocArray.path);
+ }
+
+ if (!_pathType.$isSchemaMap) {
+ continue;
+ }
+
+ const val = doc.$__getValue(path);
+ if (val == null) {
+ continue;
+ }
+ for (const key of val.keys()) {
+ paths.add(path + '.' + key);
+ }
+ }
+
+ paths = Array.from(paths);
+ return [paths, doValidateOptions];
+}
+
+function _addArrayPathsToValidate(doc, paths) {
for (const path of paths) {
const _pathType = doc.$__schema.path(path);
if (!_pathType) {
@@ -2747,56 +2879,19 @@ function _getPathsToValidate(doc) {
const val = doc.$__getValue(path);
_pushNestedArrayPaths(val, paths, path);
}
+}
- function _pushNestedArrayPaths(val, paths, path) {
- if (val != null) {
- const numElements = val.length;
- for (let j = 0; j < numElements; ++j) {
- if (Array.isArray(val[j])) {
- _pushNestedArrayPaths(val[j], paths, path + '.' + j);
- } else {
- paths.add(path + '.' + j);
- }
- }
- }
- }
-
- const flattenOptions = { skipArrays: true };
- for (const pathToCheck of paths) {
- if (doc.$__schema.nested[pathToCheck]) {
- let _v = doc.$__getValue(pathToCheck);
- if (isMongooseObject(_v)) {
- _v = _v.toObject({ transform: false });
+function _pushNestedArrayPaths(val, paths, path) {
+ if (val != null) {
+ const numElements = val.length;
+ for (let j = 0; j < numElements; ++j) {
+ if (Array.isArray(val[j])) {
+ _pushNestedArrayPaths(val[j], paths, path + '.' + j);
+ } else {
+ paths.add(path + '.' + j);
}
- const flat = flatten(_v, pathToCheck, flattenOptions, doc.$__schema);
- Object.keys(flat).forEach(addToPaths);
- }
- }
-
- for (const path of paths) {
- // Single nested paths (paths embedded under single nested subdocs) will
- // be validated on their own when we call `validate()` on the subdoc itself.
- // Re: gh-8468
- if (doc.$__schema.singleNestedPaths.hasOwnProperty(path)) {
- paths.delete(path);
- continue;
- }
- const _pathType = doc.$__schema.path(path);
- if (!_pathType || !_pathType.$isSchemaMap) {
- continue;
- }
-
- const val = doc.$__getValue(path);
- if (val == null) {
- continue;
- }
- for (const key of val.keys()) {
- paths.add(path + '.' + key);
}
}
-
- paths = Array.from(paths);
- return [paths, skipSchemaValidators];
}
/*!
@@ -2804,7 +2899,9 @@ function _getPathsToValidate(doc) {
*/
Document.prototype.$__validate = function(pathsToValidate, options, callback) {
- if (typeof pathsToValidate === 'function') {
+ if (this.$__.saveOptions && this.$__.saveOptions.pathsToSave && !pathsToValidate) {
+ pathsToValidate = [...this.$__.saveOptions.pathsToSave];
+ } else if (typeof pathsToValidate === 'function') {
callback = pathsToValidate;
options = null;
pathsToValidate = null;
@@ -2826,6 +2923,19 @@ Document.prototype.$__validate = function(pathsToValidate, options, callback) {
shouldValidateModifiedOnly = this.$__schema.options.validateModifiedOnly;
}
+ const validateAllPaths = options && options.validateAllPaths;
+ if (validateAllPaths) {
+ if (pathsToSkip) {
+ throw new TypeError('Cannot set both `validateAllPaths` and `pathsToSkip`');
+ }
+ if (pathsToValidate) {
+ throw new TypeError('Cannot set both `validateAllPaths` and `pathsToValidate`');
+ }
+ if (hasValidateModifiedOnlyOption && shouldValidateModifiedOnly) {
+ throw new TypeError('Cannot set both `validateAllPaths` and `validateModifiedOnly`');
+ }
+ }
+
const _this = this;
const _complete = () => {
let validationError = this.$__.validationError;
@@ -2863,19 +2973,36 @@ Document.prototype.$__validate = function(pathsToValidate, options, callback) {
};
// only validate required fields when necessary
- const pathDetails = _getPathsToValidate(this);
- let paths = shouldValidateModifiedOnly ?
- pathDetails[0].filter((path) => this.$isModified(path)) :
- pathDetails[0];
- const skipSchemaValidators = pathDetails[1];
+ let paths;
+ let doValidateOptionsByPath;
+ if (validateAllPaths) {
+ paths = new Set(Object.keys(this.$__schema.paths));
+ // gh-661: if a whole array is modified, make sure to run validation on all
+ // the children as well
+ for (const path of paths) {
+ const schemaType = this.$__schema.path(path);
+ if (!schemaType || !schemaType.$isMongooseArray) {
+ continue;
+ }
+ const val = this.$__getValue(path);
+ if (!val) {
+ continue;
+ }
+ _pushNestedArrayPaths(val, paths, path);
+ }
+ paths = [...paths];
+ doValidateOptionsByPath = {};
+ } else {
+ const pathDetails = _getPathsToValidate(this, pathsToValidate, pathsToSkip, options && options._nestedValidate);
+ paths = shouldValidateModifiedOnly ?
+ pathDetails[0].filter((path) => this.$isModified(path)) :
+ pathDetails[0];
+ doValidateOptionsByPath = pathDetails[1];
+ }
+
if (typeof pathsToValidate === 'string') {
pathsToValidate = pathsToValidate.split(' ');
}
- if (Array.isArray(pathsToValidate)) {
- paths = _handlePathsToValidate(paths, pathsToValidate);
- } else if (pathsToSkip) {
- paths = _handlePathsToSkip(paths, pathsToSkip);
- }
if (paths.length === 0) {
return immediate(function() {
@@ -2892,8 +3019,19 @@ Document.prototype.$__validate = function(pathsToValidate, options, callback) {
const validated = {};
let total = 0;
- for (const path of paths) {
- validatePath(path);
+ let pathsToSave = this.$__.saveOptions?.pathsToSave;
+ if (Array.isArray(pathsToSave)) {
+ pathsToSave = new Set(pathsToSave);
+ for (const path of paths) {
+ if (!pathsToSave.has(path)) {
+ continue;
+ }
+ validatePath(path);
+ }
+ } else {
+ for (const path of paths) {
+ validatePath(path);
+ }
}
function validatePath(path) {
@@ -2933,23 +3071,24 @@ Document.prototype.$__validate = function(pathsToValidate, options, callback) {
} else if (val != null && val.$__ != null && val.$__.wasPopulated) {
// Array paths, like `somearray.1`, do not show up as populated with `$populated()`,
// so in that case pull out the document's id
- val = val._id;
+ val = val._doc._id;
}
const scope = _this.$__.pathsToScopes != null && path in _this.$__.pathsToScopes ?
_this.$__.pathsToScopes[path] :
_this;
const doValidateOptions = {
- skipSchemaValidators: skipSchemaValidators[path],
+ ...doValidateOptionsByPath[path],
path: path,
- validateModifiedOnly: shouldValidateModifiedOnly
+ validateAllPaths,
+ _nestedValidate: true
};
schemaType.doValidate(val, function(err) {
if (err) {
const isSubdoc = schemaType.$isSingleNested ||
- schemaType.$isArraySubdocument ||
- schemaType.$isMongooseDocumentArray;
+ schemaType.$isArraySubdocument ||
+ schemaType.$isMongooseDocumentArray;
if (isSubdoc && err instanceof ValidationError) {
return --total || complete();
}
@@ -2994,12 +3133,12 @@ function _handlePathsToValidate(paths, pathsToValidate) {
}
}
- const ret = [];
+ const ret = new Set();
for (const path of paths) {
if (_pathsToValidate.has(path)) {
- ret.push(path);
+ ret.add(path);
} else if (parentPaths.has(path)) {
- ret.push(parentPaths.get(path));
+ ret.add(parentPaths.get(path));
}
}
return ret;
@@ -3011,8 +3150,8 @@ function _handlePathsToValidate(paths, pathsToValidate) {
function _handlePathsToSkip(paths, pathsToSkip) {
pathsToSkip = new Set(pathsToSkip);
- paths = paths.filter(p => !pathsToSkip.has(p));
- return paths;
+ paths = Array.from(paths).filter(p => !pathsToSkip.has(p));
+ return new Set(paths);
}
/**
@@ -3060,6 +3199,16 @@ Document.prototype.validateSync = function(pathsToValidate, options) {
let pathsToSkip = options && options.pathsToSkip;
+ const validateAllPaths = options && options.validateAllPaths;
+ if (validateAllPaths) {
+ if (pathsToSkip) {
+ throw new TypeError('Cannot set both `validateAllPaths` and `pathsToSkip`');
+ }
+ if (pathsToValidate) {
+ throw new TypeError('Cannot set both `validateAllPaths` and `pathsToValidate`');
+ }
+ }
+
if (typeof pathsToValidate === 'string') {
const isOnePathOnly = pathsToValidate.indexOf(' ') === -1;
pathsToValidate = isOnePathOnly ? [pathsToValidate] : pathsToValidate.split(' ');
@@ -3068,17 +3217,33 @@ Document.prototype.validateSync = function(pathsToValidate, options) {
}
// only validate required fields when necessary
- const pathDetails = _getPathsToValidate(this);
- let paths = shouldValidateModifiedOnly ?
- pathDetails[0].filter((path) => this.$isModified(path)) :
- pathDetails[0];
- const skipSchemaValidators = pathDetails[1];
-
- if (Array.isArray(pathsToValidate)) {
- paths = _handlePathsToValidate(paths, pathsToValidate);
- } else if (Array.isArray(pathsToSkip)) {
- paths = _handlePathsToSkip(paths, pathsToSkip);
+ let paths;
+ let skipSchemaValidators;
+ if (validateAllPaths) {
+ paths = new Set(Object.keys(this.$__schema.paths));
+ // gh-661: if a whole array is modified, make sure to run validation on all
+ // the children as well
+ for (const path of paths) {
+ const schemaType = this.$__schema.path(path);
+ if (!schemaType || !schemaType.$isMongooseArray) {
+ continue;
+ }
+ const val = this.$__getValue(path);
+ if (!val) {
+ continue;
+ }
+ _pushNestedArrayPaths(val, paths, path);
+ }
+ paths = [...paths];
+ skipSchemaValidators = {};
+ } else {
+ const pathDetails = _getPathsToValidate(this, pathsToValidate, pathsToSkip);
+ paths = shouldValidateModifiedOnly ?
+ pathDetails[0].filter((path) => this.$isModified(path)) :
+ pathDetails[0];
+ skipSchemaValidators = pathDetails[1];
}
+
const validating = {};
for (let i = 0, len = paths.length; i < len; ++i) {
@@ -3102,7 +3267,8 @@ Document.prototype.validateSync = function(pathsToValidate, options) {
const err = p.doValidateSync(val, _this, {
skipSchemaValidators: skipSchemaValidators[path],
path: path,
- validateModifiedOnly: shouldValidateModifiedOnly
+ validateModifiedOnly: shouldValidateModifiedOnly,
+ validateAllPaths
});
if (err) {
const isSubdoc = p.$isSingleNested ||
@@ -3254,8 +3420,8 @@ function _checkImmutableSubpaths(subdoc, schematype, priorVal) {
}
/**
- * Saves this document by inserting a new document into the database if [document.isNew](#document_Document-isNew) is `true`,
- * or sends an [updateOne](#document_Document-updateOne) operation **only** with the modifications to the database, it does not replace the whole document in the latter case.
+ * Saves this document by inserting a new document into the database if [document.isNew](https://mongoosejs.com/docs/api/document.html#Document.prototype.isNew()) is `true`,
+ * or sends an [updateOne](https://mongoosejs.com/docs/api/document.html#Document.prototype.updateOne()) operation **only** with the modifications to the database, it does not replace the whole document in the latter case.
*
* #### Example:
*
@@ -3271,21 +3437,20 @@ function _checkImmutableSubpaths(subdoc, schematype, priorVal) {
* newProduct === product; // true
*
* @param {Object} [options] options optional options
- * @param {Session} [options.session=null] the [session](https://www.mongodb.com/docs/manual/reference/server-sessions/) associated with this save operation. If not specified, defaults to the [document's associated session](#document_Document-$session).
- * @param {Object} [options.safe] (DEPRECATED) overrides [schema's safe option](https://mongoosejs.com//docs/guide.html#safe). Use the `w` option instead.
+ * @param {Session} [options.session=null] the [session](https://www.mongodb.com/docs/manual/reference/server-sessions/) associated with this save operation. If not specified, defaults to the [document's associated session](https://mongoosejs.com/docs/api/document.html#Document.prototype.$session()).
+ * @param {Object} [options.safe] (DEPRECATED) overrides [schema's safe option](https://mongoosejs.com/docs/guide.html#safe). Use the `w` option instead.
* @param {Boolean} [options.validateBeforeSave] set to false to save without validating.
* @param {Boolean} [options.validateModifiedOnly=false] If `true`, Mongoose will only validate modified paths, as opposed to modified paths and `required` paths.
- * @param {Number|String} [options.w] set the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/#w-option). Overrides the [schema-level `writeConcern` option](/docs/guide.html#writeConcern)
- * @param {Boolean} [options.j] set to true for MongoDB to wait until this `save()` has been [journaled before resolving the returned promise](https://www.mongodb.com/docs/manual/reference/write-concern/#j-option). Overrides the [schema-level `writeConcern` option](/docs/guide.html#writeConcern)
- * @param {Number} [options.wtimeout] sets a [timeout for the write concern](https://www.mongodb.com/docs/manual/reference/write-concern/#wtimeout). Overrides the [schema-level `writeConcern` option](/docs/guide.html#writeConcern).
+ * @param {Number|String} [options.w] set the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/#w-option). Overrides the [schema-level `writeConcern` option](https://mongoosejs.com/docs/guide.html#writeConcern)
+ * @param {Boolean} [options.j] set to true for MongoDB to wait until this `save()` has been [journaled before resolving the returned promise](https://www.mongodb.com/docs/manual/reference/write-concern/#j-option). Overrides the [schema-level `writeConcern` option](https://mongoosejs.com/docs/guide.html#writeConcern)
+ * @param {Number} [options.wtimeout] sets a [timeout for the write concern](https://www.mongodb.com/docs/manual/reference/write-concern/#wtimeout). Overrides the [schema-level `writeConcern` option](https://mongoosejs.com/docs/guide.html#writeConcern).
* @param {Boolean} [options.checkKeys=true] the MongoDB driver prevents you from saving keys that start with '$' or contain '.' by default. Set this option to `false` to skip that check. See [restrictions on field names](https://www.mongodb.com/docs/manual/reference/limits/#Restrictions-on-Field-Names)
- * @param {Boolean} [options.timestamps=true] if `false` and [timestamps](/docs/guide.html#timestamps) are enabled, skip timestamps for this `save()`.
- * @param {Function} [fn] optional callback
+ * @param {Boolean} [options.timestamps=true] if `false` and [timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this `save()`.
* @method save
* @memberOf Document
* @instance
- * @throws {DocumentNotFoundError} if this [save updates an existing document](#document_Document-isNew) but the document doesn't exist in the database. For example, you will get this error if the document is [deleted between when you retrieved the document and when you saved it](documents.html#updating).
- * @return {Promise|undefined} Returns undefined if used with callback or a Promise otherwise.
+ * @throws {DocumentNotFoundError} if this [save updates an existing document](https://mongoosejs.com/docs/api/document.html#Document.prototype.isNew()) but the document doesn't exist in the database. For example, you will get this error if the document is [deleted between when you retrieved the document and when you saved it](documents.html#updating).
+ * @return {Promise}
* @api public
* @see middleware https://mongoosejs.com/docs/middleware.html
*/
@@ -3332,44 +3497,11 @@ Document.prototype.$__reset = function reset() {
let _this = this;
// Skip for subdocuments
- const subdocs = this.$parent() === this ? this.$getAllSubdocs() : [];
- const resetArrays = new Set();
- for (const subdoc of subdocs) {
- const fullPathWithIndexes = subdoc.$__fullPathWithIndexes();
- if (this.isModified(fullPathWithIndexes) || isParentInit(fullPathWithIndexes)) {
+ const subdocs = !this.$isSubdocument ? this.$getAllSubdocs({ useCache: true }) : null;
+ if (subdocs && subdocs.length > 0) {
+ for (const subdoc of subdocs) {
subdoc.$__reset();
- if (subdoc.$isDocumentArrayElement) {
- if (!resetArrays.has(subdoc.parentArray())) {
- const array = subdoc.parentArray();
- this.$__.activePaths.clearPath(fullPathWithIndexes.replace(/\.\d+$/, '').slice(-subdoc.$basePath - 1));
- array[arrayAtomicsBackupSymbol] = array[arrayAtomicsSymbol];
- array[arrayAtomicsSymbol] = {};
-
- resetArrays.add(array);
- }
- } else {
- if (subdoc.$parent() === this) {
- this.$__.activePaths.clearPath(subdoc.$basePath);
- } else if (subdoc.$parent() != null && subdoc.$parent().$isSubdocument) {
- // If map path underneath subdocument, may end up with a case where
- // map path is modified but parent still needs to be reset. See gh-10295
- subdoc.$parent().$__reset();
- }
- }
- }
- }
-
- function isParentInit(path) {
- path = path.indexOf('.') === -1 ? [path] : path.split('.');
- let cur = '';
- for (let i = 0; i < path.length; ++i) {
- cur += (cur.length ? '.' : '') + path[i];
- if (_this.$__.activePaths[cur] === 'init') {
- return true;
- }
}
-
- return false;
}
// clear atomics
@@ -3534,7 +3666,7 @@ Document.prototype.$__setSchema = function(schema) {
*/
Document.prototype.$__getArrayPathsToValidate = function() {
- DocumentArray || (DocumentArray = require('./types/DocumentArray'));
+ DocumentArray || (DocumentArray = require('./types/documentArray'));
// validate all document arrays.
return this.$__.activePaths
@@ -3555,6 +3687,7 @@ Document.prototype.$__getArrayPathsToValidate = function() {
/**
* Get all subdocs (by bfs)
*
+ * @param {Object} [options] options. Currently for internal use.
* @return {Array}
* @api public
* @method $getAllSubdocs
@@ -3562,57 +3695,53 @@ Document.prototype.$__getArrayPathsToValidate = function() {
* @instance
*/
-Document.prototype.$getAllSubdocs = function() {
- DocumentArray || (DocumentArray = require('./types/DocumentArray'));
- Embedded = Embedded || require('./types/ArraySubdocument');
-
- function docReducer(doc, seed, path) {
- let val = doc;
- let isNested = false;
- if (path) {
- if (doc instanceof Document && doc[documentSchemaSymbol].paths[path]) {
- val = doc._doc[path];
- } else if (doc instanceof Document && doc[documentSchemaSymbol].nested[path]) {
- val = doc._doc[path];
- isNested = true;
- } else {
- val = doc[path];
- }
- }
- if (val instanceof Embedded) {
- seed.push(val);
- } else if (val instanceof Map) {
- seed = Array.from(val.keys()).reduce(function(seed, path) {
- return docReducer(val.get(path), seed, null);
- }, seed);
- } else if (val && !Array.isArray(val) && val.$isSingleNested) {
- seed = Object.keys(val._doc).reduce(function(seed, path) {
- return docReducer(val, seed, path);
- }, seed);
- seed.push(val);
- } else if (val && utils.isMongooseDocumentArray(val)) {
- val.forEach(function _docReduce(doc) {
- if (!doc || !doc._doc) {
- return;
+Document.prototype.$getAllSubdocs = function(options) {
+ if (options?.useCache && this.$__.saveOptions?.__subdocs) {
+ return this.$__.saveOptions.__subdocs;
+ }
+
+ DocumentArray || (DocumentArray = require('./types/documentArray'));
+ Embedded = Embedded || require('./types/arraySubdocument');
+
+ const subDocs = [];
+ function getSubdocs(doc) {
+ const newSubdocs = [];
+
+ for (const { model } of doc.$__schema.childSchemas) {
+ // Avoid using `childSchemas.path` to avoid compatibility versions with pre-8.8 versions of Mongoose
+ const val = doc.$__getValue(model.path);
+ if (val == null) {
+ continue;
+ }
+ if (val.$__) {
+ newSubdocs.push(val);
+ }
+ if (Array.isArray(val)) {
+ for (const el of val) {
+ if (el != null && el.$__) {
+ newSubdocs.push(el);
+ }
}
- seed = Object.keys(doc._doc).reduce(function(seed, path) {
- return docReducer(doc._doc, seed, path);
- }, seed);
- if (doc instanceof Embedded) {
- seed.push(doc);
+ }
+ if (val instanceof Map) {
+ for (const el of val.values()) {
+ if (el != null && el.$__) {
+ newSubdocs.push(el);
+ }
}
- });
- } else if (isNested && val != null) {
- for (const path of Object.keys(val)) {
- docReducer(val, seed, path);
}
}
- return seed;
+
+ for (const subdoc of newSubdocs) {
+ getSubdocs(subdoc);
+ }
+ subDocs.push(...newSubdocs);
}
- const subDocs = [];
- for (const path of Object.keys(this._doc)) {
- docReducer(this, subDocs, path);
+ getSubdocs(this);
+
+ if (this.$__.saveOptions) {
+ this.$__.saveOptions.__subdocs = subDocs;
}
return subDocs;
@@ -3649,7 +3778,7 @@ Document.prototype.$__handleReject = function handleReject(err) {
};
/**
- * Internal helper for toObject() and toJSON() that doesn't manipulate options
+ * Internal common logic for toObject() and toJSON()
*
* @return {Object}
* @api private
@@ -3659,21 +3788,9 @@ Document.prototype.$__handleReject = function handleReject(err) {
*/
Document.prototype.$toObject = function(options, json) {
- let defaultOptions = {
- transform: true,
- flattenDecimals: true
- };
+ const defaultOptions = this.$__schema._defaultToObjectOptions(json);
- const path = json ? 'toJSON' : 'toObject';
- const baseOptions = this.constructor &&
- 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.
- defaultOptions = utils.options(defaultOptions, clone(baseOptions));
- defaultOptions = utils.options(defaultOptions, clone(schemaOptions[path] || {}));
+ const hasOnlyPrimitiveValues = this.$__hasOnlyPrimitiveValues();
// If options do not exist or is not an object, set it to empty object
options = utils.isPOJO(options) ? { ...options } : {};
@@ -3682,83 +3799,92 @@ Document.prototype.$toObject = function(options, json) {
let _minimize;
if (options._calledWithOptions.minimize != null) {
_minimize = options.minimize;
- } else if (defaultOptions.minimize != null) {
+ } else if (defaultOptions != null && defaultOptions.minimize != null) {
_minimize = defaultOptions.minimize;
} else {
- _minimize = schemaOptions.minimize;
+ _minimize = this.$__schema.options.minimize;
}
- let flattenMaps;
- if (options._calledWithOptions.flattenMaps != null) {
- flattenMaps = options.flattenMaps;
- } else if (defaultOptions.flattenMaps != null) {
- flattenMaps = defaultOptions.flattenMaps;
- } else {
- flattenMaps = schemaOptions.flattenMaps;
- }
-
- // The original options that will be passed to `clone()`. Important because
- // `clone()` will recursively call `$toObject()` on embedded docs, so we
- // need the original options the user passed in, plus `_isNested` and
- // `_parentOptions` for checking whether we need to depopulate.
- const cloneOptions = Object.assign({}, options, {
- _isNested: true,
- json: json,
- minimize: _minimize,
- flattenMaps: flattenMaps,
- _seen: (options && options._seen) || new Map()
- });
-
- if (utils.hasUserDefinedProperty(options, 'getters')) {
- cloneOptions.getters = options.getters;
- }
- if (utils.hasUserDefinedProperty(options, 'virtuals')) {
- cloneOptions.virtuals = options.virtuals;
+ options.minimize = _minimize;
+ if (!hasOnlyPrimitiveValues) {
+ options._seen = options._seen || new Map();
}
- const depopulate = options.depopulate ||
- (options._parentOptions && options._parentOptions.depopulate || false);
+ const depopulate = options._calledWithOptions.depopulate
+ ?? defaultOptions?.depopulate
+ ?? options.depopulate
+ ?? false;
// _isNested will only be true if this is not the top level document, we
// should never depopulate the top-level document
if (depopulate && options._isNested && this.$__.wasPopulated) {
- return clone(this.$__.wasPopulated.value || this._id, cloneOptions);
+ return clone(this.$__.wasPopulated.value || this._doc._id, options);
+ }
+ if (depopulate) {
+ options.depopulate = true;
}
// merge default options with input options.
- options = utils.options(defaultOptions, options);
+ if (defaultOptions != null) {
+ for (const key of Object.keys(defaultOptions)) {
+ if (options[key] == null) {
+ options[key] = defaultOptions[key];
+ }
+ }
+ }
options._isNested = true;
options.json = json;
options.minimize = _minimize;
- cloneOptions._parentOptions = options;
- cloneOptions._skipSingleNestedGetters = false;
-
- const gettersOptions = Object.assign({}, cloneOptions);
- gettersOptions._skipSingleNestedGetters = true;
+ const parentOptions = options._parentOptions;
+ // Parent options should only bubble down for subdocuments, not populated docs
+ options._parentOptions = this.$isSubdocument ? options : null;
// remember the root transform function
// to save it from being overwritten by sub-transform functions
- const originalTransform = options.transform;
+ // const originalTransform = options.transform;
- let ret = clone(this._doc, cloneOptions) || {};
+ let ret;
+ if (hasOnlyPrimitiveValues && !options.flattenObjectIds) {
+ // Fast path: if we don't have any nested objects or arrays, we only need a
+ // shallow clone.
+ ret = this.$__toObjectShallow();
+ } else {
+ ret = clone(this._doc, options) || {};
+ }
- if (options.getters) {
- applyGetters(this, ret, gettersOptions);
+ const getters = options._calledWithOptions.getters
+ ?? options.getters
+ ?? defaultOptions.getters
+ ?? false;
+
+ if (getters) {
+ applyGetters(this, ret);
if (options.minimize) {
ret = minimize(ret) || {};
}
}
- if (options.virtuals || (options.getters && options.virtuals !== false)) {
- applyVirtuals(this, ret, gettersOptions, options);
+ const virtuals = options._calledWithOptions.virtuals
+ ?? defaultOptions.virtuals
+ ?? parentOptions?.virtuals
+ ?? undefined;
+
+ if (virtuals || (getters && virtuals !== false)) {
+ applyVirtuals(this, ret, options, options);
}
if (options.versionKey === false && this.$__schema.options.versionKey) {
delete ret[this.$__schema.options.versionKey];
}
- let transform = options.transform;
+ const transform = options._calledWithOptions.transform ?? true;
+ let transformFunction = undefined;
+ if (transform === true) {
+ transformFunction = defaultOptions.transform;
+ } else if (typeof transform === 'function') {
+ transformFunction = transform;
+ }
// In the case where a subdocument has its own transform function, we need to
// check and see if the parent has a transform (options.transform) and if the
@@ -3773,20 +3899,30 @@ Document.prototype.$toObject = function(options, json) {
omitDeselectedFields(this, ret);
}
- if (transform === true || (schemaOptions.toObject && transform)) {
- const opts = options.json ? schemaOptions.toJSON : schemaOptions.toObject;
-
- if (opts) {
- transform = (typeof options.transform === 'function' ? options.transform : opts.transform);
+ if (typeof transformFunction === 'function') {
+ const xformed = transformFunction(this, ret, options);
+ if (typeof xformed !== 'undefined') {
+ ret = xformed;
}
- } else {
- options.transform = originalTransform;
}
- if (typeof transform === 'function') {
- const xformed = transform(this, ret, options);
- if (typeof xformed !== 'undefined') {
- ret = xformed;
+ return ret;
+};
+
+/*!
+ * Internal shallow clone alternative to `$toObject()`: much faster, no options processing
+ */
+
+Document.prototype.$__toObjectShallow = function $__toObjectShallow() {
+ const ret = {};
+ if (this._doc != null) {
+ for (const key of Object.keys(this._doc)) {
+ const value = this._doc[key];
+ if (value instanceof Date) {
+ ret[key] = new Date(value);
+ } else if (value !== undefined) {
+ ret[key] = value;
+ }
}
}
@@ -3812,7 +3948,7 @@ Document.prototype.$toObject = function(options, json) {
*
* doc.toObject({ getters: true })
*
- * To apply these options to every document of your schema by default, set your [schemas](#schema_Schema) `toObject` option to the same argument.
+ * To apply these options to every document of your schema by default, set your [schemas](https://mongoosejs.com/docs/api/schema.html#Schema()) `toObject` option to the same argument.
*
* schema.set('toObject', { virtuals: true })
*
@@ -3859,14 +3995,24 @@ Document.prototype.$toObject = function(options, json) {
*
* _Note: if a transform function returns `undefined`, the return value will be ignored._
*
- * Transformations may also be applied inline, overridding any transform set in the options:
+ * Transformations may also be applied inline, overridding any transform set in the schema options.
+ * Any transform function specified in `toObject` options also propagates to any subdocuments.
*
- * function xform (doc, ret, options) {
- * return { inline: ret.name, custom: true }
+ * function deleteId(doc, ret, options) {
+ * delete ret._id;
+ * return ret;
* }
*
- * // pass the transform as an inline option
- * doc.toObject({ transform: xform }); // { inline: 'Wreck-it Ralph', custom: true }
+ * const schema = mongoose.Schema({ name: String, docArr: [{ name: String }] });
+ * const TestModel = mongoose.model('Test', schema);
+ *
+ * const doc = new TestModel({ name: 'test', docArr: [{ name: 'test' }] });
+ *
+ * // pass the transform as an inline option. Deletes `_id` property
+ * // from both the top-level document and the subdocument.
+ * const obj = doc.toObject({ transform: deleteId });
+ * obj._id; // undefined
+ * obj.docArr[0]._id; // undefined
*
* If you want to skip transformations, use `transform: false`:
*
@@ -3886,7 +4032,7 @@ Document.prototype.$toObject = function(options, json) {
* doc.toObject({ hide: 'secret _id', transform: true }); // { name: 'Wreck-it Ralph' }
*
* If you pass a transform in `toObject()` options, Mongoose will apply the transform
- * to [subdocuments](/docs/subdocs.html) in addition to the top-level document.
+ * to [subdocuments](https://mongoosejs.com/docs/subdocs.html) in addition to the top-level document.
* Similarly, `transform: false` skips transforms for all subdocuments.
* Note that this behavior is different for transforms defined in the schema:
* if you define a transform in `schema.options.toObject.transform`, that transform
@@ -3908,19 +4054,20 @@ Document.prototype.$toObject = function(options, json) {
*
* Transforms, like all of these options, are also available for `toJSON`. See [this guide to `JSON.stringify()`](https://thecodebarbarian.com/the-80-20-guide-to-json-stringify-in-javascript.html) to learn why `toJSON()` and `toObject()` are separate functions.
*
- * See [schema options](/docs/guide.html#toObject) for some more details.
+ * See [schema options](https://mongoosejs.com/docs/guide.html#toObject) for some more details.
*
* _During save, no custom options are applied to the document before being sent to the database._
*
* @param {Object} [options]
* @param {Boolean} [options.getters=false] if true, apply all getters, including virtuals
- * @param {Boolean} [options.virtuals=false] if true, apply virtuals, including aliases. Use `{ getters: true, virtuals: false }` to just apply getters, not virtuals
+ * @param {Boolean|Object} [options.virtuals=false] if true, apply virtuals, including aliases. Use `{ getters: true, virtuals: false }` to just apply getters, not virtuals. An object of the form `{ pathsToSkip: ['someVirtual'] }` may also be used to omit specific virtuals.
* @param {Boolean} [options.aliases=true] if `options.virtuals = true`, you can set `options.aliases = false` to skip applying aliases. This option is a no-op if `options.virtuals = false`.
* @param {Boolean} [options.minimize=true] if true, omit any empty objects from the output
* @param {Function|null} [options.transform=null] if set, mongoose will call this function to allow you to transform the returned object
* @param {Boolean} [options.depopulate=false] if true, replace any conventionally populated paths with the original id in the output. Has no affect on virtual populated paths.
* @param {Boolean} [options.versionKey=true] if false, exclude the version key (`__v` by default) from the output
* @param {Boolean} [options.flattenMaps=false] if true, convert Maps to POJOs. Useful if you want to `JSON.stringify()` the result of `toObject()`.
+ * @param {Boolean} [options.flattenObjectIds=false] if true, convert any ObjectIds in the result to 24 character hex strings.
* @param {Boolean} [options.useProjection=false] - If true, omits fields that are excluded in this document's projection. Unless you specified a projection, this will omit any field that has `select: false` in the schema.
* @return {Object} js object (not a POJO)
* @see mongodb.Binary https://mongodb.github.io/node-mongodb-native/4.9/classes/Binary.html
@@ -3933,49 +4080,14 @@ Document.prototype.toObject = function(options) {
return this.$toObject(options);
};
-/**
- * Minimizes an object, removing undefined values and empty objects
- *
- * @param {Object} object to minimize
- * @return {Object}
- * @api private
- */
-
-function minimize(obj) {
- const keys = Object.keys(obj);
- let i = keys.length;
- let hasKeys;
- let key;
- let val;
-
- while (i--) {
- key = keys[i];
- val = obj[key];
-
- if (utils.isPOJO(val)) {
- obj[key] = minimize(val);
- }
-
- if (undefined === obj[key]) {
- delete obj[key];
- continue;
- }
-
- hasKeys = true;
- }
-
- return hasKeys
- ? obj
- : undefined;
-}
-
/*!
* Applies virtuals properties to `json`.
*/
function applyVirtuals(self, json, options, toObjectOptions) {
const schema = self.$__schema;
- const paths = Object.keys(schema.virtuals);
+ const virtuals = schema.virtuals;
+ const paths = Object.keys(virtuals);
let i = paths.length;
const numPaths = i;
let path;
@@ -3986,11 +4098,11 @@ function applyVirtuals(self, json, options, toObjectOptions) {
? toObjectOptions.aliases
: true;
+ options = options || {};
let virtualsToApply = null;
if (Array.isArray(options.virtuals)) {
virtualsToApply = new Set(options.virtuals);
- }
- else if (options.virtuals && options.virtuals.pathsToSkip) {
+ } else if (options.virtuals && options.virtuals.pathsToSkip) {
virtualsToApply = new Set(paths);
for (let i = 0; i < options.virtuals.pathsToSkip.length; i++) {
if (virtualsToApply.has(options.virtuals.pathsToSkip[i])) {
@@ -4003,7 +4115,6 @@ function applyVirtuals(self, json, options, toObjectOptions) {
return json;
}
- options = options || {};
for (i = 0; i < numPaths; ++i) {
path = paths[i];
@@ -4026,6 +4137,15 @@ function applyVirtuals(self, json, options, toObjectOptions) {
}
assignPath = path.substring(options.path.length + 1);
}
+ if (assignPath.indexOf('.') === -1 && assignPath === path) {
+ v = virtuals[path].applyGetters(void 0, self);
+ if (v === void 0) {
+ continue;
+ }
+ v = clone(v, options);
+ json[assignPath] = v;
+ continue;
+ }
const parts = assignPath.split('.');
v = clone(self.get(path), options);
if (v === void 0) {
@@ -4049,12 +4169,11 @@ function applyVirtuals(self, json, options, toObjectOptions) {
*
* @param {Document} self
* @param {Object} json
- * @param {Object} [options]
* @return {Object} `json`
* @api private
*/
-function applyGetters(self, json, options) {
+function applyGetters(self, json) {
const schema = self.$__schema;
const paths = Object.keys(schema.paths);
let i = paths.length;
@@ -4070,6 +4189,7 @@ function applyGetters(self, json, options) {
path = paths[i];
const parts = path.split('.');
+
const plen = parts.length;
const last = plen - 1;
let branch = json;
@@ -4083,9 +4203,24 @@ function applyGetters(self, json, options) {
for (let ii = 0; ii < plen; ++ii) {
part = parts[ii];
v = cur[part];
- if (ii === last) {
- const val = self.$get(path);
- branch[part] = clone(val, options);
+ // If we've reached a non-object part of the branch, continuing would
+ // cause "Cannot create property 'foo' on string 'bar'" error.
+ // Necessary for mongoose-intl plugin re: gh-14446
+ if (branch != null && typeof branch !== 'object') {
+ break;
+ } else if (ii === last) {
+ branch[part] = schema.paths[path].applyGetters(
+ branch[part],
+ self
+ );
+ if (Array.isArray(branch[part]) && schema.paths[path].$embeddedSchemaType) {
+ for (let i = 0; i < branch[part].length; ++i) {
+ branch[part][i] = schema.paths[path].$embeddedSchemaType.applyGetters(
+ branch[part][i],
+ self
+ );
+ }
+ }
} else if (v == null) {
if (part in cur) {
branch[part] = v;
@@ -4190,20 +4325,21 @@ function omitDeselectedFields(self, json) {
/**
* The return value of this method is used in calls to [`JSON.stringify(doc)`](https://thecodebarbarian.com/the-80-20-guide-to-json-stringify-in-javascript#the-tojson-function).
*
- * This method accepts the same options as [Document#toObject](#document_Document-toObject). To apply the options to every document of your schema by default, set your [schemas](#schema_Schema) `toJSON` option to the same argument.
+ * This method accepts the same options as [Document#toObject](https://mongoosejs.com/docs/api/document.html#Document.prototype.toObject()). To apply the options to every document of your schema by default, set your [schemas](https://mongoosejs.com/docs/api/schema.html#Schema()) `toJSON` option to the same argument.
*
* schema.set('toJSON', { virtuals: true });
*
* There is one difference between `toJSON()` and `toObject()` options.
- * When you call `toJSON()`, the [`flattenMaps` option](./document.html#document_Document-toObject) defaults to `true`, because `JSON.stringify()` doesn't convert maps to objects by default.
+ * When you call `toJSON()`, the [`flattenMaps` option](https://mongoosejs.com/docs/api/document.html#Document.prototype.toObject()) defaults to `true`, because `JSON.stringify()` doesn't convert maps to objects by default.
* When you call `toObject()`, the `flattenMaps` option is `false` by default.
*
- * See [schema options](/docs/guide.html#toJSON) for more information on setting `toJSON` option defaults.
+ * See [schema options](https://mongoosejs.com/docs/guide.html#toJSON) for more information on setting `toJSON` option defaults.
*
* @param {Object} options
* @param {Boolean} [options.flattenMaps=true] if true, convert Maps to [POJOs](https://masteringjs.io/tutorials/fundamentals/pojo). Useful if you want to `JSON.stringify()` the result.
+ * @param {Boolean} [options.flattenObjectIds=false] if true, convert any ObjectIds in the result to 24 character hex strings.
* @return {Object}
- * @see Document#toObject #document_Document-toObject
+ * @see Document#toObject https://mongoosejs.com/docs/api/document.html#Document.prototype.toObject()
* @see JSON.stringify() in JavaScript https://thecodebarbarian.com/the-80-20-guide-to-json-stringify-in-javascript.html
* @api public
* @memberOf Document
@@ -4214,6 +4350,9 @@ Document.prototype.toJSON = function(options) {
return this.$toObject(options, true);
};
+/*!
+ * ignore
+ */
Document.prototype.ownerDocument = function() {
return this;
@@ -4239,7 +4378,7 @@ Document.prototype.parent = function() {
};
/**
- * Alias for [`parent()`](#document_Document-parent). If this document is a subdocument or populated
+ * Alias for [`parent()`](https://mongoosejs.com/docs/api/document.html#Document.prototype.parent()). If this document is a subdocument or populated
* document, returns the document's parent. Returns `undefined` otherwise.
*
* @return {Document}
@@ -4268,7 +4407,8 @@ Document.prototype.inspect = function(options) {
opts = options;
opts.minimize = false;
}
- const ret = this.toObject(opts);
+
+ const ret = arguments.length > 0 ? this.toObject(opts) : this.toObject();
if (ret == null) {
// If `toObject()` returns null, `this` is still an object, so if `inspect()`
@@ -4361,33 +4501,31 @@ Document.prototype.equals = function(doc) {
* @param {Object} [match] Conditions for the population query
* @param {Object} [options] Options for the population query (sort, etc)
* @param {String} [options.path=null] The path to populate.
- * @param {string|PopulateOptions} [options.populate=null] Recursively populate paths in the populated documents. See [deep populate docs](/docs/populate.html#deep-populate).
+ * @param {string|PopulateOptions} [options.populate=null] Recursively populate paths in the populated documents. See [deep populate docs](https://mongoosejs.com/docs/populate.html#deep-populate).
* @param {boolean} [options.retainNullValues=false] by default, Mongoose removes null and undefined values from populated arrays. Use this option to make `populate()` retain `null` and `undefined` array entries.
- * @param {boolean} [options.getters=false] if true, Mongoose will call any getters defined on the `localField`. By default, Mongoose gets the raw value of `localField`. For example, you would need to set this option to `true` if you wanted to [add a `lowercase` getter to your `localField`](/docs/schematypes.html#schematype-options).
+ * @param {boolean} [options.getters=false] if true, Mongoose will call any getters defined on the `localField`. By default, Mongoose gets the raw value of `localField`. For example, you would need to set this option to `true` if you wanted to [add a `lowercase` getter to your `localField`](https://mongoosejs.com/docs/schematypes.html#schematype-options).
* @param {boolean} [options.clone=false] When you do `BlogPost.find().populate('author')`, blog posts with the same author will share 1 copy of an `author` doc. Enable this option to make Mongoose clone populated docs before assigning them.
* @param {Object|Function} [options.match=null] Add an additional filter to the populate query. Can be a filter object containing [MongoDB query syntax](https://www.mongodb.com/docs/manual/tutorial/query-documents/), or a function that returns a filter object.
* @param {Function} [options.transform=null] Function that Mongoose will call on every populated document that allows you to transform the populated document.
* @param {Object} [options.options=null] Additional options like `limit` and `lean`.
* @param {Function} [callback] Callback
- * @see population /docs/populate.html
- * @see Query#select #query_Query-select
- * @see Model.populate #model_Model-populate
+ * @see population https://mongoosejs.com/docs/populate.html
+ * @see Query#select https://mongoosejs.com/docs/api/query.html#Query.prototype.select()
+ * @see Model.populate https://mongoosejs.com/docs/api/model.html#Model.populate()
* @memberOf Document
* @instance
* @return {Promise|null} Returns a Promise if no `callback` is given.
* @api public
*/
-Document.prototype.populate = function populate() {
+Document.prototype.populate = async function populate() {
const pop = {};
const args = [...arguments];
- let fn;
+ if (typeof args[args.length - 1] === 'function') {
+ throw new MongooseError('Document.prototype.populate() no longer accepts a callback');
+ }
if (args.length !== 0) {
- if (typeof args[args.length - 1] === 'function') {
- fn = args.pop();
- }
-
// use hash to remove duplicate paths
const res = utils.populate.apply(null, args);
for (const populateOptions of res) {
@@ -4424,7 +4562,7 @@ Document.prototype.populate = function populate() {
p._localModel = topLevelModel;
});
- return topLevelModel.populate(this, paths, fn);
+ return topLevelModel.populate(this, paths);
};
/**
@@ -4517,7 +4655,7 @@ Document.prototype.populated = function(path, val, options) {
};
/**
- * Alias of [`.populated`](#document_Document-populated).
+ * Alias of [`.populated`](https://mongoosejs.com/docs/api/document.html#Document.prototype.populated()).
*
* @method $populated
* @memberOf Document
@@ -4582,7 +4720,7 @@ Document.prototype.$assertPopulated = function $assertPopulated(path, values) {
*
* @param {String|String[]} [path] Specific Path to depopulate. If unset, will depopulate all paths on the Document. Or multiple space-delimited paths.
* @return {Document} this
- * @see Document.populate #document_Document-populate
+ * @see Document.populate https://mongoosejs.com/docs/api/document.html#Document.prototype.populate()
* @api public
* @memberOf Document
* @instance
@@ -4613,7 +4751,23 @@ Document.prototype.depopulate = function(path) {
continue;
}
delete populated[key];
- utils.setValue(key, populatedIds, this._doc);
+ if (Array.isArray(populatedIds)) {
+ const arr = utils.getValue(key, this._doc);
+ if (arr.isMongooseArray) {
+ const rawArray = arr.__array;
+ for (let i = 0; i < rawArray.length; ++i) {
+ const subdoc = rawArray[i];
+ if (subdoc == null) {
+ continue;
+ }
+ rawArray[i] = subdoc instanceof Document ? subdoc._doc._id : subdoc._id;
+ }
+ } else {
+ utils.setValue(key, populatedIds, this._doc);
+ }
+ } else {
+ utils.setValue(key, populatedIds, this._doc);
+ }
}
return this;
}
@@ -4626,7 +4780,23 @@ Document.prototype.depopulate = function(path) {
delete this.$$populatedVirtuals[singlePath];
delete this._doc[singlePath];
} else if (populatedIds) {
- utils.setValue(singlePath, populatedIds, this._doc);
+ if (Array.isArray(populatedIds)) {
+ const arr = utils.getValue(singlePath, this._doc);
+ if (arr.isMongooseArray) {
+ const rawArray = arr.__array;
+ for (let i = 0; i < rawArray.length; ++i) {
+ const subdoc = rawArray[i];
+ if (subdoc == null) {
+ continue;
+ }
+ rawArray[i] = subdoc instanceof Document ? subdoc._doc._id : subdoc._id;
+ }
+ } else {
+ utils.setValue(singlePath, populatedIds, this._doc);
+ }
+ } else {
+ utils.setValue(singlePath, populatedIds, this._doc);
+ }
}
}
return this;
@@ -4696,6 +4866,344 @@ Document.prototype.getChanges = function() {
return changes;
};
+/**
+ * Produces a special query document of the modified properties used in updates.
+ *
+ * @api private
+ * @method $__delta
+ * @memberOf Document
+ * @instance
+ */
+
+Document.prototype.$__delta = function $__delta() {
+ const dirty = this.$__dirty();
+ const optimisticConcurrency = this.$__schema.options.optimisticConcurrency;
+ if (optimisticConcurrency) {
+ if (Array.isArray(optimisticConcurrency)) {
+ const optCon = new Set(optimisticConcurrency);
+ const modPaths = this.modifiedPaths();
+ if (modPaths.find(path => optCon.has(path))) {
+ this.$__.version = dirty.length ? VERSION_ALL : VERSION_WHERE;
+ }
+ } else {
+ this.$__.version = dirty.length ? VERSION_ALL : VERSION_WHERE;
+ }
+ }
+
+ if (!dirty.length && VERSION_ALL !== this.$__.version) {
+ return;
+ }
+ const where = {};
+ const delta = {};
+ const len = dirty.length;
+ const divergent = [];
+ let d = 0;
+
+ where._id = this._doc._id;
+ // If `_id` is an object, need to depopulate, but also need to be careful
+ // because `_id` can technically be null (see gh-6406)
+ if ((where && where._id && where._id.$__ || null) != null) {
+ where._id = where._id.toObject({ transform: false, depopulate: true });
+ }
+ for (; d < len; ++d) {
+ const data = dirty[d];
+ let value = data.value;
+ const match = checkDivergentArray(this, data.path, value);
+ if (match) {
+ divergent.push(match);
+ continue;
+ }
+
+ const pop = this.$populated(data.path, true);
+ if (!pop && this.$__.selected) {
+ // If any array was selected using an $elemMatch projection, we alter the path and where clause
+ // NOTE: MongoDB only supports projected $elemMatch on top level array.
+ const pathSplit = data.path.split('.');
+ const top = pathSplit[0];
+ if (this.$__.selected[top] && this.$__.selected[top].$elemMatch) {
+ // If the selected array entry was modified
+ if (pathSplit.length > 1 && pathSplit[1] == 0 && typeof where[top] === 'undefined') {
+ where[top] = this.$__.selected[top];
+ pathSplit[1] = '$';
+ data.path = pathSplit.join('.');
+ }
+ // if the selected array was modified in any other way throw an error
+ else {
+ divergent.push(data.path);
+ continue;
+ }
+ }
+ }
+
+ // If this path is set to default, and either this path or one of
+ // its parents is excluded, don't treat this path as dirty.
+ if (this.$isDefault(data.path) && this.$__.selected) {
+ if (data.path.indexOf('.') === -1 && isPathExcluded(this.$__.selected, data.path)) {
+ continue;
+ }
+
+ const pathsToCheck = parentPaths(data.path);
+ if (pathsToCheck.find(path => isPathExcluded(this.$__.isSelected, path))) {
+ continue;
+ }
+ }
+
+ if (divergent.length) continue;
+ if (value === undefined) {
+ operand(this, where, delta, data, 1, '$unset');
+ } else if (value === null) {
+ operand(this, where, delta, data, null);
+ } else if (utils.isMongooseArray(value) && value.$path() && value[arrayAtomicsSymbol]) {
+ // arrays and other custom types (support plugins etc)
+ handleAtomics(this, where, delta, data, value);
+ } else if (value[MongooseBuffer.pathSymbol] && Buffer.isBuffer(value)) {
+ // MongooseBuffer
+ value = value.toObject();
+ operand(this, where, delta, data, value);
+ } else {
+ if (this.$__.primitiveAtomics && this.$__.primitiveAtomics[data.path] != null) {
+ const val = this.$__.primitiveAtomics[data.path];
+ const op = firstKey(val);
+ operand(this, where, delta, data, val[op], op);
+ } else {
+ value = clone(value, {
+ depopulate: true,
+ transform: false,
+ virtuals: false,
+ getters: false,
+ omitUndefined: true,
+ _isNested: true
+ });
+ operand(this, where, delta, data, value);
+ }
+ }
+ }
+
+ if (divergent.length) {
+ return new DivergentArrayError(divergent);
+ }
+
+ if (this.$__.version) {
+ this.$__version(where, delta);
+ }
+
+ if (Object.keys(delta).length === 0) {
+ return [where, null];
+ }
+
+ return [where, delta];
+};
+
+/**
+ * Determine if array was populated with some form of filter and is now
+ * being updated in a manner which could overwrite data unintentionally.
+ *
+ * @see https://github.com/Automattic/mongoose/issues/1334
+ * @param {Document} doc
+ * @param {String} path
+ * @param {Any} array
+ * @return {String|undefined}
+ * @api private
+ */
+
+function checkDivergentArray(doc, path, array) {
+ // see if we populated this path
+ const pop = doc.$populated(path, true);
+
+ if (!pop && doc.$__.selected) {
+ // If any array was selected using an $elemMatch projection, we deny the update.
+ // NOTE: MongoDB only supports projected $elemMatch on top level array.
+ const top = path.split('.')[0];
+ if (doc.$__.selected[top + '.$']) {
+ return top;
+ }
+ }
+
+ if (!(pop && utils.isMongooseArray(array))) return;
+
+ // If the array was populated using options that prevented all
+ // documents from being returned (match, skip, limit) or they
+ // deselected the _id field, $pop and $set of the array are
+ // not safe operations. If _id was deselected, we do not know
+ // how to remove elements. $pop will pop off the _id from the end
+ // of the array in the db which is not guaranteed to be the
+ // same as the last element we have here. $set of the entire array
+ // would be similarly destructive as we never received all
+ // elements of the array and potentially would overwrite data.
+ const check = pop.options.match ||
+ pop.options.options && utils.object.hasOwnProperty(pop.options.options, 'limit') || // 0 is not permitted
+ pop.options.options && pop.options.options.skip || // 0 is permitted
+ pop.options.select && // deselected _id?
+ (pop.options.select._id === 0 ||
+ /\s?-_id\s?/.test(pop.options.select));
+
+ if (check) {
+ const atomics = array[arrayAtomicsSymbol];
+ if (Object.keys(atomics).length === 0 || atomics.$set || atomics.$pop) {
+ return path;
+ }
+ }
+}
+
+/**
+ * Apply the operation to the delta (update) clause as
+ * well as track versioning for our where clause.
+ *
+ * @param {Document} self
+ * @param {Object} where Unused
+ * @param {Object} delta
+ * @param {Object} data
+ * @param {Mixed} val
+ * @param {String} [op]
+ * @api private
+ */
+
+function operand(self, where, delta, data, val, op) {
+ // delta
+ op || (op = '$set');
+ if (!delta[op]) delta[op] = {};
+ delta[op][data.path] = val;
+ // disabled versioning?
+ if (self.$__schema.options.versionKey === false) return;
+
+ // path excluded from versioning?
+ if (shouldSkipVersioning(self, data.path)) return;
+
+ // already marked for versioning?
+ if (VERSION_ALL === (VERSION_ALL & self.$__.version)) return;
+
+ if (self.$__schema.options.optimisticConcurrency) {
+ return;
+ }
+
+ switch (op) {
+ case '$set':
+ case '$unset':
+ case '$pop':
+ case '$pull':
+ case '$pullAll':
+ case '$push':
+ case '$addToSet':
+ case '$inc':
+ break;
+ default:
+ // nothing to do
+ return;
+ }
+
+ // ensure updates sent with positional notation are
+ // editing the correct array element.
+ // only increment the version if an array position changes.
+ // modifying elements of an array is ok if position does not change.
+ if (op === '$push' || op === '$addToSet' || op === '$pullAll' || op === '$pull') {
+ if (/\.\d+\.|\.\d+$/.test(data.path)) {
+ self.$__.version = VERSION_ALL;
+ } else {
+ self.$__.version = VERSION_INC;
+ }
+ } else if (/^\$p/.test(op)) {
+ // potentially changing array positions
+ self.$__.version = VERSION_ALL;
+ } else if (Array.isArray(val)) {
+ // $set an array
+ self.$__.version = VERSION_ALL;
+ } else if (/\.\d+\.|\.\d+$/.test(data.path)) {
+ // now handling $set, $unset
+ // subpath of array
+ self.$__.version = VERSION_WHERE;
+ }
+}
+
+/**
+ * Compiles an update and where clause for a `val` with _atomics.
+ *
+ * @param {Document} self
+ * @param {Object} where
+ * @param {Object} delta
+ * @param {Object} data
+ * @param {Array} value
+ * @api private
+ */
+
+function handleAtomics(self, where, delta, data, value) {
+ if (delta.$set && delta.$set[data.path]) {
+ // $set has precedence over other atomics
+ return;
+ }
+
+ if (typeof value.$__getAtomics === 'function') {
+ value.$__getAtomics().forEach(function(atomic) {
+ const op = atomic[0];
+ const val = atomic[1];
+ operand(self, where, delta, data, val, op);
+ });
+ return;
+ }
+
+ // legacy support for plugins
+
+ const atomics = value[arrayAtomicsSymbol];
+ const ops = Object.keys(atomics);
+ let i = ops.length;
+ let val;
+ let op;
+
+ if (i === 0) {
+ // $set
+
+ if (utils.isMongooseObject(value)) {
+ value = value.toObject({ depopulate: 1, _isNested: true });
+ } else if (value.valueOf) {
+ value = value.valueOf();
+ }
+
+ return operand(self, where, delta, data, value);
+ }
+
+ function iter(mem) {
+ return utils.isMongooseObject(mem)
+ ? mem.toObject({ depopulate: 1, _isNested: true })
+ : mem;
+ }
+
+ while (i--) {
+ op = ops[i];
+ val = atomics[op];
+
+ if (utils.isMongooseObject(val)) {
+ val = val.toObject({ depopulate: true, transform: false, _isNested: true });
+ } else if (Array.isArray(val)) {
+ val = val.map(iter);
+ } else if (val.valueOf) {
+ val = val.valueOf();
+ }
+
+ if (op === '$addToSet') {
+ val = { $each: val };
+ }
+
+ operand(self, where, delta, data, val, op);
+ }
+}
+
+/**
+ * Determines whether versioning should be skipped for the given path
+ *
+ * @param {Document} self
+ * @param {String} path
+ * @return {Boolean} true if versioning should be skipped for the given path
+ * @api private
+ */
+function shouldSkipVersioning(self, path) {
+ const skipVersioning = self.$__schema.options.skipVersioning;
+ if (!skipVersioning) return false;
+
+ // Remove any array indexes from the path
+ path = path.replace(/\.\d+\./, '.');
+
+ return skipVersioning[path];
+}
+
/**
* Returns a copy of this document with a deep clone of `_doc` and `$__`.
*
@@ -4711,7 +5219,7 @@ Document.prototype.$clone = function() {
const clonedDoc = new Model();
clonedDoc.$isNew = this.$isNew;
if (this._doc) {
- clonedDoc._doc = clone(this._doc);
+ clonedDoc._doc = clone(this._doc, { retainDocuments: true });
}
if (this.$__) {
const Cache = this.$__.constructor;
@@ -4728,9 +5236,132 @@ Document.prototype.$clone = function() {
return clonedDoc;
};
+/**
+ * Creates a snapshot of this document's internal change tracking state. You can later
+ * reset this document's change tracking state using `$restoreModifiedPathsSnapshot()`.
+ *
+ * #### Example:
+ *
+ * const doc = await TestModel.findOne();
+ * const snapshot = doc.$createModifiedPathsSnapshot();
+ *
+ * @return {ModifiedPathsSnapshot} a copy of this document's internal change tracking state
+ * @api public
+ * @method $createModifiedPathsSnapshot
+ * @memberOf Document
+ * @instance
+ */
+
+Document.prototype.$createModifiedPathsSnapshot = function $createModifiedPathsSnapshot() {
+ const subdocSnapshot = new WeakMap();
+ if (!this.$isSubdocument) {
+ const subdocs = this.$getAllSubdocs();
+ for (const child of subdocs) {
+ subdocSnapshot.set(child, child.$__.activePaths.clone());
+ }
+ }
+
+ return new ModifiedPathsSnapshot(
+ subdocSnapshot,
+ this.$__.activePaths.clone(),
+ this.$__.version
+ );
+};
+
+/**
+ * Restore this document's change tracking state to the given snapshot.
+ * Note that `$restoreModifiedPathsSnapshot()` does **not** modify the document's
+ * properties, just resets the change tracking state.
+ *
+ * This method is especially useful when writing [custom transaction wrappers](https://github.com/Automattic/mongoose/issues/14268#issuecomment-2100505554) that need to restore change tracking when aborting a transaction.
+ *
+ * #### Example:
+ *
+ * const doc = await TestModel.findOne();
+ * const snapshot = doc.$createModifiedPathsSnapshot();
+ *
+ * doc.name = 'test';
+ * doc.$restoreModifiedPathsSnapshot(snapshot);
+ * doc.$isModified('name'); // false because `name` was not modified when snapshot was taken
+ * doc.name; // 'test', `$restoreModifiedPathsSnapshot()` does **not** modify the document's data, only change tracking
+ *
+ * @param {ModifiedPathsSnapshot} snapshot of the document's internal change tracking state snapshot to restore
+ * @api public
+ * @method $restoreModifiedPathsSnapshot
+ * @return {Document} this
+ * @memberOf Document
+ * @instance
+ */
+
+Document.prototype.$restoreModifiedPathsSnapshot = function $restoreModifiedPathsSnapshot(snapshot) {
+ this.$__.activePaths = snapshot.activePaths.clone();
+ this.$__.version = snapshot.version;
+ if (!this.$isSubdocument) {
+ const subdocs = this.$getAllSubdocs();
+ for (const child of subdocs) {
+ if (snapshot.subdocSnapshot.has(child)) {
+ child.$__.activePaths = snapshot.subdocSnapshot.get(child);
+ }
+ }
+ }
+
+ return this;
+};
+
+/**
+ * Clear the document's modified paths.
+ *
+ * #### Example:
+ *
+ * const doc = await TestModel.findOne();
+ *
+ * doc.name = 'test';
+ * doc.$isModified('name'); // true
+ *
+ * doc.$clearModifiedPaths();
+ * doc.name; // 'test', `$clearModifiedPaths()` does **not** modify the document's data, only change tracking
+ *
+ * @api public
+ * @return {Document} this
+ * @method $clearModifiedPaths
+ * @memberOf Document
+ * @instance
+ */
+
+Document.prototype.$clearModifiedPaths = function $clearModifiedPaths() {
+ this.$__.activePaths.clear('modify');
+ this.$__.activePaths.clear('init');
+ this.$__.version = 0;
+ if (!this.$isSubdocument) {
+ const subdocs = this.$getAllSubdocs();
+ for (const child of subdocs) {
+ child.$clearModifiedPaths();
+ }
+ }
+
+ return this;
+};
+
+/*!
+ * Check if the given document only has primitive values
+ */
+
+Document.prototype.$__hasOnlyPrimitiveValues = function $__hasOnlyPrimitiveValues() {
+ return !this.$__.populated && !this.$__.wasPopulated && (this._doc == null || Object.values(this._doc).every(v => {
+ return v == null
+ || typeof v !== 'object'
+ || (utils.isNativeObject(v) && !Array.isArray(v))
+ || isBsonType(v, 'ObjectId')
+ || isBsonType(v, 'Decimal128');
+ }));
+};
+
/*!
* Module exports.
*/
+Document.VERSION_WHERE = VERSION_WHERE;
+Document.VERSION_INC = VERSION_INC;
+Document.VERSION_ALL = VERSION_ALL;
Document.ValidationError = ValidationError;
module.exports = exports = Document;
diff --git a/lib/document_provider.js b/lib/documentProvider.js
similarity index 90%
rename from lib/document_provider.js
rename to lib/documentProvider.js
index 1ace61f4fb3..894494403f4 100644
--- a/lib/document_provider.js
+++ b/lib/documentProvider.js
@@ -15,7 +15,7 @@ let isBrowser = false;
*
* @api private
*/
-module.exports = function() {
+module.exports = function documentProvider() {
if (isBrowser) {
return BrowserDocument;
}
diff --git a/lib/drivers/browser/ReadPreference.js b/lib/drivers/browser/ReadPreference.js
deleted file mode 100644
index 1363570810b..00000000000
--- a/lib/drivers/browser/ReadPreference.js
+++ /dev/null
@@ -1,7 +0,0 @@
-/*!
- * ignore
- */
-
-'use strict';
-
-module.exports = function() {};
diff --git a/lib/drivers/browser/index.js b/lib/drivers/browser/index.js
index bc4ac5f6b28..681ba9519dc 100644
--- a/lib/drivers/browser/index.js
+++ b/lib/drivers/browser/index.js
@@ -4,13 +4,9 @@
'use strict';
-exports.Binary = require('./binary');
exports.Collection = function() {
throw new Error('Cannot create a collection from browser library');
};
-exports.getConnection = () => function() {
+exports.Connection = function() {
throw new Error('Cannot create a connection from browser library');
};
-exports.Decimal128 = require('./decimal128');
-exports.ObjectId = require('./objectid');
-exports.ReadPreference = require('./ReadPreference');
diff --git a/lib/drivers/node-mongodb-native/ReadPreference.js b/lib/drivers/node-mongodb-native/ReadPreference.js
deleted file mode 100644
index bb999ebd684..00000000000
--- a/lib/drivers/node-mongodb-native/ReadPreference.js
+++ /dev/null
@@ -1,47 +0,0 @@
-/*!
- * Module dependencies.
- */
-
-'use strict';
-
-const mongodb = require('mongodb');
-const ReadPref = mongodb.ReadPreference;
-
-/**
- * Converts arguments to ReadPrefs the driver
- * can understand.
- *
- * @param {String|Array} pref
- * @param {Array} [tags]
- */
-
-module.exports = function readPref(pref, tags) {
- if (Array.isArray(pref)) {
- tags = pref[1];
- pref = pref[0];
- }
-
- if (pref instanceof ReadPref) {
- return pref;
- }
-
- switch (pref) {
- case 'p':
- pref = 'primary';
- break;
- case 'pp':
- pref = 'primaryPreferred';
- break;
- case 's':
- pref = 'secondary';
- break;
- case 'sp':
- pref = 'secondaryPreferred';
- break;
- case 'n':
- pref = 'nearest';
- break;
- }
-
- return new ReadPref(pref, tags);
-};
diff --git a/lib/drivers/node-mongodb-native/binary.js b/lib/drivers/node-mongodb-native/binary.js
deleted file mode 100644
index 4e3c86f78c8..00000000000
--- a/lib/drivers/node-mongodb-native/binary.js
+++ /dev/null
@@ -1,10 +0,0 @@
-
-/*!
- * Module dependencies.
- */
-
-'use strict';
-
-const Binary = require('mongodb').Binary;
-
-module.exports = exports = Binary;
diff --git a/lib/drivers/node-mongodb-native/collection.js b/lib/drivers/node-mongodb-native/collection.js
index 6452c22c8c2..d2150c6ad57 100644
--- a/lib/drivers/node-mongodb-native/collection.js
+++ b/lib/drivers/node-mongodb-native/collection.js
@@ -7,8 +7,9 @@
const MongooseCollection = require('../../collection');
const MongooseError = require('../../error/mongooseError');
const Collection = require('mongodb').Collection;
-const ObjectId = require('./objectid');
+const ObjectId = require('../../types/objectid');
const getConstructorName = require('../../helpers/getConstructorName');
+const internalToObjectOptions = require('../../options').internalToObjectOptions;
const stream = require('stream');
const util = require('util');
@@ -122,10 +123,6 @@ function iter(i) {
let _args = args;
let callback = null;
if (this._shouldBufferCommands() && this.buffer) {
- if (syncCollectionMethods[i] && typeof lastArg !== 'function') {
- throw new Error('Collection method ' + i + ' is synchronous');
- }
-
this.conn.emit('buffer', {
_id: opId,
modelName: _this.modelName,
@@ -138,10 +135,23 @@ function iter(i) {
let _args = args;
let promise = null;
let timeout = null;
- if (syncCollectionMethods[i]) {
- this.addQueue(() => {
- lastArg.call(this, null, this[i].apply(this, _args.slice(0, _args.length - 1)));
- }, []);
+ if (syncCollectionMethods[i] && typeof lastArg === 'function') {
+ this.addQueue(i, _args);
+ callback = lastArg;
+ } else if (syncCollectionMethods[i]) {
+ promise = new this.Promise((resolve, reject) => {
+ callback = function collectionOperationCallback(err, res) {
+ if (timeout != null) {
+ clearTimeout(timeout);
+ }
+ if (err != null) {
+ return reject(err);
+ }
+ resolve(res);
+ };
+ _args = args.concat([callback]);
+ this.addQueue(i, _args);
+ });
} else if (typeof lastArg === 'function') {
callback = function collectionOperationCallback() {
if (timeout != null) {
@@ -151,7 +161,7 @@ function iter(i) {
};
_args = args.slice(0, args.length - 1).concat([callback]);
} else {
- promise = new this.Promise((resolve, reject) => {
+ promise = new Promise((resolve, reject) => {
callback = function collectionOperationCallback(err, res) {
if (timeout != null) {
clearTimeout(timeout);
@@ -198,8 +208,14 @@ function iter(i) {
if (debug) {
if (typeof debug === 'function') {
+ let argsToAdd = null;
+ if (typeof args[args.length - 1] == 'function') {
+ argsToAdd = args.slice(0, args.length - 1);
+ } else {
+ argsToAdd = args;
+ }
debug.apply(_this,
- [_this.name, i].concat(args.slice(0, args.length - 1)));
+ [_this.name, i].concat(argsToAdd));
} else if (debug instanceof stream.Writable) {
this.$printToStream(_this.name, i, args, debug);
} else {
@@ -220,19 +236,30 @@ function iter(i) {
}
if (syncCollectionMethods[i] && typeof lastArg === 'function') {
- return lastArg.call(this, null, collection[i].apply(collection, _args.slice(0, _args.length - 1)));
+ const result = collection[i].apply(collection, _args.slice(0, _args.length - 1));
+ this.conn.emit('operation-end', { _id: opId, modelName: _this.modelName, collectionName: this.name, method: i, result });
+ return lastArg.call(this, null, result);
}
const ret = collection[i].apply(collection, _args);
if (ret != null && typeof ret.then === 'function') {
return ret.then(
- res => {
- this.conn.emit('operation-end', { _id: opId, modelName: this.modelName, collectionName: this.name, method: i, result: res });
- return res;
+ result => {
+ if (typeof lastArg === 'function') {
+ lastArg(null, result);
+ } else {
+ this.conn.emit('operation-end', { _id: opId, modelName: _this.modelName, collectionName: this.name, method: i, result });
+ }
+ return result;
},
- err => {
- this.conn.emit('operation-end', { _id: opId, modelName: this.modelName, collectionName: this.name, method: i, error: err });
- throw err;
+ error => {
+ if (typeof lastArg === 'function') {
+ lastArg(error);
+ return;
+ } else {
+ this.conn.emit('operation-end', { _id: opId, modelName: _this.modelName, collectionName: this.name, method: i, error });
+ }
+ throw error;
}
);
}
@@ -357,12 +384,12 @@ function format(obj, sub, color, shell) {
}
const clone = require('../../helpers/clone');
- let x = clone(obj, { transform: false });
+ let x = clone(obj, internalToObjectOptions);
const constructorName = getConstructorName(x);
if (constructorName === 'Binary') {
x = 'BinData(' + x.sub_type + ', "' + x.toString('base64') + '")';
- } else if (constructorName === 'ObjectID') {
+ } else if (constructorName === 'ObjectId') {
x = inspectable('ObjectId("' + x.toHexString() + '")');
} else if (constructorName === 'Date') {
x = inspectable('new Date("' + x.toUTCString() + '")');
@@ -391,7 +418,7 @@ function format(obj, sub, color, shell) {
x[key].buffer.toString('base64') + '")';
} else if (_constructorName === 'Object') {
x[key] = format(x[key], true);
- } else if (_constructorName === 'ObjectID') {
+ } else if (_constructorName === 'ObjectId') {
formatObjectId(x, key);
} else if (_constructorName === 'Date') {
formatDate(x, key, shell);
@@ -427,7 +454,6 @@ function format(obj, sub, color, shell) {
/**
* Retrieves information about this collections indexes.
*
- * @param {Function} callback
* @method getIndexes
* @api public
*/
diff --git a/lib/drivers/node-mongodb-native/connection.js b/lib/drivers/node-mongodb-native/connection.js
index 5c6976d92b4..e626cb09d82 100644
--- a/lib/drivers/node-mongodb-native/connection.js
+++ b/lib/drivers/node-mongodb-native/connection.js
@@ -5,9 +5,13 @@
'use strict';
const MongooseConnection = require('../../connection');
-const STATES = require('../../connectionstate');
-const immediate = require('../../helpers/immediate');
+const MongooseError = require('../../error/index');
+const STATES = require('../../connectionState');
+const mongodb = require('mongodb');
+const pkg = require('../../../package.json');
+const processConnectionOptions = require('../../helpers/processConnectionOptions');
const setTimeout = require('../../helpers/timers').setTimeout;
+const utils = require('../../utils');
/**
* A [node-mongodb-native](https://github.com/mongodb/node-mongodb-native) connection implementation.
@@ -19,6 +23,11 @@ const setTimeout = require('../../helpers/timers').setTimeout;
function NativeConnection() {
MongooseConnection.apply(this, arguments);
this._listening = false;
+ // Tracks the last time (as unix timestamp) the connection received a
+ // serverHeartbeatSucceeded or serverHeartbeatFailed event from the underlying MongoClient.
+ // If we haven't received one in a while (like due to a frozen AWS Lambda container) then
+ // `readyState` is likely stale.
+ this._lastHeartbeatAt = null;
}
/**
@@ -92,7 +101,7 @@ NativeConnection.prototype.useDb = function(name, options) {
if (this.db && this._readyState === STATES.connected) {
wireup();
} else {
- this.once('connected', wireup);
+ this._queue.push({ fn: wireup });
}
function wireup() {
@@ -102,6 +111,7 @@ NativeConnection.prototype.useDb = function(name, options) {
_opts.noListener = options.noListener;
}
newConn.db = _this.client.db(name, _opts);
+ newConn._lastHeartbeatAt = _this._lastHeartbeatAt;
newConn.onOpen();
}
@@ -122,18 +132,54 @@ NativeConnection.prototype.useDb = function(name, options) {
return newConn;
};
+/**
+ * Removes the database connection with the given name created with `useDb()`.
+ *
+ * Throws an error if the database connection was not found.
+ *
+ * #### Example:
+ *
+ * // Connect to `initialdb` first
+ * const conn = await mongoose.createConnection('mongodb://127.0.0.1:27017/initialdb').asPromise();
+ *
+ * // Creates an un-cached connection to `mydb`
+ * const db = conn.useDb('mydb');
+ *
+ * // Closes `db`, and removes `db` from `conn.relatedDbs` and `conn.otherDbs`
+ * await conn.removeDb('mydb');
+ *
+ * @method removeDb
+ * @memberOf Connection
+ * @param {String} name The database name
+ * @return {Connection} this
+ */
+
+NativeConnection.prototype.removeDb = function removeDb(name) {
+ const dbs = this.otherDbs.filter(db => db.name === name);
+ if (!dbs.length) {
+ throw new MongooseError(`No connections to database "${name}" found`);
+ }
+
+ for (const db of dbs) {
+ db._closeCalled = true;
+ db._destroyCalled = true;
+ db._readyState = STATES.disconnected;
+ db.$wasForceClosed = true;
+ }
+ delete this.relatedDbs[name];
+ this.otherDbs = this.otherDbs.filter(db => db.name !== name);
+};
+
/**
* Closes the connection
*
* @param {Boolean} [force]
- * @param {Function} [fn]
* @return {Connection} this
* @api private
*/
-NativeConnection.prototype.doClose = function(force, fn) {
+NativeConnection.prototype.doClose = async function doClose(force) {
if (this.client == null) {
- immediate(() => fn());
return this;
}
@@ -144,20 +190,249 @@ NativeConnection.prototype.doClose = function(force, fn) {
}
if (skipCloseClient) {
- immediate(() => fn());
return this;
}
- this.client.close(force, (err, res) => {
- // Defer because the driver will wait at least 1ms before finishing closing
- // the pool, see https://github.com/mongodb-js/mongodb-core/blob/a8f8e4ce41936babc3b9112bf42d609779f03b39/lib/connection/pool.js#L1026-L1030.
- // If there's queued operations, you may still get some background work
- // after the callback is called.
- setTimeout(() => fn(err, res), 1);
- });
+ await this.client.close(force);
+ // Defer because the driver will wait at least 1ms before finishing closing
+ // the pool, see https://github.com/mongodb-js/mongodb-core/blob/a8f8e4ce41936babc3b9112bf42d609779f03b39/lib/connection/pool.js#L1026-L1030.
+ // If there's queued operations, you may still get some background work
+ // after the callback is called.
+ await new Promise(resolve => setTimeout(resolve, 1));
+
return this;
};
+/**
+ * Implementation of `listDatabases()` for MongoDB driver
+ *
+ * @return Promise
+ * @api public
+ */
+
+NativeConnection.prototype.listDatabases = async function listDatabases() {
+ await this._waitForConnect();
+
+ return await this.db.admin().listDatabases();
+};
+
+/*!
+ * ignore
+ */
+
+NativeConnection.prototype.createClient = async function createClient(uri, options) {
+ if (typeof uri !== 'string') {
+ throw new MongooseError('The `uri` parameter to `openUri()` must be a ' +
+ `string, got "${typeof uri}". Make sure the first parameter to ` +
+ '`mongoose.connect()` or `mongoose.createConnection()` is a string.');
+ }
+
+ if (this._destroyCalled) {
+ throw new MongooseError(
+ 'Connection has been closed and destroyed, and cannot be used for re-opening the connection. ' +
+ 'Please create a new connection with `mongoose.createConnection()` or `mongoose.connect()`.'
+ );
+ }
+
+ if (this.readyState === STATES.connecting || this.readyState === STATES.connected) {
+ if (this._connectionString !== uri) {
+ throw new MongooseError('Can\'t call `openUri()` on an active connection with ' +
+ 'different connection strings. Make sure you aren\'t calling `mongoose.connect()` ' +
+ 'multiple times. See: https://mongoosejs.com/docs/connections.html#multiple_connections');
+ }
+ }
+
+ options = processConnectionOptions(uri, options);
+
+ if (options) {
+
+ const autoIndex = options.config && options.config.autoIndex != null ?
+ options.config.autoIndex :
+ options.autoIndex;
+ if (autoIndex != null) {
+ this.config.autoIndex = autoIndex !== false;
+ delete options.config;
+ delete options.autoIndex;
+ }
+
+ if ('autoCreate' in options) {
+ this.config.autoCreate = !!options.autoCreate;
+ delete options.autoCreate;
+ }
+
+ if ('sanitizeFilter' in options) {
+ this.config.sanitizeFilter = options.sanitizeFilter;
+ delete options.sanitizeFilter;
+ }
+
+ if ('autoSearchIndex' in options) {
+ this.config.autoSearchIndex = options.autoSearchIndex;
+ delete options.autoSearchIndex;
+ }
+
+ // Backwards compat
+ if (options.user || options.pass) {
+ options.auth = options.auth || {};
+ options.auth.username = options.user;
+ options.auth.password = options.pass;
+
+ this.user = options.user;
+ this.pass = options.pass;
+ }
+ delete options.user;
+ delete options.pass;
+
+ if (options.bufferCommands != null) {
+ this.config.bufferCommands = options.bufferCommands;
+ delete options.bufferCommands;
+ }
+ } else {
+ options = {};
+ }
+
+ this._connectionOptions = options;
+ const dbName = options.dbName;
+ if (dbName != null) {
+ this.$dbName = dbName;
+ }
+ delete options.dbName;
+
+ if (!utils.hasUserDefinedProperty(options, 'driverInfo')) {
+ options.driverInfo = {
+ name: 'Mongoose',
+ version: pkg.version
+ };
+ }
+
+ this.readyState = STATES.connecting;
+ this._connectionString = uri;
+
+ let client;
+ try {
+ client = new mongodb.MongoClient(uri, options);
+ } catch (error) {
+ this.readyState = STATES.disconnected;
+ throw error;
+ }
+ this.client = client;
+
+ client.setMaxListeners(0);
+ await client.connect();
+
+ _setClient(this, client, options, dbName);
+
+ for (const db of this.otherDbs) {
+ _setClient(db, client, {}, db.name);
+ }
+ return this;
+};
+
+/*!
+ * ignore
+ */
+
+NativeConnection.prototype.setClient = function setClient(client) {
+ if (!(client instanceof mongodb.MongoClient)) {
+ throw new MongooseError('Must call `setClient()` with an instance of MongoClient');
+ }
+ if (this.readyState !== STATES.disconnected) {
+ throw new MongooseError('Cannot call `setClient()` on a connection that is already connected.');
+ }
+ if (client.topology == null) {
+ throw new MongooseError('Cannot call `setClient()` with a MongoClient that you have not called `connect()` on yet.');
+ }
+
+ this._connectionString = client.s.url;
+ _setClient(this, client, {}, client.s.options.dbName);
+
+ for (const model of Object.values(this.models)) {
+ // Errors handled internally, so safe to ignore error
+ model.init().catch(function $modelInitNoop() {});
+ }
+
+ return this;
+};
+
+/*!
+ * ignore
+ */
+
+function _setClient(conn, client, options, dbName) {
+ const db = dbName != null ? client.db(dbName) : client.db();
+ conn.db = db;
+ conn.client = client;
+ conn.host = client &&
+ client.s &&
+ client.s.options &&
+ client.s.options.hosts &&
+ client.s.options.hosts[0] &&
+ client.s.options.hosts[0].host || void 0;
+ conn.port = client &&
+ client.s &&
+ client.s.options &&
+ client.s.options.hosts &&
+ client.s.options.hosts[0] &&
+ client.s.options.hosts[0].port || void 0;
+ conn.name = dbName != null ? dbName : db.databaseName;
+ conn._closeCalled = client._closeCalled;
+
+ const _handleReconnect = () => {
+ // If we aren't disconnected, we assume this reconnect is due to a
+ // socket timeout. If there's no activity on a socket for
+ // `socketTimeoutMS`, the driver will attempt to reconnect and emit
+ // this event.
+ if (conn.readyState !== STATES.connected) {
+ conn.readyState = STATES.connected;
+ conn.emit('reconnect');
+ conn.emit('reconnected');
+ conn.onOpen();
+ }
+ };
+
+ const type = client &&
+ client.topology &&
+ client.topology.description &&
+ client.topology.description.type || '';
+
+ if (type === 'Single') {
+ client.on('serverDescriptionChanged', ev => {
+ const newDescription = ev.newDescription;
+ if (newDescription.type === 'Unknown') {
+ conn.readyState = STATES.disconnected;
+ } else {
+ _handleReconnect();
+ }
+ });
+ } else if (type.startsWith('ReplicaSet')) {
+ client.on('topologyDescriptionChanged', ev => {
+ // Emit disconnected if we've lost connectivity to the primary
+ const description = ev.newDescription;
+ if (conn.readyState === STATES.connected && description.type !== 'ReplicaSetWithPrimary') {
+ // Implicitly emits 'disconnected'
+ conn.readyState = STATES.disconnected;
+ } else if (conn.readyState === STATES.disconnected && description.type === 'ReplicaSetWithPrimary') {
+ _handleReconnect();
+ }
+ });
+ }
+ client.on('serverHeartbeatSucceeded', () => {
+ conn._lastHeartbeatAt = Date.now();
+ });
+
+ if (options.monitorCommands) {
+ client.on('commandStarted', (data) => conn.emit('commandStarted', data));
+ client.on('commandFailed', (data) => conn.emit('commandFailed', data));
+ client.on('commandSucceeded', (data) => conn.emit('commandSucceeded', data));
+ }
+
+ conn.onOpen();
+
+ for (const i in conn.collections) {
+ if (utils.object.hasOwnProperty(conn.collections, i)) {
+ conn.collections[i].onOpen();
+ }
+ }
+}
/*!
* Module exports.
diff --git a/lib/drivers/node-mongodb-native/decimal128.js b/lib/drivers/node-mongodb-native/decimal128.js
deleted file mode 100644
index c895f17fd2a..00000000000
--- a/lib/drivers/node-mongodb-native/decimal128.js
+++ /dev/null
@@ -1,7 +0,0 @@
-/*!
- * ignore
- */
-
-'use strict';
-
-module.exports = require('mongodb').Decimal128;
diff --git a/lib/drivers/node-mongodb-native/index.js b/lib/drivers/node-mongodb-native/index.js
index e6714679f50..de82be5986b 100644
--- a/lib/drivers/node-mongodb-native/index.js
+++ b/lib/drivers/node-mongodb-native/index.js
@@ -4,9 +4,5 @@
'use strict';
-exports.Binary = require('./binary');
exports.Collection = require('./collection');
-exports.Decimal128 = require('./decimal128');
-exports.ObjectId = require('./objectid');
-exports.ReadPreference = require('./ReadPreference');
-exports.getConnection = () => require('./connection');
+exports.Connection = require('./connection');
diff --git a/lib/drivers/node-mongodb-native/objectid.js b/lib/drivers/node-mongodb-native/objectid.js
deleted file mode 100644
index 6f432b79685..00000000000
--- a/lib/drivers/node-mongodb-native/objectid.js
+++ /dev/null
@@ -1,16 +0,0 @@
-
-/*!
- * [node-mongodb-native](https://github.com/mongodb/node-mongodb-native) ObjectId
- * @constructor NodeMongoDbObjectId
- * @see ObjectId
- */
-
-'use strict';
-
-const ObjectId = require('mongodb').ObjectId;
-
-/*!
- * ignore
- */
-
-module.exports = exports = ObjectId;
diff --git a/lib/error/browserMissingSchema.js b/lib/error/browserMissingSchema.js
index 3f271499d4d..ffeffc77257 100644
--- a/lib/error/browserMissingSchema.js
+++ b/lib/error/browserMissingSchema.js
@@ -4,13 +4,14 @@
'use strict';
-const MongooseError = require('./');
+const MongooseError = require('./mongooseError');
+/**
+ * MissingSchema Error constructor.
+ */
class MissingSchemaError extends MongooseError {
- /**
- * MissingSchema Error constructor.
- */
+
constructor() {
super('Schema hasn\'t been registered for document.\n'
+ 'Use mongoose.Document(name, schema)');
diff --git a/lib/error/bulkSaveIncompleteError.js b/lib/error/bulkSaveIncompleteError.js
new file mode 100644
index 00000000000..c4b88e5d7bb
--- /dev/null
+++ b/lib/error/bulkSaveIncompleteError.js
@@ -0,0 +1,44 @@
+/*!
+ * Module dependencies.
+ */
+
+'use strict';
+
+const MongooseError = require('./mongooseError');
+
+
+/**
+ * If the underwriting `bulkWrite()` for `bulkSave()` succeeded, but wasn't able to update or
+ * insert all documents, we throw this error.
+ *
+ * @api private
+ */
+
+class MongooseBulkSaveIncompleteError extends MongooseError {
+ constructor(modelName, documents, bulkWriteResult) {
+ const matchedCount = bulkWriteResult?.matchedCount ?? 0;
+ const insertedCount = bulkWriteResult?.insertedCount ?? 0;
+ let preview = documents.map(doc => doc._id).join(', ');
+ if (preview.length > 100) {
+ preview = preview.slice(0, 100) + '...';
+ }
+
+ const numDocumentsNotUpdated = documents.length - matchedCount - insertedCount;
+ super(`${modelName}.bulkSave() was not able to update ${numDocumentsNotUpdated} of the given documents due to incorrect version or optimistic concurrency, document ids: ${preview}`);
+
+ this.modelName = modelName;
+ this.documents = documents;
+ this.bulkWriteResult = bulkWriteResult;
+ this.numDocumentsNotUpdated = numDocumentsNotUpdated;
+ }
+}
+
+Object.defineProperty(MongooseBulkSaveIncompleteError.prototype, 'name', {
+ value: 'MongooseBulkSaveIncompleteError'
+});
+
+/*!
+ * exports
+ */
+
+module.exports = MongooseBulkSaveIncompleteError;
diff --git a/lib/error/bulkWriteError.js b/lib/error/bulkWriteError.js
new file mode 100644
index 00000000000..1711b03b586
--- /dev/null
+++ b/lib/error/bulkWriteError.js
@@ -0,0 +1,41 @@
+/*!
+ * Module dependencies.
+ */
+
+'use strict';
+
+const MongooseError = require('./');
+
+
+/**
+ * If `bulkWrite()` or `insertMany()` has validation errors, but
+ * all valid operations succeed, and 'throwOnValidationError' is true,
+ * Mongoose will throw this error.
+ *
+ * @api private
+ */
+
+class MongooseBulkWriteError extends MongooseError {
+ constructor(validationErrors, results, rawResult, operation) {
+ let preview = validationErrors.map(e => e.message).join(', ');
+ if (preview.length > 200) {
+ preview = preview.slice(0, 200) + '...';
+ }
+ super(`${operation} failed with ${validationErrors.length} Mongoose validation errors: ${preview}`);
+
+ this.validationErrors = validationErrors;
+ this.results = results;
+ this.rawResult = rawResult;
+ this.operation = operation;
+ }
+}
+
+Object.defineProperty(MongooseBulkWriteError.prototype, 'name', {
+ value: 'MongooseBulkWriteError'
+});
+
+/*!
+ * exports
+ */
+
+module.exports = MongooseBulkWriteError;
diff --git a/lib/error/cast.js b/lib/error/cast.js
index c42e8216691..115927117f7 100644
--- a/lib/error/cast.js
+++ b/lib/error/cast.js
@@ -20,10 +20,9 @@ class CastError extends MongooseError {
constructor(type, value, path, reason, schemaType) {
// If no args, assume we'll `init()` later.
if (arguments.length > 0) {
- const stringValue = getStringValue(value);
const valueType = getValueType(value);
const messageFormat = getMessageFormat(schemaType);
- const msg = formatMessage(null, type, stringValue, path, messageFormat, valueType, reason);
+ const msg = formatMessage(null, type, value, path, messageFormat, valueType, reason);
super(msg);
this.init(type, value, path, reason, schemaType);
} else {
@@ -76,8 +75,7 @@ class CastError extends MongooseError {
* ignore
*/
setModel(model) {
- this.model = model;
- this.message = formatMessage(model, this.kind, this.stringValue, this.path,
+ this.message = formatMessage(model, this.kind, this.value, this.path,
this.messageFormat, this.valueType);
}
}
@@ -111,10 +109,8 @@ function getValueType(value) {
}
function getMessageFormat(schemaType) {
- const messageFormat = schemaType &&
- schemaType.options &&
- schemaType.options.cast || null;
- if (typeof messageFormat === 'string') {
+ const messageFormat = schemaType && schemaType._castErrorMessage || null;
+ if (typeof messageFormat === 'string' || typeof messageFormat === 'function') {
return messageFormat;
}
}
@@ -123,8 +119,9 @@ function getMessageFormat(schemaType) {
* ignore
*/
-function formatMessage(model, kind, stringValue, path, messageFormat, valueType, reason) {
- if (messageFormat != null) {
+function formatMessage(model, kind, value, path, messageFormat, valueType, reason) {
+ if (typeof messageFormat === 'string') {
+ const stringValue = getStringValue(value);
let ret = messageFormat.
replace('{KIND}', kind).
replace('{VALUE}', stringValue).
@@ -134,7 +131,10 @@ function formatMessage(model, kind, stringValue, path, messageFormat, valueType,
}
return ret;
+ } else if (typeof messageFormat === 'function') {
+ return messageFormat(value, path, model, kind);
} else {
+ const stringValue = getStringValue(value);
const valueTypeMsg = valueType ? ' (type ' + valueType + ')' : '';
let ret = 'Cast to ' + kind + ' failed for value ' +
stringValue + valueTypeMsg + ' at path "' + path + '"';
diff --git a/lib/error/createCollectionsError.js b/lib/error/createCollectionsError.js
new file mode 100644
index 00000000000..4b69cae617e
--- /dev/null
+++ b/lib/error/createCollectionsError.js
@@ -0,0 +1,26 @@
+'use strict';
+
+const MongooseError = require('./mongooseError');
+
+/**
+ * createCollections Error constructor
+ *
+ * @param {String} message
+ * @param {String} errorsMap
+ * @inherits MongooseError
+ * @api private
+ */
+
+class CreateCollectionsError extends MongooseError {
+ constructor(message, errorsMap) {
+ super(message);
+ this.errors = errorsMap;
+ }
+}
+
+Object.defineProperty(CreateCollectionsError.prototype, 'name', {
+ value: 'CreateCollectionsError'
+});
+
+module.exports = CreateCollectionsError;
+
diff --git a/lib/error/disconnected.js b/lib/error/disconnected.js
deleted file mode 100644
index 9e8c6125c14..00000000000
--- a/lib/error/disconnected.js
+++ /dev/null
@@ -1,33 +0,0 @@
-/*!
- * Module dependencies.
- */
-
-'use strict';
-
-const MongooseError = require('./');
-
-
-/**
- * The connection failed to reconnect and will never successfully reconnect to
- * MongoDB without manual intervention.
- * @api private
- */
-class DisconnectedError extends MongooseError {
- /**
- * @param {String} connectionString
- */
- constructor(id, fnName) {
- super('Connection ' + id +
- ' was disconnected when calling `' + fnName + '()`');
- }
-}
-
-Object.defineProperty(DisconnectedError.prototype, 'name', {
- value: 'DisconnectedError'
-});
-
-/*!
- * exports
- */
-
-module.exports = DisconnectedError;
diff --git a/lib/error/divergentArray.js b/lib/error/divergentArray.js
index 7a8d1da5d34..bc3f1816264 100644
--- a/lib/error/divergentArray.js
+++ b/lib/error/divergentArray.js
@@ -5,14 +5,16 @@
'use strict';
-const MongooseError = require('./');
+const MongooseError = require('./mongooseError');
+
+/**
+ * DivergentArrayError constructor.
+ * @param {Array} paths
+ * @api private
+ */
class DivergentArrayError extends MongooseError {
- /**
- * DivergentArrayError constructor.
- * @param {Array} paths
- * @api private
- */
+
constructor(paths) {
const msg = 'For your own good, using `document.save()` to update an array '
+ 'which was selected using an $elemMatch projection OR '
@@ -21,7 +23,7 @@ class DivergentArrayError extends MongooseError {
+ 'the entire array is not supported. The following '
+ 'path(s) would have been modified unsafely:\n'
+ ' ' + paths.join('\n ') + '\n'
- + 'Use Model.update() to update these arrays instead.';
+ + 'Use Model.updateOne() to update these arrays instead.';
// TODO write up a docs page (FAQ) and link to it
super(msg);
}
diff --git a/lib/error/eachAsyncMultiError.js b/lib/error/eachAsyncMultiError.js
index 9c04020312b..b14156a09a3 100644
--- a/lib/error/eachAsyncMultiError.js
+++ b/lib/error/eachAsyncMultiError.js
@@ -4,7 +4,7 @@
'use strict';
-const MongooseError = require('./');
+const MongooseError = require('./mongooseError');
/**
diff --git a/lib/error/index.js b/lib/error/index.js
index d2a93849f5a..6e31a83ee82 100644
--- a/lib/error/index.js
+++ b/lib/error/index.js
@@ -25,19 +25,18 @@ const MongooseError = require('./mongooseError');
*
* - `MongooseError`: general Mongoose error
* - `CastError`: Mongoose could not convert a value to the type defined in the schema path. May be in a `ValidationError` class' `errors` property.
- * - `DisconnectedError`: This [connection](/docs/connections.html) timed out in trying to reconnect to MongoDB and will not successfully reconnect to MongoDB unless you explicitly reconnect.
* - `DivergentArrayError`: You attempted to `save()` an array that was modified after you loaded it with a `$elemMatch` or similar projection
- * - `MissingSchemaError`: You tried to access a model with [`mongoose.model()`](mongoose.html#mongoose_Mongoose-model) that was not defined
- * - `DocumentNotFoundError`: The document you tried to [`save()`](document.html#document_Document-save) was not found
+ * - `MissingSchemaError`: You tried to access a model with [`mongoose.model()`](https://mongoosejs.com/docs/api/mongoose.html#Mongoose.model()) that was not defined
+ * - `DocumentNotFoundError`: The document you tried to [`save()`](https://mongoosejs.com/docs/api/document.html#Document.prototype.save()) was not found
* - `ValidatorError`: error from an individual schema path's validator
- * - `ValidationError`: error returned from [`validate()`](document.html#document_Document-validate) or [`validateSync()`](document.html#document_Document-validateSync). Contains zero or more `ValidatorError` instances in `.errors` property.
+ * - `ValidationError`: error returned from [`validate()`](https://mongoosejs.com/docs/api/document.html#Document.prototype.validate()) or [`validateSync()`](https://mongoosejs.com/docs/api/document.html#Document.prototype.validateSync()). Contains zero or more `ValidatorError` instances in `.errors` property.
* - `MissingSchemaError`: You called `mongoose.Document()` without a schema
- * - `ObjectExpectedError`: Thrown when you set a nested path to a non-object value with [strict mode set](/docs/guide.html#strict).
+ * - `ObjectExpectedError`: Thrown when you set a nested path to a non-object value with [strict mode set](https://mongoosejs.com/docs/guide.html#strict).
* - `ObjectParameterError`: Thrown when you pass a non-object value to a function which expects an object as a paramter
- * - `OverwriteModelError`: Thrown when you call [`mongoose.model()`](mongoose.html#mongoose_Mongoose-model) to re-define a model that was already defined.
- * - `ParallelSaveError`: Thrown when you call [`save()`](model.html#model_Model-save) on a document when the same document instance is already saving.
- * - `StrictModeError`: Thrown when you set a path that isn't the schema and [strict mode](/docs/guide.html#strict) is set to `throw`.
- * - `VersionError`: Thrown when the [document is out of sync](/docs/guide.html#versionKey)
+ * - `OverwriteModelError`: Thrown when you call [`mongoose.model()`](https://mongoosejs.com/docs/api/mongoose.html#Mongoose.model()) to re-define a model that was already defined.
+ * - `ParallelSaveError`: Thrown when you call [`save()`](https://mongoosejs.com/docs/api/model.html#Model.prototype.save()) on a document when the same document instance is already saving.
+ * - `StrictModeError`: Thrown when you set a path that isn't the schema and [strict mode](https://mongoosejs.com/docs/guide.html#strict) is set to `throw`.
+ * - `VersionError`: Thrown when the [document is out of sync](https://mongoosejs.com/docs/guide.html#versionKey)
*
* @api public
* @property {String} name
@@ -54,7 +53,7 @@ module.exports = exports = MongooseError;
/**
* The default built-in validator error messages.
*
- * @see Error.messages #error_messages_MongooseError-messages
+ * @see Error.messages https://mongoosejs.com/docs/api/error.html#Error.messages
* @api public
* @memberOf Error
* @static
@@ -69,7 +68,7 @@ MongooseError.Messages = MongooseError.messages;
* An instance of this error class will be returned when `save()` fails
* because the underlying
* document was not found. The constructor takes one parameter, the
- * conditions that mongoose passed to `update()` when trying to update
+ * conditions that mongoose passed to `updateOne()` when trying to update
* the document.
*
* @api public
@@ -91,7 +90,7 @@ MongooseError.DocumentNotFoundError = require('./notFound');
MongooseError.CastError = require('./cast');
/**
- * An instance of this error class will be returned when [validation](/docs/validation.html) failed.
+ * An instance of this error class will be returned when [validation](https://mongoosejs.com/docs/validation.html) failed.
* The `errors` property contains an object whose keys are the paths that failed and whose values are
* instances of CastError or ValidationError.
*
@@ -140,7 +139,7 @@ MongooseError.ValidatorError = require('./validator');
/**
* An instance of this error class will be returned when you call `save()` after
* the document in the database was changed in a potentially unsafe way. See
- * the [`versionKey` option](/docs/guide.html#versionKey) for more information.
+ * the [`versionKey` option](https://mongoosejs.com/docs/guide.html#versionKey) for more information.
*
* @api public
* @memberOf Error
@@ -151,7 +150,7 @@ MongooseError.VersionError = require('./version');
/**
* An instance of this error class will be returned when you call `save()` multiple
- * times on the same document in parallel. See the [FAQ](/docs/faq.html) for more
+ * times on the same document in parallel. See the [FAQ](https://mongoosejs.com/docs/faq.html) for more
* information.
*
* @api public
@@ -163,7 +162,7 @@ MongooseError.ParallelSaveError = require('./parallelSave');
/**
* Thrown when a model with the given name was already registered on the connection.
- * See [the FAQ about `OverwriteModelError`](/docs/faq.html#overwrite-model-error).
+ * See [the FAQ about `OverwriteModelError`](https://mongoosejs.com/docs/faq.html#overwrite-model-error).
*
* @api public
* @memberOf Error
diff --git a/lib/error/invalidSchemaOption.js b/lib/error/invalidSchemaOption.js
new file mode 100644
index 00000000000..9e7e4ff4f17
--- /dev/null
+++ b/lib/error/invalidSchemaOption.js
@@ -0,0 +1,32 @@
+
+/*!
+ * Module dependencies.
+ */
+
+'use strict';
+
+const MongooseError = require('./mongooseError');
+
+/**
+ * InvalidSchemaOption Error constructor.
+ * @param {String} name
+ * @api private
+ */
+
+class InvalidSchemaOptionError extends MongooseError {
+
+ constructor(name, option) {
+ const msg = `Cannot create use schema for property "${name}" because the schema has the ${option} option enabled.`;
+ super(msg);
+ }
+}
+
+Object.defineProperty(InvalidSchemaOptionError.prototype, 'name', {
+ value: 'InvalidSchemaOptionError'
+});
+
+/*!
+ * exports
+ */
+
+module.exports = InvalidSchemaOptionError;
diff --git a/lib/error/messages.js b/lib/error/messages.js
index b750d5d5ec2..a2db50a6fce 100644
--- a/lib/error/messages.js
+++ b/lib/error/messages.js
@@ -6,7 +6,7 @@
* const mongoose = require('mongoose');
* mongoose.Error.messages.String.enum = "Your custom message for {PATH}.";
*
- * As you might have noticed, error messages support basic templating
+ * Error messages support basic templating. Mongoose will replace the following strings with the corresponding value.
*
* - `{PATH}` is replaced with the invalid document path
* - `{VALUE}` is replaced with the invalid value
diff --git a/lib/error/missingSchema.js b/lib/error/missingSchema.js
index 50c81054a90..790f7853848 100644
--- a/lib/error/missingSchema.js
+++ b/lib/error/missingSchema.js
@@ -5,14 +5,16 @@
'use strict';
-const MongooseError = require('./');
+const MongooseError = require('./mongooseError');
+
+/**
+ * MissingSchema Error constructor.
+ * @param {String} name
+ * @api private
+ */
class MissingSchemaError extends MongooseError {
- /**
- * MissingSchema Error constructor.
- * @param {String} name
- * @api private
- */
+
constructor(name) {
const msg = 'Schema hasn\'t been registered for model "' + name + '".\n'
+ 'Use mongoose.model(name, schema)';
diff --git a/lib/error/notFound.js b/lib/error/notFound.js
index e1064bb89d9..87fdd8bc649 100644
--- a/lib/error/notFound.js
+++ b/lib/error/notFound.js
@@ -4,14 +4,16 @@
* Module dependencies.
*/
-const MongooseError = require('./');
+const MongooseError = require('./mongooseError');
const util = require('util');
+/**
+ * OverwriteModel Error constructor.
+ * @api private
+ */
+
class DocumentNotFoundError extends MongooseError {
- /**
- * OverwriteModel Error constructor.
- * @api private
- */
+
constructor(filter, model, numAffected, result) {
let msg;
const messages = MongooseError.messages;
diff --git a/lib/error/objectExpected.js b/lib/error/objectExpected.js
index 6506f60656a..bd89ffc77e1 100644
--- a/lib/error/objectExpected.js
+++ b/lib/error/objectExpected.js
@@ -4,17 +4,18 @@
'use strict';
-const MongooseError = require('./');
+const MongooseError = require('./mongooseError');
+/**
+ * Strict mode error constructor
+ *
+ * @param {string} type
+ * @param {string} value
+ * @api private
+ */
class ObjectExpectedError extends MongooseError {
- /**
- * Strict mode error constructor
- *
- * @param {string} type
- * @param {string} value
- * @api private
- */
+
constructor(path, val) {
const typeDescription = Array.isArray(val) ? 'array' : 'primitive value';
super('Tried to set nested object field `' + path +
diff --git a/lib/error/objectParameter.js b/lib/error/objectParameter.js
index 295582e2484..0a2108e5c9b 100644
--- a/lib/error/objectParameter.js
+++ b/lib/error/objectParameter.js
@@ -4,18 +4,20 @@
'use strict';
-const MongooseError = require('./');
+const MongooseError = require('./mongooseError');
+
+/**
+ * Constructor for errors that happen when a parameter that's expected to be
+ * an object isn't an object
+ *
+ * @param {Any} value
+ * @param {String} paramName
+ * @param {String} fnName
+ * @api private
+ */
class ObjectParameterError extends MongooseError {
- /**
- * Constructor for errors that happen when a parameter that's expected to be
- * an object isn't an object
- *
- * @param {Any} value
- * @param {String} paramName
- * @param {String} fnName
- * @api private
- */
+
constructor(value, paramName, fnName) {
super('Parameter "' + paramName + '" to ' + fnName +
'() must be an object, got "' + value.toString() + '" (type ' + typeof value + ')');
diff --git a/lib/error/overwriteModel.js b/lib/error/overwriteModel.js
index 1ff180b0498..ef828f91731 100644
--- a/lib/error/overwriteModel.js
+++ b/lib/error/overwriteModel.js
@@ -5,15 +5,16 @@
'use strict';
-const MongooseError = require('./');
+const MongooseError = require('./mongooseError');
+/**
+ * OverwriteModel Error constructor.
+ * @param {String} name
+ * @api private
+ */
class OverwriteModelError extends MongooseError {
- /**
- * OverwriteModel Error constructor.
- * @param {String} name
- * @api private
- */
+
constructor(name) {
super('Cannot overwrite `' + name + '` model once compiled.');
}
diff --git a/lib/error/parallelSave.js b/lib/error/parallelSave.js
index e0628576de7..fd554fa3bbc 100644
--- a/lib/error/parallelSave.js
+++ b/lib/error/parallelSave.js
@@ -4,18 +4,21 @@
* Module dependencies.
*/
-const MongooseError = require('./');
+const MongooseError = require('./mongooseError');
+
+
+/**
+ * ParallelSave Error constructor.
+ *
+ * @param {Document} doc
+ * @api private
+ */
class ParallelSaveError extends MongooseError {
- /**
- * ParallelSave Error constructor.
- *
- * @param {Document} doc
- * @api private
- */
+
constructor(doc) {
const msg = 'Can\'t save() the same doc multiple times in parallel. Document: ';
- super(msg + doc._id);
+ super(msg + doc._doc._id);
}
}
diff --git a/lib/error/parallelValidate.js b/lib/error/parallelValidate.js
index 477954dc870..d70e296e869 100644
--- a/lib/error/parallelValidate.js
+++ b/lib/error/parallelValidate.js
@@ -7,16 +7,18 @@
const MongooseError = require('./mongooseError');
+/**
+ * ParallelValidate Error constructor.
+ *
+ * @param {Document} doc
+ * @api private
+ */
+
class ParallelValidateError extends MongooseError {
- /**
- * ParallelValidate Error constructor.
- *
- * @param {Document} doc
- * @api private
- */
+
constructor(doc) {
const msg = 'Can\'t validate() the same doc multiple times in parallel. Document: ';
- super(msg + doc._id);
+ super(msg + doc._doc._id);
}
}
diff --git a/lib/error/setOptionError.js b/lib/error/setOptionError.js
index b38a0d30244..369096fd306 100644
--- a/lib/error/setOptionError.js
+++ b/lib/error/setOptionError.js
@@ -8,13 +8,15 @@ const MongooseError = require('./mongooseError');
const util = require('util');
const combinePathErrors = require('../helpers/error/combinePathErrors');
+/**
+ * Mongoose.set Error
+ *
+ * @api private
+ * @inherits MongooseError
+ */
+
class SetOptionError extends MongooseError {
- /**
- * Mongoose.set Error
- *
- * @api private
- * @inherits MongooseError
- */
+
constructor() {
super('');
diff --git a/lib/error/strict.js b/lib/error/strict.js
index 393ca6e1fc7..eda7d9ae6f5 100644
--- a/lib/error/strict.js
+++ b/lib/error/strict.js
@@ -4,19 +4,21 @@
'use strict';
-const MongooseError = require('./');
+const MongooseError = require('./mongooseError');
+
+/**
+ * Strict mode error constructor
+ *
+ * @param {String} path
+ * @param {String} [msg]
+ * @param {Boolean} [immutable]
+ * @inherits MongooseError
+ * @api private
+ */
class StrictModeError extends MongooseError {
- /**
- * Strict mode error constructor
- *
- * @param {String} path
- * @param {String} [msg]
- * @param {Boolean} [immutable]
- * @inherits MongooseError
- * @api private
- */
+
constructor(path, msg, immutable) {
msg = msg || 'Field `' + path + '` is not in schema and strict ' +
'mode is set to throw.';
diff --git a/lib/error/strictPopulate.js b/lib/error/strictPopulate.js
index f7addfa5287..d554d71271d 100644
--- a/lib/error/strictPopulate.js
+++ b/lib/error/strictPopulate.js
@@ -4,17 +4,19 @@
'use strict';
-const MongooseError = require('./');
+const MongooseError = require('./mongooseError');
+
+/**
+ * Strict mode error constructor
+ *
+ * @param {String} path
+ * @param {String} [msg]
+ * @inherits MongooseError
+ * @api private
+ */
class StrictPopulateError extends MongooseError {
- /**
- * Strict mode error constructor
- *
- * @param {String} path
- * @param {String} [msg]
- * @inherits MongooseError
- * @api private
- */
+
constructor(path, msg) {
msg = msg || 'Cannot populate path `' + path + '` because it is not in your schema. ' + 'Set the `strictPopulate` option to false to override.';
super(msg);
diff --git a/lib/error/validation.js b/lib/error/validation.js
index 5e222e980f9..faa4ea799aa 100644
--- a/lib/error/validation.js
+++ b/lib/error/validation.js
@@ -9,14 +9,16 @@ const getConstructorName = require('../helpers/getConstructorName');
const util = require('util');
const combinePathErrors = require('../helpers/error/combinePathErrors');
+/**
+ * Document Validation Error
+ *
+ * @api private
+ * @param {Document} [instance]
+ * @inherits MongooseError
+ */
+
class ValidationError extends MongooseError {
- /**
- * Document Validation Error
- *
- * @api private
- * @param {Document} [instance]
- * @inherits MongooseError
- */
+
constructor(instance) {
let _message;
if (getConstructorName(instance) === 'model') {
diff --git a/lib/error/validator.js b/lib/error/validator.js
index 4ca7316d7bf..38f98f0087d 100644
--- a/lib/error/validator.js
+++ b/lib/error/validator.js
@@ -4,17 +4,18 @@
'use strict';
-const MongooseError = require('./');
+const MongooseError = require('./mongooseError');
+/**
+ * Schema validator error
+ *
+ * @param {Object} properties
+ * @param {Document} doc
+ * @api private
+ */
class ValidatorError extends MongooseError {
- /**
- * Schema validator error
- *
- * @param {Object} properties
- * @param {Document} doc
- * @api private
- */
+
constructor(properties, doc) {
let msg = properties.message;
if (!msg) {
diff --git a/lib/error/version.js b/lib/error/version.js
index b357fb16ca3..4eb8054cdfb 100644
--- a/lib/error/version.js
+++ b/lib/error/version.js
@@ -4,20 +4,22 @@
* Module dependencies.
*/
-const MongooseError = require('./');
+const MongooseError = require('./mongooseError');
+
+/**
+ * Version Error constructor.
+ *
+ * @param {Document} doc
+ * @param {Number} currentVersion
+ * @param {Array} modifiedPaths
+ * @api private
+ */
class VersionError extends MongooseError {
- /**
- * Version Error constructor.
- *
- * @param {Document} doc
- * @param {Number} currentVersion
- * @param {Array} modifiedPaths
- * @api private
- */
+
constructor(doc, currentVersion, modifiedPaths) {
const modifiedPathsStr = modifiedPaths.join(', ');
- super('No matching document found for id "' + doc._id +
+ super('No matching document found for id "' + doc._doc._id +
'" version ' + currentVersion + ' modifiedPaths "' + modifiedPathsStr + '"');
this.version = currentVersion;
this.modifiedPaths = modifiedPaths;
diff --git a/lib/helpers/clone.js b/lib/helpers/clone.js
index f7ee5d8d2a8..a8dd587dbf9 100644
--- a/lib/helpers/clone.js
+++ b/lib/helpers/clone.js
@@ -6,18 +6,19 @@ const specialProperties = require('./specialProperties');
const isMongooseObject = require('./isMongooseObject');
const getFunctionName = require('./getFunctionName');
const isBsonType = require('./isBsonType');
+const isMongooseArray = require('../types/array/isMongooseArray').isMongooseArray;
const isObject = require('./isObject');
+const isPOJO = require('./isPOJO');
const symbols = require('./symbols');
const trustedSymbol = require('./query/trusted').trustedSymbol;
-const utils = require('../utils');
-
+const BSON = require('bson');
/**
* Object clone with Mongoose natives support.
*
* If options.minimize is true, creates a minimal data object. Empty objects and undefined values will not be cloned. This makes the data payload sent to MongoDB as small as possible.
*
- * Functions are never cloned.
+ * Functions and primitives are never cloned.
*
* @param {Object} obj the object to clone
* @param {Object} options
@@ -31,19 +32,34 @@ function clone(obj, options, isArrayChild) {
return obj;
}
+ if (isBsonType(obj, 'Double')) {
+ return new BSON.Double(obj.value);
+ }
+ if (typeof obj === 'number' || typeof obj === 'string' || typeof obj === 'boolean' || typeof obj === 'bigint') {
+ return obj;
+ }
+
if (Array.isArray(obj)) {
- return cloneArray(utils.isMongooseArray(obj) ? obj.__array : obj, options);
+ return cloneArray(isMongooseArray(obj) ? obj.__array : obj, options);
}
if (isMongooseObject(obj)) {
- // Single nested subdocs should apply getters later in `applyGetters()`
- // when calling `toObject()`. See gh-7442, gh-8295
- if (options && options._skipSingleNestedGetters && obj.$isSingleNested) {
- options = Object.assign({}, options, { getters: false });
+ if (options) {
+ if (options.retainDocuments && obj.$__ != null) {
+ const clonedDoc = obj.$clone();
+ if (obj.__index != null) {
+ clonedDoc.__index = obj.__index;
+ }
+ if (obj.__parentArray != null) {
+ clonedDoc.__parentArray = obj.__parentArray;
+ }
+ clonedDoc.$__parent = obj.$__parent;
+ return clonedDoc;
+ }
}
const isSingleNested = obj.$isSingleNested;
- if (utils.isPOJO(obj) && obj.$__ != null && obj._doc != null) {
+ if (isPOJO(obj) && obj.$__ != null && obj._doc != null) {
return obj._doc;
}
@@ -54,7 +70,7 @@ function clone(obj, options, isArrayChild) {
ret = obj.toObject(options);
}
- if (options && options.minimize && isSingleNested && Object.keys(ret).length === 0) {
+ if (options && options.minimize && !obj.constructor.$__required && isSingleNested && Object.keys(ret).length === 0) {
return undefined;
}
@@ -77,7 +93,10 @@ function clone(obj, options, isArrayChild) {
}
}
- if (isBsonType(obj, 'ObjectID')) {
+ if (isBsonType(obj, 'ObjectId')) {
+ if (options && options.flattenObjectIds) {
+ return obj.toJSON();
+ }
return new ObjectId(obj.id);
}
@@ -132,13 +151,12 @@ function cloneObject(obj, options, isArrayChild) {
ret[trustedSymbol] = obj[trustedSymbol];
}
- let i = 0;
- let key = '';
const keys = Object.keys(obj);
const len = keys.length;
- for (i = 0; i < len; ++i) {
- if (specialProperties.has(key = keys[i])) {
+ for (let i = 0; i < len; ++i) {
+ const key = keys[i];
+ if (specialProperties.has(key)) {
continue;
}
diff --git a/lib/helpers/common.js b/lib/helpers/common.js
index ec7524da4c9..5a1bee1c313 100644
--- a/lib/helpers/common.js
+++ b/lib/helpers/common.js
@@ -4,7 +4,7 @@
* Module dependencies.
*/
-const Binary = require('../driver').get().Binary;
+const Binary = require('bson').Binary;
const isBsonType = require('./isBsonType');
const isMongooseObject = require('./isMongooseObject');
const MongooseError = require('../error');
@@ -119,7 +119,7 @@ function shouldFlatten(val) {
return val &&
typeof val === 'object' &&
!(val instanceof Date) &&
- !isBsonType(val, 'ObjectID') &&
+ !isBsonType(val, 'ObjectId') &&
(!Array.isArray(val) || val.length !== 0) &&
!(val instanceof Buffer) &&
!isBsonType(val, 'Decimal128') &&
diff --git a/lib/helpers/cursor/eachAsync.js b/lib/helpers/cursor/eachAsync.js
index e3f6f8a24f4..c52c6d56648 100644
--- a/lib/helpers/cursor/eachAsync.js
+++ b/lib/helpers/cursor/eachAsync.js
@@ -6,7 +6,6 @@
const EachAsyncMultiError = require('../../error/eachAsyncMultiError');
const immediate = require('../immediate');
-const promiseOrCallback = require('../promiseOrCallback');
/**
* Execute `fn` for every document in the cursor. If `fn` returns a promise,
@@ -19,13 +18,12 @@ const promiseOrCallback = require('../promiseOrCallback');
* @param {Number} [options.batchSize=null] if set, Mongoose will call `fn` with an array of at most `batchSize` documents, instead of a single document
* @param {Number} [options.parallel=1] maximum number of `fn` calls that Mongoose will run in parallel
* @param {AbortSignal} [options.signal] allow cancelling this eachAsync(). Once the abort signal is fired, `eachAsync()` will immediately fulfill the returned promise (or call the callback) and not fetch any more documents.
- * @param {Function} [callback] executed when all docs have been processed
* @return {Promise}
* @api public
* @method eachAsync
*/
-module.exports = function eachAsync(next, fn, options, callback) {
+module.exports = async function eachAsync(next, fn, options) {
const parallel = options.parallel || 1;
const batchSize = options.batchSize;
const signal = options.signal;
@@ -35,15 +33,15 @@ module.exports = function eachAsync(next, fn, options, callback) {
let aborted = false;
- return promiseOrCallback(callback, cb => {
+ return new Promise((resolve, reject) => {
if (signal != null) {
if (signal.aborted) {
- return cb(null);
+ return resolve(null);
}
signal.addEventListener('abort', () => {
aborted = true;
- return cb(null);
+ return resolve(null);
}, { once: true });
}
@@ -57,7 +55,12 @@ module.exports = function eachAsync(next, fn, options, callback) {
}
}
- iterate(cb);
+ iterate((err, res) => {
+ if (err != null) {
+ return reject(err);
+ }
+ resolve(res);
+ });
});
function iterate(finalCallback) {
diff --git a/lib/helpers/discriminator/applyEmbeddedDiscriminators.js b/lib/helpers/discriminator/applyEmbeddedDiscriminators.js
new file mode 100644
index 00000000000..01593a9b316
--- /dev/null
+++ b/lib/helpers/discriminator/applyEmbeddedDiscriminators.js
@@ -0,0 +1,36 @@
+'use strict';
+
+module.exports = applyEmbeddedDiscriminators;
+
+function applyEmbeddedDiscriminators(schema, seen = new WeakSet(), overwriteExisting = false) {
+ if (seen.has(schema)) {
+ return;
+ }
+ seen.add(schema);
+ for (const path of Object.keys(schema.paths)) {
+ const schemaType = schema.paths[path];
+ if (!schemaType.schema) {
+ continue;
+ }
+ applyEmbeddedDiscriminators(schemaType.schema, seen);
+ if (!schemaType.schema._applyDiscriminators) {
+ continue;
+ }
+ if (schemaType._appliedDiscriminators && !overwriteExisting) {
+ continue;
+ }
+ for (const discriminatorKey of schemaType.schema._applyDiscriminators.keys()) {
+ const {
+ schema: discriminatorSchema,
+ options
+ } = schemaType.schema._applyDiscriminators.get(discriminatorKey);
+ applyEmbeddedDiscriminators(discriminatorSchema, seen);
+ schemaType.discriminator(
+ discriminatorKey,
+ discriminatorSchema,
+ overwriteExisting ? { ...options, overwriteExisting: true } : options
+ );
+ }
+ schemaType._appliedDiscriminators = true;
+ }
+}
diff --git a/lib/helpers/discriminator/areDiscriminatorValuesEqual.js b/lib/helpers/discriminator/areDiscriminatorValuesEqual.js
index 68ea9390698..1b81a48f2d3 100644
--- a/lib/helpers/discriminator/areDiscriminatorValuesEqual.js
+++ b/lib/helpers/discriminator/areDiscriminatorValuesEqual.js
@@ -9,7 +9,7 @@ module.exports = function areDiscriminatorValuesEqual(a, b) {
if (typeof a === 'number' && typeof b === 'number') {
return a === b;
}
- if (isBsonType(a, 'ObjectID') && isBsonType(b, 'ObjectID')) {
+ if (isBsonType(a, 'ObjectId') && isBsonType(b, 'ObjectId')) {
return a.toString() === b.toString();
}
return false;
diff --git a/lib/helpers/discriminator/getConstructor.js b/lib/helpers/discriminator/getConstructor.js
index 7a821c5c99f..17583e271e9 100644
--- a/lib/helpers/discriminator/getConstructor.js
+++ b/lib/helpers/discriminator/getConstructor.js
@@ -7,15 +7,18 @@ const getDiscriminatorByValue = require('./getDiscriminatorByValue');
* @api private
*/
-module.exports = function getConstructor(Constructor, value) {
+module.exports = function getConstructor(Constructor, value, defaultDiscriminatorValue) {
const discriminatorKey = Constructor.schema.options.discriminatorKey;
- if (value != null &&
- Constructor.discriminators &&
- value[discriminatorKey] != null) {
- if (Constructor.discriminators[value[discriminatorKey]]) {
- Constructor = Constructor.discriminators[value[discriminatorKey]];
+ let discriminatorValue = (value != null && value[discriminatorKey]);
+ if (discriminatorValue == null) {
+ discriminatorValue = defaultDiscriminatorValue;
+ }
+ if (Constructor.discriminators &&
+ discriminatorValue != null) {
+ if (Constructor.discriminators[discriminatorValue]) {
+ Constructor = Constructor.discriminators[discriminatorValue];
} else {
- const constructorByValue = getDiscriminatorByValue(Constructor.discriminators, value[discriminatorKey]);
+ const constructorByValue = getDiscriminatorByValue(Constructor.discriminators, discriminatorValue);
if (constructorByValue) {
Constructor = constructorByValue;
}
diff --git a/lib/helpers/discriminator/mergeDiscriminatorSchema.js b/lib/helpers/discriminator/mergeDiscriminatorSchema.js
index bbe4503f15c..b48d5c4db07 100644
--- a/lib/helpers/discriminator/mergeDiscriminatorSchema.js
+++ b/lib/helpers/discriminator/mergeDiscriminatorSchema.js
@@ -13,18 +13,31 @@ const isObject = require('../../helpers/isObject');
* @api private
*/
-module.exports = function mergeDiscriminatorSchema(to, from, path) {
+module.exports = function mergeDiscriminatorSchema(to, from, path, seen) {
const keys = Object.keys(from);
let i = 0;
const len = keys.length;
let key;
path = path || '';
+ seen = seen || new WeakSet();
+
+ if (seen.has(from)) {
+ return;
+ }
+ seen.add(from);
while (i < len) {
key = keys[i++];
- if (key === 'discriminators' || key === 'base' || key === '_applyDiscriminators') {
- continue;
+ if (!path) {
+ if (key === 'discriminators' ||
+ key === 'base' ||
+ key === '_applyDiscriminators' ||
+ key === '_userProvidedOptions' ||
+ key === 'options' ||
+ key === 'tree') {
+ continue;
+ }
}
if (path === 'tree' && from != null && from.instanceOfSchema) {
continue;
@@ -53,12 +66,16 @@ module.exports = function mergeDiscriminatorSchema(to, from, path) {
to[key] = from[key].clone();
}
continue;
- } else if (isBsonType(from[key], 'ObjectID')) {
+ } else if (isBsonType(from[key], 'ObjectId')) {
to[key] = new ObjectId(from[key]);
continue;
}
}
- mergeDiscriminatorSchema(to[key], from[key], path ? path + '.' + key : key);
+ mergeDiscriminatorSchema(to[key], from[key], path ? path + '.' + key : key, seen);
}
}
+
+ if (from != null && from.instanceOfSchema) {
+ to.tree = Object.assign({}, from.tree, to.tree);
+ }
};
diff --git a/lib/helpers/document/applyDefaults.js b/lib/helpers/document/applyDefaults.js
index 10a4e1ffefd..258e570ec30 100644
--- a/lib/helpers/document/applyDefaults.js
+++ b/lib/helpers/document/applyDefaults.js
@@ -1,8 +1,11 @@
'use strict';
-module.exports = function applyDefaults(doc, fields, exclude, hasIncludedChildren, isBeforeSetters, pathsToSkip) {
+const isNestedProjection = require('../projection/isNestedProjection');
+
+module.exports = function applyDefaults(doc, fields, exclude, hasIncludedChildren, isBeforeSetters, pathsToSkip, options) {
const paths = Object.keys(doc.$__schema.paths);
const plen = paths.length;
+ const skipParentChangeTracking = options && options.skipParentChangeTracking;
for (let i = 0; i < plen; ++i) {
let def;
@@ -32,7 +35,7 @@ module.exports = function applyDefaults(doc, fields, exclude, hasIncludedChildre
}
} else if (exclude === false && fields && !included) {
const hasSubpaths = type.$isSingleNested || type.$isMongooseDocumentArray;
- if (curPath in fields || (j === len - 1 && hasSubpaths && hasIncludedChildren != null && hasIncludedChildren[curPath])) {
+ if ((curPath in fields && !isNestedProjection(fields[curPath])) || (j === len - 1 && hasSubpaths && hasIncludedChildren != null && hasIncludedChildren[curPath])) {
included = true;
} else if (hasIncludedChildren != null && !hasIncludedChildren[curPath]) {
break;
@@ -78,7 +81,7 @@ module.exports = function applyDefaults(doc, fields, exclude, hasIncludedChildre
if (typeof def !== 'undefined') {
doc_[piece] = def;
- applyChangeTracking(doc, p);
+ applyChangeTracking(doc, p, skipParentChangeTracking);
}
} else if (included) {
// selected field
@@ -91,7 +94,7 @@ module.exports = function applyDefaults(doc, fields, exclude, hasIncludedChildre
if (typeof def !== 'undefined') {
doc_[piece] = def;
- applyChangeTracking(doc, p);
+ applyChangeTracking(doc, p, skipParentChangeTracking);
}
}
} else {
@@ -104,7 +107,7 @@ module.exports = function applyDefaults(doc, fields, exclude, hasIncludedChildre
if (typeof def !== 'undefined') {
doc_[piece] = def;
- applyChangeTracking(doc, p);
+ applyChangeTracking(doc, p, skipParentChangeTracking);
}
}
} else {
@@ -118,9 +121,9 @@ module.exports = function applyDefaults(doc, fields, exclude, hasIncludedChildre
* ignore
*/
-function applyChangeTracking(doc, fullPath) {
+function applyChangeTracking(doc, fullPath, skipParentChangeTracking) {
doc.$__.activePaths.default(fullPath);
- if (doc.$isSubdocument && doc.$isSingleNested && doc.$parent() != null) {
+ if (!skipParentChangeTracking && doc.$isSubdocument && doc.$isSingleNested && doc.$parent() != null) {
doc.$parent().$__.activePaths.default(doc.$__pathRelativeToParent(fullPath));
}
}
diff --git a/lib/helpers/document/applyTimestamps.js b/lib/helpers/document/applyTimestamps.js
new file mode 100644
index 00000000000..425e144c867
--- /dev/null
+++ b/lib/helpers/document/applyTimestamps.js
@@ -0,0 +1,105 @@
+'use strict';
+
+const handleTimestampOption = require('../schema/handleTimestampOption');
+const mpath = require('mpath');
+
+module.exports = applyTimestamps;
+
+/**
+ * Apply a given schema's timestamps to the given POJO
+ *
+ * @param {Schema} schema
+ * @param {Object} obj
+ * @param {Object} [options]
+ * @param {Boolean} [options.isUpdate=false] if true, treat this as an update: just set updatedAt, skip setting createdAt. If false, set both createdAt and updatedAt
+ * @param {Function} [options.currentTime] if set, Mongoose will call this function to get the current time.
+ */
+
+function applyTimestamps(schema, obj, options) {
+ if (obj == null) {
+ return obj;
+ }
+
+ applyTimestampsToChildren(schema, obj, options);
+ return applyTimestampsToDoc(schema, obj, options);
+}
+
+/**
+ * Apply timestamps to any subdocuments
+ *
+ * @param {Schema} schema subdocument schema
+ * @param {Object} res subdocument
+ * @param {Object} [options]
+ * @param {Boolean} [options.isUpdate=false] if true, treat this as an update: just set updatedAt, skip setting createdAt. If false, set both createdAt and updatedAt
+ * @param {Function} [options.currentTime] if set, Mongoose will call this function to get the current time.
+ */
+
+function applyTimestampsToChildren(schema, res, options) {
+ for (const childSchema of schema.childSchemas) {
+ const _path = childSchema.model.path;
+ const _schema = childSchema.schema;
+ if (!_path) {
+ continue;
+ }
+ const _obj = mpath.get(_path, res);
+ if (_obj == null || (Array.isArray(_obj) && _obj.flat(Infinity).length === 0)) {
+ continue;
+ }
+
+ applyTimestamps(_schema, _obj, options);
+ }
+}
+
+/**
+ * Apply timestamps to a given document. Does not apply timestamps to subdocuments: use `applyTimestampsToChildren` instead
+ *
+ * @param {Schema} schema
+ * @param {Object} obj
+ * @param {Object} [options]
+ * @param {Boolean} [options.isUpdate=false] if true, treat this as an update: just set updatedAt, skip setting createdAt. If false, set both createdAt and updatedAt
+ * @param {Function} [options.currentTime] if set, Mongoose will call this function to get the current time.
+ */
+
+function applyTimestampsToDoc(schema, obj, options) {
+ if (obj == null || typeof obj !== 'object') {
+ return;
+ }
+ if (Array.isArray(obj)) {
+ for (const el of obj) {
+ applyTimestampsToDoc(schema, el, options);
+ }
+ return;
+ }
+
+ if (schema.discriminators && Object.keys(schema.discriminators).length > 0) {
+ for (const discriminatorKey of Object.keys(schema.discriminators)) {
+ const discriminator = schema.discriminators[discriminatorKey];
+ const key = discriminator.discriminatorMapping.key;
+ const value = discriminator.discriminatorMapping.value;
+ if (obj[key] == value) {
+ schema = discriminator;
+ break;
+ }
+ }
+ }
+
+ const createdAt = handleTimestampOption(schema.options.timestamps, 'createdAt');
+ const updatedAt = handleTimestampOption(schema.options.timestamps, 'updatedAt');
+ const currentTime = options?.currentTime;
+
+ let ts = null;
+ if (currentTime != null) {
+ ts = currentTime();
+ } else if (schema.base?.now) {
+ ts = schema.base.now();
+ } else {
+ ts = new Date();
+ }
+
+ if (createdAt && obj[createdAt] == null && !options?.isUpdate) {
+ obj[createdAt] = ts;
+ }
+ if (updatedAt) {
+ obj[updatedAt] = ts;
+ }
+}
diff --git a/lib/helpers/document/applyVirtuals.js b/lib/helpers/document/applyVirtuals.js
new file mode 100644
index 00000000000..5fbe7ca82ba
--- /dev/null
+++ b/lib/helpers/document/applyVirtuals.js
@@ -0,0 +1,146 @@
+'use strict';
+
+const mpath = require('mpath');
+
+module.exports = applyVirtuals;
+
+/**
+ * Apply a given schema's virtuals to a given POJO
+ *
+ * @param {Schema} schema
+ * @param {Object} obj
+ * @param {Array} [virtuals] optional whitelist of virtuals to apply
+ * @returns
+ */
+
+function applyVirtuals(schema, obj, virtuals) {
+ if (obj == null) {
+ return obj;
+ }
+
+ let virtualsForChildren = virtuals;
+ let toApply = null;
+
+ if (Array.isArray(virtuals)) {
+ virtualsForChildren = [];
+ toApply = [];
+ for (const virtual of virtuals) {
+ if (virtual.length === 1) {
+ toApply.push(virtual[0]);
+ } else {
+ virtualsForChildren.push(virtual);
+ }
+ }
+ }
+
+ applyVirtualsToChildren(schema, obj, virtualsForChildren);
+ return applyVirtualsToDoc(schema, obj, toApply);
+}
+
+/**
+ * Apply virtuals to any subdocuments
+ *
+ * @param {Schema} schema subdocument schema
+ * @param {Object} res subdocument
+ * @param {Array} [virtuals] optional whitelist of virtuals to apply
+ */
+
+function applyVirtualsToChildren(schema, res, virtuals) {
+ let attachedVirtuals = false;
+ for (const childSchema of schema.childSchemas) {
+ const _path = childSchema.model.path;
+ const _schema = childSchema.schema;
+ if (!_path) {
+ continue;
+ }
+ const _obj = mpath.get(_path, res);
+ if (_obj == null || (Array.isArray(_obj) && _obj.flat(Infinity).length === 0)) {
+ continue;
+ }
+
+ let virtualsForChild = null;
+ if (Array.isArray(virtuals)) {
+ virtualsForChild = [];
+ for (const virtual of virtuals) {
+ if (virtual[0] == _path) {
+ virtualsForChild.push(virtual.slice(1));
+ }
+ }
+
+ if (virtualsForChild.length === 0) {
+ continue;
+ }
+ }
+
+ applyVirtuals(_schema, _obj, virtualsForChild);
+ attachedVirtuals = true;
+ }
+
+ if (virtuals && virtuals.length && !attachedVirtuals) {
+ applyVirtualsToDoc(schema, res, virtuals);
+ }
+}
+
+/**
+ * Apply virtuals to a given document. Does not apply virtuals to subdocuments: use `applyVirtualsToChildren` instead
+ *
+ * @param {Schema} schema
+ * @param {Object} doc
+ * @param {Array} [virtuals] optional whitelist of virtuals to apply
+ * @returns
+ */
+
+function applyVirtualsToDoc(schema, obj, virtuals) {
+ if (obj == null || typeof obj !== 'object') {
+ return;
+ }
+ if (Array.isArray(obj)) {
+ for (const el of obj) {
+ applyVirtualsToDoc(schema, el, virtuals);
+ }
+ return;
+ }
+
+ if (schema.discriminators && Object.keys(schema.discriminators).length > 0) {
+ for (const discriminatorKey of Object.keys(schema.discriminators)) {
+ const discriminator = schema.discriminators[discriminatorKey];
+ const key = discriminator.discriminatorMapping.key;
+ const value = discriminator.discriminatorMapping.value;
+ if (obj[key] == value) {
+ schema = discriminator;
+ break;
+ }
+ }
+ }
+
+ if (virtuals == null) {
+ virtuals = Object.keys(schema.virtuals);
+ }
+ for (const virtual of virtuals) {
+ if (schema.virtuals[virtual] == null) {
+ continue;
+ }
+ const virtualType = schema.virtuals[virtual];
+ const sp = Array.isArray(virtual)
+ ? virtual
+ : virtual.indexOf('.') === -1
+ ? [virtual]
+ : virtual.split('.');
+ let cur = obj;
+ for (let i = 0; i < sp.length - 1; ++i) {
+ cur[sp[i]] = sp[i] in cur ? cur[sp[i]] : {};
+ cur = cur[sp[i]];
+ }
+ let val = virtualType.applyGetters(cur[sp[sp.length - 1]], obj);
+ const isPopulateVirtual =
+ virtualType.options && (virtualType.options.ref || virtualType.options.refPath);
+ if (isPopulateVirtual && val === undefined) {
+ if (virtualType.options.justOne) {
+ val = null;
+ } else {
+ val = [];
+ }
+ }
+ cur[sp[sp.length - 1]] = val;
+ }
+}
diff --git a/lib/helpers/document/cleanModifiedSubpaths.js b/lib/helpers/document/cleanModifiedSubpaths.js
index 43c225e4fd2..c12b5e2eea5 100644
--- a/lib/helpers/document/cleanModifiedSubpaths.js
+++ b/lib/helpers/document/cleanModifiedSubpaths.js
@@ -25,11 +25,21 @@ module.exports = function cleanModifiedSubpaths(doc, path, options) {
++deleted;
if (doc.$isSubdocument) {
- const owner = doc.ownerDocument();
- const fullPath = doc.$__fullPath(modifiedPath);
- owner.$__.activePaths.clearPath(fullPath);
+ cleanParent(doc, modifiedPath);
}
}
}
return deleted;
};
+
+function cleanParent(doc, path, seen = new Set()) {
+ if (seen.has(doc)) {
+ throw new Error('Infinite subdocument loop: subdoc with _id ' + doc._id + ' is a parent of itself');
+ }
+ const parent = doc.$parent();
+ const newPath = doc.$__pathRelativeToParent(void 0, false) + '.' + path;
+ parent.$__.activePaths.clearPath(newPath);
+ if (parent.$isSubdocument) {
+ cleanParent(parent, newPath, seen);
+ }
+}
diff --git a/lib/helpers/document/compile.js b/lib/helpers/document/compile.js
index 248211bdb66..51721e7a490 100644
--- a/lib/helpers/document/compile.js
+++ b/lib/helpers/document/compile.js
@@ -1,5 +1,6 @@
'use strict';
+const clone = require('../../helpers/clone');
const documentSchemaSymbol = require('../../helpers/symbols').documentSchemaSymbol;
const internalToObjectOptions = require('../../options').internalToObjectOptions;
const utils = require('../../utils');
@@ -115,7 +116,7 @@ function defineKey({ prop, subprops, prototype, prefix, options }) {
configurable: true,
writable: false,
value: function() {
- return utils.clone(_this.get(path, null, {
+ return clone(_this.get(path, null, {
virtuals: this &&
this.schema &&
this.schema.options &&
diff --git a/lib/helpers/document/getDeepestSubdocumentForPath.js b/lib/helpers/document/getDeepestSubdocumentForPath.js
new file mode 100644
index 00000000000..994bb3088ac
--- /dev/null
+++ b/lib/helpers/document/getDeepestSubdocumentForPath.js
@@ -0,0 +1,38 @@
+'use strict';
+
+/**
+ * Find the deepest subdocument along a given path to ensure setter functions run
+ * with the correct subdocument as `this`. If no subdocuments, returns the top-level
+ * document.
+ *
+ * @param {Document} doc
+ * @param {String[]} parts
+ * @param {Schema} schema
+ * @returns Document
+ */
+
+module.exports = function getDeepestSubdocumentForPath(doc, parts, schema) {
+ let curPath = parts[0];
+ let curSchema = schema;
+ let subdoc = doc;
+ for (let i = 0; i < parts.length - 1; ++i) {
+ const curSchemaType = curSchema.path(curPath);
+ if (curSchemaType && curSchemaType.schema) {
+ let newSubdoc = subdoc.get(curPath);
+ curSchema = curSchemaType.schema;
+ curPath = parts[i + 1];
+ if (Array.isArray(newSubdoc) && !isNaN(curPath)) {
+ newSubdoc = newSubdoc[curPath];
+ curPath = '';
+ }
+ if (newSubdoc == null) {
+ break;
+ }
+ subdoc = newSubdoc;
+ } else {
+ curPath += curPath.length ? '.' + parts[i + 1] : parts[i + 1];
+ }
+ }
+
+ return subdoc;
+};
diff --git a/lib/helpers/document/getEmbeddedDiscriminatorPath.js b/lib/helpers/document/getEmbeddedDiscriminatorPath.js
index ecbedabc3fa..3f3056eb5d9 100644
--- a/lib/helpers/document/getEmbeddedDiscriminatorPath.js
+++ b/lib/helpers/document/getEmbeddedDiscriminatorPath.js
@@ -6,8 +6,9 @@ const getSchemaDiscriminatorByValue = require('../discriminator/getSchemaDiscrim
/**
* Like `schema.path()`, except with a document, because impossible to
* determine path type without knowing the embedded discriminator key.
+ *
* @param {Document} doc
- * @param {String} path
+ * @param {String|String[]} path
* @param {Object} [options]
* @api private
*/
@@ -15,7 +16,9 @@ const getSchemaDiscriminatorByValue = require('../discriminator/getSchemaDiscrim
module.exports = function getEmbeddedDiscriminatorPath(doc, path, options) {
options = options || {};
const typeOnly = options.typeOnly;
- const parts = path.indexOf('.') === -1 ? [path] : path.split('.');
+ const parts = Array.isArray(path) ?
+ path :
+ (path.indexOf('.') === -1 ? [path] : path.split('.'));
let schemaType = null;
let type = 'adhocOrUndefined';
diff --git a/lib/helpers/indexes/getRelatedIndexes.js b/lib/helpers/indexes/getRelatedIndexes.js
index 42d5798c623..a22a5e1202d 100644
--- a/lib/helpers/indexes/getRelatedIndexes.js
+++ b/lib/helpers/indexes/getRelatedIndexes.js
@@ -1,5 +1,7 @@
'use strict';
+const hasDollarKeys = require('../query/hasDollarKeys');
+
function getRelatedSchemaIndexes(model, schemaIndexes) {
return getRelatedIndexes({
baseModelName: model.baseModelName,
@@ -46,7 +48,9 @@ function getRelatedIndexes({
return indexes.filter(index => {
const partialFilterExpression = getPartialFilterExpression(index, indexesType);
- return !partialFilterExpression || !partialFilterExpression[discriminatorKey];
+ return !partialFilterExpression
+ || !partialFilterExpression[discriminatorKey]
+ || (hasDollarKeys(partialFilterExpression[discriminatorKey]) && !('$eq' in partialFilterExpression[discriminatorKey]));
});
}
diff --git a/lib/helpers/indexes/isIndexSpecEqual.js b/lib/helpers/indexes/isIndexSpecEqual.js
new file mode 100644
index 00000000000..defcfb0d10d
--- /dev/null
+++ b/lib/helpers/indexes/isIndexSpecEqual.js
@@ -0,0 +1,32 @@
+'use strict';
+
+/**
+ * Compares two index specifications to determine if they are equal.
+ *
+ * #### Example:
+ * isIndexSpecEqual({ a: 1, b: 1 }, { a: 1, b: 1 }); // true
+ * isIndexSpecEqual({ a: 1, b: 1 }, { b: 1, a: 1 }); // false
+ * isIndexSpecEqual({ a: 1, b: -1 }, { a: 1, b: 1 }); // false
+ *
+ * @param {Object} spec1 The first index specification to compare.
+ * @param {Object} spec2 The second index specification to compare.
+ * @returns {Boolean} Returns true if the index specifications are equal, otherwise returns false.
+ */
+
+module.exports = function isIndexSpecEqual(spec1, spec2) {
+ const spec1Keys = Object.keys(spec1);
+ const spec2Keys = Object.keys(spec2);
+
+ if (spec1Keys.length !== spec2Keys.length) {
+ return false;
+ }
+
+ for (let i = 0; i < spec1Keys.length; i++) {
+ const key = spec1Keys[i];
+ if (key !== spec2Keys[i] || spec1[key] !== spec2[key]) {
+ return false;
+ }
+ }
+
+ return true;
+};
diff --git a/lib/helpers/indexes/isTimeseriesIndex.js b/lib/helpers/indexes/isTimeseriesIndex.js
new file mode 100644
index 00000000000..0a4512b91ed
--- /dev/null
+++ b/lib/helpers/indexes/isTimeseriesIndex.js
@@ -0,0 +1,16 @@
+'use strict';
+
+/**
+ * Returns `true` if the given index matches the schema's `timestamps` options
+ */
+
+module.exports = function isTimeseriesIndex(dbIndex, schemaOptions) {
+ if (schemaOptions.timeseries == null) {
+ return false;
+ }
+ const { timeField, metaField } = schemaOptions.timeseries;
+ if (typeof timeField !== 'string' || typeof metaField !== 'string') {
+ return false;
+ }
+ return Object.keys(dbIndex.key).length === 2 && dbIndex.key[timeField] === 1 && dbIndex.key[metaField] === 1;
+};
diff --git a/lib/helpers/isBsonType.js b/lib/helpers/isBsonType.js
index f75fd40169d..ab6ceba58e6 100644
--- a/lib/helpers/isBsonType.js
+++ b/lib/helpers/isBsonType.js
@@ -7,8 +7,7 @@
function isBsonType(obj, typename) {
return (
- typeof obj === 'object' &&
- obj !== null &&
+ obj != null &&
obj._bsontype === typename
);
}
diff --git a/lib/helpers/isPOJO.js b/lib/helpers/isPOJO.js
new file mode 100644
index 00000000000..2f1a00f0a6e
--- /dev/null
+++ b/lib/helpers/isPOJO.js
@@ -0,0 +1,12 @@
+'use strict';
+
+module.exports = function isPOJO(arg) {
+ if (arg == null || typeof arg !== 'object') {
+ return false;
+ }
+ const proto = Object.getPrototypeOf(arg);
+ // Prototype may be null if you used `Object.create(null)`
+ // Checking `proto`'s constructor is safe because `getPrototypeOf()`
+ // explicitly crosses the boundary from object data to object metadata
+ return !proto || proto.constructor.name === 'Object';
+};
diff --git a/lib/helpers/minimize.js b/lib/helpers/minimize.js
new file mode 100644
index 00000000000..902309f7bb8
--- /dev/null
+++ b/lib/helpers/minimize.js
@@ -0,0 +1,41 @@
+'use strict';
+
+const { isPOJO } = require('../utils');
+
+module.exports = minimize;
+
+/**
+ * Minimizes an object, removing undefined values and empty objects
+ *
+ * @param {Object} object to minimize
+ * @return {Object|undefined}
+ * @api private
+ */
+
+function minimize(obj) {
+ const keys = Object.keys(obj);
+ let i = keys.length;
+ let hasKeys;
+ let key;
+ let val;
+
+ while (i--) {
+ key = keys[i];
+ val = obj[key];
+
+ if (isPOJO(val)) {
+ obj[key] = minimize(val);
+ }
+
+ if (undefined === obj[key]) {
+ delete obj[key];
+ continue;
+ }
+
+ hasKeys = true;
+ }
+
+ return hasKeys
+ ? obj
+ : undefined;
+}
diff --git a/lib/helpers/model/applyHooks.js b/lib/helpers/model/applyHooks.js
index 7ed7895d4b3..998da62f42a 100644
--- a/lib/helpers/model/applyHooks.js
+++ b/lib/helpers/model/applyHooks.js
@@ -64,7 +64,7 @@ function applyHooks(model, schema, options) {
continue;
}
- applyHooks(childModel, type.schema, options);
+ applyHooks(childModel, type.schema, { ...options, isChildSchema: true });
if (childModel.discriminators != null) {
const keys = Object.keys(childModel.discriminators);
for (const key of keys) {
@@ -104,7 +104,8 @@ function applyHooks(model, schema, options) {
objToDecorate.$__originalValidate = objToDecorate.$__originalValidate || objToDecorate.$__validate;
- for (const method of ['save', 'validate', 'remove', 'deleteOne']) {
+ const internalMethodsToWrap = options && options.isChildSchema ? ['save', 'validate', 'deleteOne'] : ['save', 'validate'];
+ for (const method of internalMethodsToWrap) {
const toWrap = method === 'validate' ? '$__originalValidate' : `$__${method}`;
const wrapped = middleware.
createWrapper(method, objToDecorate[toWrap], null, kareemOptions);
diff --git a/lib/helpers/model/applyStaticHooks.js b/lib/helpers/model/applyStaticHooks.js
index 934f9452ade..40116462f26 100644
--- a/lib/helpers/model/applyStaticHooks.js
+++ b/lib/helpers/model/applyStaticHooks.js
@@ -1,7 +1,16 @@
'use strict';
-const middlewareFunctions = require('../query/applyQueryMiddleware').middlewareFunctions;
const promiseOrCallback = require('../promiseOrCallback');
+const { queryMiddlewareFunctions, aggregateMiddlewareFunctions, modelMiddlewareFunctions, documentMiddlewareFunctions } = require('../../constants');
+
+const middlewareFunctions = Array.from(
+ new Set([
+ ...queryMiddlewareFunctions,
+ ...aggregateMiddlewareFunctions,
+ ...modelMiddlewareFunctions,
+ ...documentMiddlewareFunctions
+ ])
+);
module.exports = function applyStaticHooks(model, hooks, statics) {
const kareemOptions = {
@@ -9,8 +18,11 @@ module.exports = function applyStaticHooks(model, hooks, statics) {
numCallbackParams: 1
};
+ model.$__insertMany = hooks.createWrapper('insertMany',
+ model.$__insertMany, model, kareemOptions);
+
hooks = hooks.filter(hook => {
- // If the custom static overwrites an existing query middleware, don't apply
+ // If the custom static overwrites an existing middleware, don't apply
// middleware to it by default. This avoids a potential backwards breaking
// change with plugins like `mongoose-delete` that use statics to overwrite
// built-in Mongoose functions.
@@ -20,9 +32,6 @@ module.exports = function applyStaticHooks(model, hooks, statics) {
return hook.model !== false;
});
- model.$__insertMany = hooks.createWrapper('insertMany',
- model.$__insertMany, model, kareemOptions);
-
for (const key of Object.keys(statics)) {
if (hooks.hasHooks(key)) {
const original = model[key];
diff --git a/lib/helpers/model/castBulkWrite.js b/lib/helpers/model/castBulkWrite.js
index b58f166d0bb..fc053000db3 100644
--- a/lib/helpers/model/castBulkWrite.js
+++ b/lib/helpers/model/castBulkWrite.js
@@ -1,10 +1,14 @@
'use strict';
+const MongooseError = require('../../error/mongooseError');
const getDiscriminatorByValue = require('../../helpers/discriminator/getDiscriminatorByValue');
const applyTimestampsToChildren = require('../update/applyTimestampsToChildren');
const applyTimestampsToUpdate = require('../update/applyTimestampsToUpdate');
const cast = require('../../cast');
const castUpdate = require('../query/castUpdate');
+const clone = require('../clone');
+const decorateUpdateWithVersionKey = require('../update/decorateUpdateWithVersionKey');
+const { inspect } = require('util');
const setDefaultsOnInsert = require('../setDefaultsOnInsert');
/**
@@ -20,203 +24,255 @@ module.exports = function castBulkWrite(originalModel, op, options) {
const now = originalModel.base.now();
if (op['insertOne']) {
- return (callback) => {
- const model = decideModelByObject(originalModel, op['insertOne']['document']);
-
- const doc = new model(op['insertOne']['document']);
- if (model.schema.options.timestamps && options.timestamps !== false) {
- doc.initializeTimestamps();
- }
- if (options.session != null) {
- doc.$session(options.session);
- }
- op['insertOne']['document'] = doc;
-
- if (options.skipValidation || op['insertOne'].skipValidation) {
- callback(null);
- return;
- }
-
- op['insertOne']['document'].$validate({ __noPromise: true }, function(error) {
- if (error) {
- return callback(error, null);
- }
- callback(null);
- });
- };
+ return callback => module.exports.castInsertOne(originalModel, op['insertOne'], options).then(() => callback(null), err => callback(err));
} else if (op['updateOne']) {
return (callback) => {
try {
- if (!op['updateOne']['filter']) {
- throw new Error('Must provide a filter object.');
- }
- if (!op['updateOne']['update']) {
- throw new Error('Must provide an update object.');
- }
-
- const model = decideModelByObject(originalModel, op['updateOne']['filter']);
- const schema = model.schema;
- const strict = options.strict != null ? options.strict : model.schema.options.strict;
-
- _addDiscriminatorToObject(schema, op['updateOne']['filter']);
-
- if (model.schema.$timestamps != null && op['updateOne'].timestamps !== false) {
- const createdAt = model.schema.$timestamps.createdAt;
- const updatedAt = model.schema.$timestamps.updatedAt;
- applyTimestampsToUpdate(now, createdAt, updatedAt, op['updateOne']['update'], {});
- }
-
- applyTimestampsToChildren(now, op['updateOne']['update'], model.schema);
-
- if (op['updateOne'].setDefaultsOnInsert !== false) {
- setDefaultsOnInsert(op['updateOne']['filter'], model.schema, op['updateOne']['update'], {
- setDefaultsOnInsert: true,
- upsert: op['updateOne'].upsert
- });
- }
-
- op['updateOne']['filter'] = cast(model.schema, op['updateOne']['filter'], {
- strict: strict,
- upsert: op['updateOne'].upsert
- });
-
- op['updateOne']['update'] = castUpdate(model.schema, op['updateOne']['update'], {
- strict: strict,
- overwrite: false,
- upsert: op['updateOne'].upsert
- }, model, op['updateOne']['filter']);
- } catch (error) {
- return callback(error, null);
+ module.exports.castUpdateOne(originalModel, op['updateOne'], options, now);
+ callback(null);
+ } catch (err) {
+ callback(err);
}
-
- callback(null);
};
} else if (op['updateMany']) {
return (callback) => {
try {
- if (!op['updateMany']['filter']) {
- throw new Error('Must provide a filter object.');
- }
- if (!op['updateMany']['update']) {
- throw new Error('Must provide an update object.');
- }
-
- const model = decideModelByObject(originalModel, op['updateMany']['filter']);
- const schema = model.schema;
- const strict = options.strict != null ? options.strict : model.schema.options.strict;
-
- if (op['updateMany'].setDefaultsOnInsert !== false) {
- setDefaultsOnInsert(op['updateMany']['filter'], model.schema, op['updateMany']['update'], {
- setDefaultsOnInsert: true,
- upsert: op['updateMany'].upsert
- });
- }
-
- if (model.schema.$timestamps != null && op['updateMany'].timestamps !== false) {
- const createdAt = model.schema.$timestamps.createdAt;
- const updatedAt = model.schema.$timestamps.updatedAt;
- applyTimestampsToUpdate(now, createdAt, updatedAt, op['updateMany']['update'], {});
- }
-
- applyTimestampsToChildren(now, op['updateMany']['update'], model.schema);
-
- _addDiscriminatorToObject(schema, op['updateMany']['filter']);
-
- op['updateMany']['filter'] = cast(model.schema, op['updateMany']['filter'], {
- strict: strict,
- upsert: op['updateMany'].upsert
- });
-
- op['updateMany']['update'] = castUpdate(model.schema, op['updateMany']['update'], {
- strict: strict,
- overwrite: false,
- upsert: op['updateMany'].upsert
- }, model, op['updateMany']['filter']);
- } catch (error) {
- return callback(error, null);
+ module.exports.castUpdateMany(originalModel, op['updateMany'], options, now);
+ callback(null);
+ } catch (err) {
+ callback(err);
}
-
- callback(null);
};
} else if (op['replaceOne']) {
return (callback) => {
- const model = decideModelByObject(originalModel, op['replaceOne']['filter']);
- const schema = model.schema;
- const strict = options.strict != null ? options.strict : model.schema.options.strict;
-
- _addDiscriminatorToObject(schema, op['replaceOne']['filter']);
- try {
- op['replaceOne']['filter'] = cast(model.schema, op['replaceOne']['filter'], {
- strict: strict,
- upsert: op['replaceOne'].upsert
- });
- } catch (error) {
- return callback(error, null);
- }
-
- // set `skipId`, otherwise we get "_id field cannot be changed"
- const doc = new model(op['replaceOne']['replacement'], strict, true);
- if (model.schema.options.timestamps) {
- doc.initializeTimestamps();
- }
- if (options.session != null) {
- doc.$session(options.session);
- }
- op['replaceOne']['replacement'] = doc;
-
- if (options.skipValidation || op['replaceOne'].skipValidation) {
- op['replaceOne']['replacement'] = op['replaceOne']['replacement'].toBSON();
- callback(null);
- return;
- }
-
- op['replaceOne']['replacement'].$validate({ __noPromise: true }, function(error) {
- if (error) {
- return callback(error, null);
- }
- op['replaceOne']['replacement'] = op['replaceOne']['replacement'].toBSON();
- callback(null);
- });
+ module.exports.castReplaceOne(originalModel, op['replaceOne'], options).then(() => callback(null), err => callback(err));
};
} else if (op['deleteOne']) {
return (callback) => {
- const model = decideModelByObject(originalModel, op['deleteOne']['filter']);
- const schema = model.schema;
-
- _addDiscriminatorToObject(schema, op['deleteOne']['filter']);
-
try {
- op['deleteOne']['filter'] = cast(model.schema,
- op['deleteOne']['filter']);
- } catch (error) {
- return callback(error, null);
+ module.exports.castDeleteOne(originalModel, op['deleteOne']);
+ callback(null);
+ } catch (err) {
+ callback(err);
}
-
- callback(null);
};
} else if (op['deleteMany']) {
return (callback) => {
- const model = decideModelByObject(originalModel, op['deleteMany']['filter']);
- const schema = model.schema;
-
- _addDiscriminatorToObject(schema, op['deleteMany']['filter']);
-
try {
- op['deleteMany']['filter'] = cast(model.schema,
- op['deleteMany']['filter']);
- } catch (error) {
- return callback(error, null);
+ module.exports.castDeleteMany(originalModel, op['deleteMany']);
+ callback(null);
+ } catch (err) {
+ callback(err);
}
-
- callback(null);
};
} else {
return (callback) => {
- callback(new Error('Invalid op passed to `bulkWrite()`'), null);
+ const error = new MongooseError(`Invalid op passed to \`bulkWrite()\`: ${inspect(op)}`);
+ callback(error, null);
};
}
};
+module.exports.castInsertOne = async function castInsertOne(originalModel, insertOne, options) {
+ const model = decideModelByObject(originalModel, insertOne['document']);
+
+ const doc = new model(insertOne['document']);
+ if (model.schema.options.timestamps && getTimestampsOpt(insertOne, options)) {
+ doc.initializeTimestamps();
+ }
+ if (options.session != null) {
+ doc.$session(options.session);
+ }
+ const versionKey = model?.schema?.options?.versionKey;
+ if (versionKey && doc[versionKey] == null) {
+ doc[versionKey] = 0;
+ }
+ insertOne['document'] = doc;
+
+ if (options.skipValidation || insertOne.skipValidation) {
+ return insertOne;
+ }
+
+ await insertOne['document'].$validate();
+ return insertOne;
+};
+
+module.exports.castUpdateOne = function castUpdateOne(originalModel, updateOne, options, now) {
+ if (!updateOne['filter']) {
+ throw new Error('Must provide a filter object.');
+ }
+ if (!updateOne['update']) {
+ throw new Error('Must provide an update object.');
+ }
+
+ const model = decideModelByObject(originalModel, updateOne['filter']);
+ const schema = model.schema;
+ const strict = options.strict != null ? options.strict : model.schema.options.strict;
+
+ const update = clone(updateOne['update']);
+
+ _addDiscriminatorToObject(schema, updateOne['filter']);
+
+ const doInitTimestamps = getTimestampsOpt(updateOne, options);
+
+ if (model.schema.$timestamps != null && doInitTimestamps) {
+ const createdAt = model.schema.$timestamps.createdAt;
+ const updatedAt = model.schema.$timestamps.updatedAt;
+ applyTimestampsToUpdate(now, createdAt, updatedAt, update, {});
+ }
+
+ if (doInitTimestamps) {
+ applyTimestampsToChildren(now, update, model.schema);
+ }
+
+ const globalSetDefaultsOnInsert = originalModel.base.options.setDefaultsOnInsert;
+ const shouldSetDefaultsOnInsert = updateOne.setDefaultsOnInsert == null ?
+ globalSetDefaultsOnInsert :
+ updateOne.setDefaultsOnInsert;
+ if (shouldSetDefaultsOnInsert !== false) {
+ setDefaultsOnInsert(updateOne['filter'], model.schema, update, {
+ setDefaultsOnInsert: true,
+ upsert: updateOne.upsert
+ });
+ }
+
+ decorateUpdateWithVersionKey(
+ update,
+ updateOne,
+ model.schema.options.versionKey
+ );
+
+ updateOne['filter'] = cast(model.schema, updateOne['filter'], {
+ strict: strict,
+ upsert: updateOne.upsert
+ });
+ updateOne['update'] = castUpdate(model.schema, update, {
+ strict: strict,
+ upsert: updateOne.upsert,
+ arrayFilters: updateOne.arrayFilters,
+ overwriteDiscriminatorKey: updateOne.overwriteDiscriminatorKey
+ }, model, updateOne['filter']);
+
+ return updateOne;
+};
+
+module.exports.castUpdateMany = function castUpdateMany(originalModel, updateMany, options, now) {
+ if (!updateMany['filter']) {
+ throw new Error('Must provide a filter object.');
+ }
+ if (!updateMany['update']) {
+ throw new Error('Must provide an update object.');
+ }
+
+ const model = decideModelByObject(originalModel, updateMany['filter']);
+ const schema = model.schema;
+ const strict = options.strict != null ? options.strict : model.schema.options.strict;
+
+ const globalSetDefaultsOnInsert = originalModel.base.options.setDefaultsOnInsert;
+ const shouldSetDefaultsOnInsert = updateMany.setDefaultsOnInsert == null ?
+ globalSetDefaultsOnInsert :
+ updateMany.setDefaultsOnInsert;
+
+ if (shouldSetDefaultsOnInsert !== false) {
+ setDefaultsOnInsert(updateMany['filter'], model.schema, updateMany['update'], {
+ setDefaultsOnInsert: true,
+ upsert: updateMany.upsert
+ });
+ }
+
+ const doInitTimestamps = getTimestampsOpt(updateMany, options);
+
+ if (model.schema.$timestamps != null && doInitTimestamps) {
+ const createdAt = model.schema.$timestamps.createdAt;
+ const updatedAt = model.schema.$timestamps.updatedAt;
+ applyTimestampsToUpdate(now, createdAt, updatedAt, updateMany['update'], {});
+ }
+ if (doInitTimestamps) {
+ applyTimestampsToChildren(now, updateMany['update'], model.schema);
+ }
+
+ _addDiscriminatorToObject(schema, updateMany['filter']);
+
+ decorateUpdateWithVersionKey(
+ updateMany['update'],
+ updateMany,
+ model.schema.options.versionKey
+ );
+
+ updateMany['filter'] = cast(model.schema, updateMany['filter'], {
+ strict: strict,
+ upsert: updateMany.upsert
+ });
+
+ updateMany['update'] = castUpdate(model.schema, updateMany['update'], {
+ strict: strict,
+ upsert: updateMany.upsert,
+ arrayFilters: updateMany.arrayFilters,
+ overwriteDiscriminatorKey: updateMany.overwriteDiscriminatorKey
+ }, model, updateMany['filter']);
+};
+
+module.exports.castReplaceOne = async function castReplaceOne(originalModel, replaceOne, options) {
+ const model = decideModelByObject(originalModel, replaceOne['filter']);
+ const schema = model.schema;
+ const strict = options.strict != null ? options.strict : model.schema.options.strict;
+
+ _addDiscriminatorToObject(schema, replaceOne['filter']);
+ replaceOne['filter'] = cast(model.schema, replaceOne['filter'], {
+ strict: strict,
+ upsert: replaceOne.upsert
+ });
+
+ // set `skipId`, otherwise we get "_id field cannot be changed"
+ const doc = new model(replaceOne['replacement'], strict, true);
+ if (model.schema.options.timestamps && getTimestampsOpt(replaceOne, options)) {
+ doc.initializeTimestamps();
+ }
+ if (options.session != null) {
+ doc.$session(options.session);
+ }
+ const versionKey = model?.schema?.options?.versionKey;
+ if (versionKey && doc[versionKey] == null) {
+ doc[versionKey] = 0;
+ }
+ replaceOne['replacement'] = doc;
+
+ if (options.skipValidation || replaceOne.skipValidation) {
+ replaceOne['replacement'] = replaceOne['replacement'].toBSON();
+ return;
+ }
+
+ await replaceOne['replacement'].$validate();
+ replaceOne['replacement'] = replaceOne['replacement'].toBSON();
+};
+
+module.exports.castDeleteOne = function castDeleteOne(originalModel, deleteOne) {
+ const model = decideModelByObject(originalModel, deleteOne['filter']);
+ const schema = model.schema;
+
+ _addDiscriminatorToObject(schema, deleteOne['filter']);
+
+ deleteOne['filter'] = cast(model.schema, deleteOne['filter']);
+};
+
+module.exports.castDeleteMany = function castDeleteMany(originalModel, deleteMany) {
+ const model = decideModelByObject(originalModel, deleteMany['filter']);
+ const schema = model.schema;
+
+ _addDiscriminatorToObject(schema, deleteMany['filter']);
+
+ deleteMany['filter'] = cast(model.schema, deleteMany['filter']);
+};
+
+module.exports.cast = {
+ insertOne: module.exports.castInsertOne,
+ updateOne: module.exports.castUpdateOne,
+ updateMany: module.exports.castUpdateMany,
+ replaceOne: module.exports.castReplaceOne,
+ deleteOne: module.exports.castDeleteOne,
+ deleteMany: module.exports.castDeleteMany
+};
+
function _addDiscriminatorToObject(schema, obj) {
if (schema == null) {
return;
@@ -238,3 +294,20 @@ function decideModelByObject(model, object) {
}
return model;
}
+
+
+/**
+ * gets timestamps option for a given operation. If the option is set within an individual operation, use it. Otherwise, use the global timestamps option configured in the `bulkWrite` options. Overall default is `true`.
+ * @api private
+ */
+
+function getTimestampsOpt(opCommand, options) {
+ const opLevelOpt = opCommand.timestamps;
+ const bulkLevelOpt = options.timestamps;
+ if (opLevelOpt != null) {
+ return opLevelOpt;
+ } else if (bulkLevelOpt != null) {
+ return bulkLevelOpt;
+ }
+ return true;
+}
diff --git a/lib/helpers/model/discriminator.js b/lib/helpers/model/discriminator.js
index 0c843df10da..f5c421656da 100644
--- a/lib/helpers/model/discriminator.js
+++ b/lib/helpers/model/discriminator.js
@@ -2,6 +2,7 @@
const Mixed = require('../../schema/mixed');
const applyBuiltinPlugins = require('../schema/applyBuiltinPlugins');
+const clone = require('../clone');
const defineKey = require('../document/compile').defineKey;
const get = require('../get');
const utils = require('../../utils');
@@ -20,7 +21,7 @@ const CUSTOMIZABLE_DISCRIMINATOR_OPTIONS = {
* ignore
*/
-module.exports = function discriminator(model, name, schema, tiedValue, applyPlugins, mergeHooks) {
+module.exports = function discriminator(model, name, schema, tiedValue, applyPlugins, mergeHooks, overwriteExisting) {
if (!(schema && schema.instanceOfSchema)) {
throw new Error('You must pass a valid discriminator Schema');
}
@@ -114,13 +115,11 @@ module.exports = function discriminator(model, name, schema, tiedValue, applyPlu
}
}
- mergeDiscriminatorSchema(schema, baseSchema, {
- omit: { discriminators: true, base: true, _applyDiscriminators: true },
- omitNested: conflictingPaths.reduce((cur, path) => {
- cur['tree.' + path] = true;
- return cur;
- }, {})
- });
+ // Shallow clone `obj` so we can add additional properties without modifying original
+ // schema. `Schema.prototype.clone()` copies `obj` by reference, no cloning.
+ schema.obj = { ...schema.obj };
+ mergeDiscriminatorSchema(schema, baseSchema);
+ schema._gatherChildSchemas();
// Clean up conflicting paths _after_ merging re: gh-6076
for (const conflictingPath of conflictingPaths) {
@@ -152,7 +151,6 @@ module.exports = function discriminator(model, name, schema, tiedValue, applyPlu
if (baseSchema.options.collection) {
schema.options.collection = baseSchema.options.collection;
}
-
const toJSON = schema.options.toJSON;
const toObject = schema.options.toObject;
const _id = schema.options._id;
@@ -160,16 +158,13 @@ module.exports = function discriminator(model, name, schema, tiedValue, applyPlu
const keys = Object.keys(schema.options);
schema.options.discriminatorKey = baseSchema.options.discriminatorKey;
-
+ const userProvidedOptions = schema._userProvidedOptions;
for (const _key of keys) {
if (!CUSTOMIZABLE_DISCRIMINATOR_OPTIONS[_key]) {
- // Special case: compiling a model sets `pluralization = true` by default. Avoid throwing an error
- // for that case. See gh-9238
- if (_key === 'pluralization' && schema.options[_key] == true && baseSchema.options[_key] == null) {
- continue;
- }
-
- if (!utils.deepEqual(schema.options[_key], baseSchema.options[_key])) {
+ // Use `schema.options` in `deepEqual()` because of `discriminatorKey`
+ // set above. We don't allow customizing discriminator key, always
+ // overwrite. See gh-9238
+ if (_key in userProvidedOptions && !utils.deepEqual(schema.options[_key], baseSchema.options[_key])) {
throw new Error('Can\'t customize discriminator option ' + _key +
' (can only modify ' +
Object.keys(CUSTOMIZABLE_DISCRIMINATOR_OPTIONS).join(', ') +
@@ -177,7 +172,11 @@ module.exports = function discriminator(model, name, schema, tiedValue, applyPlu
}
}
}
- schema.options = utils.clone(baseSchema.options);
+ schema.options = clone(baseSchema.options);
+
+ for (const _key of Object.keys(userProvidedOptions)) {
+ schema.options[_key] = userProvidedOptions[_key];
+ }
if (toJSON) schema.options.toJSON = toJSON;
if (toObject) schema.options.toObject = toObject;
if (typeof _id !== 'undefined') {
@@ -210,7 +209,7 @@ module.exports = function discriminator(model, name, schema, tiedValue, applyPlu
model.schema.discriminators[name] = schema;
- if (model.discriminators[name] && !schema.options.overwriteModels) {
+ if (model.discriminators[name] && !schema.options.overwriteModels && !overwriteExisting) {
throw new Error('Discriminator with name "' + name + '" already exists');
}
diff --git a/lib/helpers/omitUndefined.js b/lib/helpers/omitUndefined.js
new file mode 100644
index 00000000000..5c9eb88564a
--- /dev/null
+++ b/lib/helpers/omitUndefined.js
@@ -0,0 +1,20 @@
+'use strict';
+
+module.exports = function omitUndefined(val) {
+ if (val == null || typeof val !== 'object') {
+ return val;
+ }
+ if (Array.isArray(val)) {
+ for (let i = val.length - 1; i >= 0; --i) {
+ if (val[i] === undefined) {
+ val.splice(i, 1);
+ }
+ }
+ }
+ for (const key of Object.keys(val)) {
+ if (val[key] === void 0) {
+ delete val[key];
+ }
+ }
+ return val;
+};
diff --git a/lib/helpers/path/flattenObjectWithDottedPaths.js b/lib/helpers/path/flattenObjectWithDottedPaths.js
deleted file mode 100644
index 2771796d082..00000000000
--- a/lib/helpers/path/flattenObjectWithDottedPaths.js
+++ /dev/null
@@ -1,39 +0,0 @@
-'use strict';
-
-const MongooseError = require('../../error/mongooseError');
-const isMongooseObject = require('../isMongooseObject');
-const setDottedPath = require('../path/setDottedPath');
-const util = require('util');
-
-/**
- * Given an object that may contain dotted paths, flatten the paths out.
- * For example: `flattenObjectWithDottedPaths({ a: { 'b.c': 42 } })` => `{ a: { b: { c: 42 } } }`
- */
-
-module.exports = function flattenObjectWithDottedPaths(obj) {
- if (obj == null || typeof obj !== 'object' || Array.isArray(obj)) {
- return;
- }
- // Avoid Mongoose docs, like docs and maps, because these may cause infinite recursion
- if (isMongooseObject(obj)) {
- return;
- }
- const keys = Object.keys(obj);
- for (const key of keys) {
- const val = obj[key];
- if (key.indexOf('.') !== -1) {
- try {
- delete obj[key];
- setDottedPath(obj, key, val);
- } catch (err) {
- if (!(err instanceof TypeError)) {
- throw err;
- }
- throw new MongooseError(`Conflicting dotted paths when setting document path, key: "${key}", value: ${util.inspect(val)}`);
- }
- continue;
- }
-
- flattenObjectWithDottedPaths(obj[key]);
- }
-};
diff --git a/lib/helpers/pluralize.js b/lib/helpers/pluralize.js
index 657c87f03a8..2f9cbf8a2e0 100644
--- a/lib/helpers/pluralize.js
+++ b/lib/helpers/pluralize.js
@@ -7,6 +7,7 @@ module.exports = pluralize;
*/
exports.pluralization = [
+ [/human$/gi, 'humans'],
[/(m)an$/gi, '$1en'],
[/(pe)rson$/gi, '$1ople'],
[/(child)$/gi, '$1ren'],
diff --git a/lib/helpers/populate/assignRawDocsToIdStructure.js b/lib/helpers/populate/assignRawDocsToIdStructure.js
index e04b601ba97..765d69f06af 100644
--- a/lib/helpers/populate/assignRawDocsToIdStructure.js
+++ b/lib/helpers/populate/assignRawDocsToIdStructure.js
@@ -1,12 +1,13 @@
'use strict';
+const clone = require('../../helpers/clone');
const leanPopulateMap = require('./leanPopulateMap');
const modelSymbol = require('../symbols').modelSymbol;
const utils = require('../../utils');
module.exports = assignRawDocsToIdStructure;
-const kHasArray = Symbol('assignRawDocsToIdStructure.hasArray');
+const kHasArray = Symbol('mongoose#assignRawDocsToIdStructure#hasArray');
/**
* Assign `vals` returned by mongo query to the `rawIds`
@@ -31,9 +32,13 @@ const kHasArray = Symbol('assignRawDocsToIdStructure.hasArray');
*/
function assignRawDocsToIdStructure(rawIds, resultDocs, resultOrder, options, recursed) {
- // honor user specified sort order
+ // honor user specified sort order, unless we're populating a single
+ // virtual underneath an array (e.g. populating `employees.mostRecentShift` where
+ // `mostRecentShift` is a virtual with `justOne`)
const newOrder = [];
- const sorting = options.sort && rawIds.length > 1;
+ const sorting = options.isVirtual && options.justOne && rawIds.length > 1
+ ? false :
+ options.sort && rawIds.length > 1;
const nullIfNotFound = options.$nullIfNotFound;
let doc;
let sid;
@@ -79,7 +84,7 @@ function assignRawDocsToIdStructure(rawIds, resultDocs, resultOrder, options, re
if (options.clone && doc != null) {
if (options.lean) {
const _model = leanPopulateMap.get(doc);
- doc = utils.clone(doc);
+ doc = clone(doc);
leanPopulateMap.set(doc, _model);
} else {
doc = doc.constructor.hydrate(doc._doc);
diff --git a/lib/helpers/populate/assignVals.js b/lib/helpers/populate/assignVals.js
index 92f0ebecd05..62b3863b583 100644
--- a/lib/helpers/populate/assignVals.js
+++ b/lib/helpers/populate/assignVals.js
@@ -1,7 +1,7 @@
'use strict';
const MongooseMap = require('../../types/map');
-const SkipPopulateValue = require('./SkipPopulateValue');
+const SkipPopulateValue = require('./skipPopulateValue');
const assignRawDocsToIdStructure = require('./assignRawDocsToIdStructure');
const get = require('../get');
const getVirtual = require('./getVirtual');
@@ -19,7 +19,8 @@ module.exports = function assignVals(o) {
// `o.options` contains options explicitly listed in `populateOptions`, like
// `match` and `limit`.
const populateOptions = Object.assign({}, o.options, userOptions, {
- justOne: o.justOne
+ justOne: o.justOne,
+ isVirtual: o.isVirtual
});
populateOptions.$nullIfNotFound = o.isVirtual;
const populatedModel = o.populatedModel;
@@ -38,8 +39,10 @@ module.exports = function assignVals(o) {
const options = o.options;
const count = o.count && o.isVirtual;
let i;
+ let setValueIndex = 0;
function setValue(val) {
+ ++setValueIndex;
if (count) {
return val;
}
@@ -79,11 +82,14 @@ module.exports = function assignVals(o) {
return valueFilter(val[0], options, populateOptions, _allIds);
} else if (o.justOne === false && !Array.isArray(val)) {
return valueFilter([val], options, populateOptions, _allIds);
+ } else if (o.justOne === true && !Array.isArray(val) && Array.isArray(_allIds)) {
+ return valueFilter(val, options, populateOptions, val == null ? val : _allIds[setValueIndex - 1]);
}
return valueFilter(val, options, populateOptions, _allIds);
}
for (i = 0; i < docs.length; ++i) {
+ setValueIndex = 0;
const _path = o.path.endsWith('.$*') ? o.path.slice(0, -3) : o.path;
const existingVal = mpath.get(_path, docs[i], lookupLocalFields);
if (existingVal == null && !getVirtual(o.originalModel.schema, _path)) {
@@ -95,8 +101,8 @@ module.exports = function assignVals(o) {
valueToSet = numDocs(rawIds[i]);
} else if (Array.isArray(o.match)) {
valueToSet = Array.isArray(rawIds[i]) ?
- rawIds[i].filter(sift(o.match[i])) :
- [rawIds[i]].filter(sift(o.match[i]))[0];
+ rawIds[i].filter(v => v == null || sift(o.match[i])(v)) :
+ [rawIds[i]].filter(v => v == null || sift(o.match[i])(v))[0];
} else {
valueToSet = rawIds[i];
}
@@ -144,7 +150,6 @@ module.exports = function assignVals(o) {
const parts = _path.split('.');
let cur = docs[i];
- const curPath = parts[0];
for (let j = 0; j < parts.length - 1; ++j) {
// If we get to an array with a dotted path, like `arr.foo`, don't set
// `foo` on the array.
@@ -160,6 +165,7 @@ module.exports = function assignVals(o) {
// If nothing to set, avoid creating an unnecessary array. Otherwise
// we'll end up with a single doc in the array with only defaults.
// See gh-8342, gh-8455
+ const curPath = parts.slice(0, j + 1).join('.');
const schematype = originalSchema._getSchema(curPath);
if (valueToSet == null && schematype != null && schematype.$isMongooseArray) {
break;
@@ -243,7 +249,7 @@ function numDocs(v) {
function valueFilter(val, assignmentOpts, populateOptions, allIds) {
const userSpecifiedTransform = typeof populateOptions.transform === 'function';
- const transform = userSpecifiedTransform ? populateOptions.transform : noop;
+ const transform = userSpecifiedTransform ? populateOptions.transform : v => v;
if (Array.isArray(val)) {
// find logic
const ret = [];
@@ -335,7 +341,3 @@ function isPopulatedObject(obj) {
obj.$__ != null ||
leanPopulateMap.has(obj);
}
-
-function noop(v) {
- return v;
-}
diff --git a/lib/helpers/populate/createPopulateQueryFilter.js b/lib/helpers/populate/createPopulateQueryFilter.js
index acfeee62ae0..47509a35658 100644
--- a/lib/helpers/populate/createPopulateQueryFilter.js
+++ b/lib/helpers/populate/createPopulateQueryFilter.js
@@ -1,6 +1,6 @@
'use strict';
-const SkipPopulateValue = require('./SkipPopulateValue');
+const SkipPopulateValue = require('./skipPopulateValue');
const parentPaths = require('../path/parentPaths');
const { trusted } = require('../query/trusted');
const hasDollarKeys = require('../query/hasDollarKeys');
diff --git a/lib/helpers/populate/getModelsMapForPopulate.js b/lib/helpers/populate/getModelsMapForPopulate.js
index 8ae89fcbbd3..f90bd0e8f33 100644
--- a/lib/helpers/populate/getModelsMapForPopulate.js
+++ b/lib/helpers/populate/getModelsMapForPopulate.js
@@ -1,7 +1,8 @@
'use strict';
const MongooseError = require('../../error/index');
-const SkipPopulateValue = require('./SkipPopulateValue');
+const SkipPopulateValue = require('./skipPopulateValue');
+const clone = require('../clone');
const get = require('../get');
const getDiscriminatorByValue = require('../discriminator/getDiscriminatorByValue');
const getConstructorName = require('../getConstructorName');
@@ -44,7 +45,8 @@ module.exports = function getModelsMapForPopulate(model, docs, options) {
let allSchemaTypes = getSchemaTypes(model, modelSchema, null, options.path);
allSchemaTypes = Array.isArray(allSchemaTypes) ? allSchemaTypes : [allSchemaTypes].filter(v => v != null);
- if (allSchemaTypes.length === 0 && options.strictPopulate !== false && options._localModel != null) {
+ const isStrictPopulateDisabled = options.strictPopulate === false || options.options?.strictPopulate === false;
+ if (!isStrictPopulateDisabled && allSchemaTypes.length === 0 && options._localModel != null) {
return new StrictPopulate(options._fullPath || options.path);
}
@@ -52,6 +54,13 @@ module.exports = function getModelsMapForPopulate(model, docs, options) {
doc = docs[i];
let justOne = null;
+ if (doc.$__ != null && doc.populated(options.path)) {
+ const forceRepopulate = options.forceRepopulate != null ? options.forceRepopulate : doc.constructor.base.options.forceRepopulate;
+ if (forceRepopulate === false) {
+ continue;
+ }
+ }
+
const docSchema = doc != null && doc.$__ != null ? doc.$__schema : modelSchema;
schema = getSchemaTypes(model, docSchema, doc, options.path);
@@ -63,7 +72,7 @@ module.exports = function getModelsMapForPopulate(model, docs, options) {
schema.options.refPath == null) {
continue;
}
- const isUnderneathDocArray = schema && schema.$isUnderneathDocArray;
+ const isUnderneathDocArray = schema && schema.$parentSchemaDocArray;
if (isUnderneathDocArray && get(options, 'options.sort') != null) {
return new MongooseError('Cannot populate with `sort` on path ' + options.path +
' because it is a subproperty of a document array');
@@ -182,6 +191,7 @@ module.exports = function getModelsMapForPopulate(model, docs, options) {
if (hasMatchFunction) {
match = match.call(doc, doc);
}
+ throwOn$where(match);
data.match = match;
data.hasMatchFunction = hasMatchFunction;
data.isRefPath = isRefPath;
@@ -388,8 +398,7 @@ function _virtualPopulate(model, docs, options, _virtualRes) {
let foreignField = virtual.options.foreignField;
if (!localField || !foreignField) {
- return new MongooseError('If you are populating a virtual, you must set the ' +
- 'localField and foreignField options');
+ return new MongooseError(`Cannot populate virtual \`${options.path}\` on model \`${model.modelName}\`, because options \`localField\` and / or \`foreignField\` are missing`);
}
if (typeof localField === 'function') {
@@ -409,26 +418,12 @@ function _virtualPopulate(model, docs, options, _virtualRes) {
justOne = options.justOne;
}
+ modelNames = virtual._getModelNamesForPopulate(doc);
if (virtual.options.refPath) {
- modelNames =
- modelNamesFromRefPath(virtual.options.refPath, doc, options.path);
justOne = !!virtual.options.justOne;
data.isRefPath = true;
} else if (virtual.options.ref) {
- let normalizedRef;
- if (typeof virtual.options.ref === 'function' && !virtual.options.ref[modelSymbol]) {
- normalizedRef = virtual.options.ref.call(doc, doc);
- } else {
- normalizedRef = virtual.options.ref;
- }
justOne = !!virtual.options.justOne;
- // When referencing nested arrays, the ref should be an Array
- // of modelNames.
- if (Array.isArray(normalizedRef)) {
- modelNames = normalizedRef;
- } else {
- modelNames = [normalizedRef];
- }
}
data.isVirtual = true;
@@ -436,13 +431,13 @@ function _virtualPopulate(model, docs, options, _virtualRes) {
data.justOne = justOne;
// `match`
- let match = get(options, 'match', null) ||
- get(data, 'virtual.options.match', null) ||
+ const baseMatch = get(data, 'virtual.options.match', null) ||
get(data, 'virtual.options.options.match', null);
+ let match = get(options, 'match', null) || baseMatch;
let hasMatchFunction = typeof match === 'function';
if (hasMatchFunction) {
- match = match.call(doc, doc);
+ match = match.call(doc, doc, data.virtual);
}
if (Array.isArray(localField) && Array.isArray(foreignField) && localField.length === foreignField.length) {
@@ -460,6 +455,8 @@ function _virtualPopulate(model, docs, options, _virtualRes) {
data.match = match;
data.hasMatchFunction = hasMatchFunction;
+ throwOn$where(match);
+
// Get local fields
const ret = _getLocalFieldValues(doc, localField, model, options, virtual);
@@ -491,9 +488,10 @@ function addModelNamesToMap(model, map, available, modelNames, options, data, re
return;
}
- let k = modelNames.length;
+ const flatModelNames = utils.array.flatten(modelNames);
+ let k = flatModelNames.length;
while (k--) {
- const modelName = modelNames[k];
+ let modelName = flatModelNames[k];
if (modelName == null) {
continue;
}
@@ -503,6 +501,7 @@ function addModelNamesToMap(model, map, available, modelNames, options, data, re
Model = options.model;
} else if (modelName[modelSymbol]) {
Model = modelName;
+ modelName = Model.modelName;
} else {
try {
Model = _getModelFromConn(connection, modelName);
@@ -515,11 +514,10 @@ function addModelNamesToMap(model, map, available, modelNames, options, data, re
}
let ids = ret;
- const flat = Array.isArray(ret) ? utils.array.flatten(ret) : [];
const modelNamesForRefPath = data.modelNamesInOrder ? data.modelNamesInOrder : modelNames;
- if (data.isRefPath && Array.isArray(ret) && flat.length === modelNamesForRefPath.length) {
- ids = flat.filter((val, i) => modelNamesForRefPath[i] === modelName);
+ if (data.isRefPath && Array.isArray(ret) && ret.length === modelNamesForRefPath.length) {
+ ids = matchIdsToRefPaths(ret, modelNamesForRefPath, modelName);
}
const perDocumentLimit = options.perDocumentLimit == null ?
@@ -531,7 +529,7 @@ function addModelNamesToMap(model, map, available, modelNames, options, data, re
model: Model
};
if (data.isVirtual && get(data.virtual, 'options.options')) {
- currentOptions.options = utils.clone(data.virtual.options.options);
+ currentOptions.options = clone(data.virtual.options.options);
} else if (schemaOptions != null) {
currentOptions.options = Object.assign({}, schemaOptions);
}
@@ -540,6 +538,7 @@ function addModelNamesToMap(model, map, available, modelNames, options, data, re
// Used internally for checking what model was used to populate this
// path.
options[populateModelSymbol] = Model;
+ currentOptions[populateModelSymbol] = Model;
available[modelName] = {
model: Model,
options: currentOptions,
@@ -580,6 +579,20 @@ function _getModelFromConn(conn, modelName) {
return conn.model(modelName);
}
+function matchIdsToRefPaths(ids, refPaths, refPathToFind) {
+ if (!Array.isArray(refPaths)) {
+ return refPaths === refPathToFind
+ ? Array.isArray(ids)
+ ? utils.array.flatten(ids)
+ : [ids]
+ : [];
+ }
+ if (Array.isArray(ids) && Array.isArray(refPaths)) {
+ return ids.flatMap((id, index) => matchIdsToRefPaths(id, refPaths[index], refPathToFind));
+ }
+ return [];
+}
+
/*!
* ignore
*/
@@ -637,7 +650,7 @@ function _getLocalFieldValues(doc, localField, model, options, virtual, schema)
function convertTo_id(val, schema) {
if (val != null && val.$__ != null) {
- return val._id;
+ return val._doc._id;
}
if (val != null && val._id != null && (schema == null || !schema.$isSchemaMap)) {
return val._id;
@@ -647,7 +660,7 @@ function convertTo_id(val, schema) {
const rawVal = val.__array != null ? val.__array : val;
for (let i = 0; i < rawVal.length; ++i) {
if (rawVal[i] != null && rawVal[i].$__ != null) {
- rawVal[i] = rawVal[i]._id;
+ rawVal[i] = rawVal[i]._doc._id;
}
}
if (utils.isMongooseArray(val) && val.$schema()) {
@@ -730,3 +743,24 @@ function _findRefPathForDiscriminators(doc, modelSchema, data, options, normaliz
return modelNames;
}
+
+/**
+ * Throw an error if there are any $where keys
+ */
+
+function throwOn$where(match) {
+ if (match == null) {
+ return;
+ }
+ if (typeof match !== 'object') {
+ return;
+ }
+ for (const key of Object.keys(match)) {
+ if (key === '$where') {
+ throw new MongooseError('Cannot use $where filter with populate() match');
+ }
+ if (match[key] != null && typeof match[key] === 'object') {
+ throwOn$where(match[key]);
+ }
+ }
+}
diff --git a/lib/helpers/populate/getSchemaTypes.js b/lib/helpers/populate/getSchemaTypes.js
index 0534f015286..8cbbcafa8b1 100644
--- a/lib/helpers/populate/getSchemaTypes.js
+++ b/lib/helpers/populate/getSchemaTypes.js
@@ -101,8 +101,8 @@ module.exports = function getSchemaTypes(model, schema, doc, path) {
nestedPath.concat(parts.slice(0, p))
);
if (ret) {
- ret.$isUnderneathDocArray = ret.$isUnderneathDocArray ||
- !foundschema.schema.$isSingleNested;
+ ret.$parentSchemaDocArray = ret.$parentSchemaDocArray ||
+ (foundschema.schema.$isSingleNested ? null : foundschema);
}
return ret;
}
@@ -117,10 +117,10 @@ module.exports = function getSchemaTypes(model, schema, doc, path) {
nestedPath.concat(parts.slice(0, p))
);
if (_ret != null) {
- _ret.$isUnderneathDocArray = _ret.$isUnderneathDocArray ||
- !foundschema.schema.$isSingleNested;
- if (_ret.$isUnderneathDocArray) {
- ret.$isUnderneathDocArray = true;
+ _ret.$parentSchemaDocArray = _ret.$parentSchemaDocArray ||
+ (foundschema.schema.$isSingleNested ? null : foundschema);
+ if (_ret.$parentSchemaDocArray) {
+ ret.$parentSchemaDocArray = _ret.$parentSchemaDocArray;
}
ret.push(_ret);
}
@@ -135,8 +135,8 @@ module.exports = function getSchemaTypes(model, schema, doc, path) {
);
if (ret) {
- ret.$isUnderneathDocArray = ret.$isUnderneathDocArray ||
- !foundschema.schema.$isSingleNested;
+ ret.$parentSchemaDocArray = ret.$parentSchemaDocArray ||
+ (foundschema.schema.$isSingleNested ? null : foundschema);
}
return ret;
}
@@ -188,10 +188,6 @@ module.exports = function getSchemaTypes(model, schema, doc, path) {
nestedPath.concat(parts.slice(0, p))
);
- if (ret) {
- ret.$isUnderneathDocArray = ret.$isUnderneathDocArray ||
- !model.schema.$isSingleNested;
- }
return ret;
}
}
@@ -212,8 +208,8 @@ module.exports = function getSchemaTypes(model, schema, doc, path) {
);
if (ret != null) {
- ret.$isUnderneathDocArray = ret.$isUnderneathDocArray ||
- !schema.$isSingleNested;
+ ret.$parentSchemaDocArray = ret.$parentSchemaDocArray ||
+ (schema.$isSingleNested ? null : schema);
return ret;
}
}
diff --git a/lib/helpers/populate/markArraySubdocsPopulated.js b/lib/helpers/populate/markArraySubdocsPopulated.js
index 28b19a1989a..c0ea5ef5207 100644
--- a/lib/helpers/populate/markArraySubdocsPopulated.js
+++ b/lib/helpers/populate/markArraySubdocsPopulated.js
@@ -17,11 +17,11 @@ const utils = require('../../utils');
*/
module.exports = function markArraySubdocsPopulated(doc, populated) {
- if (doc._id == null || populated == null || populated.length === 0) {
+ if (doc._doc._id == null || populated == null || populated.length === 0) {
return;
}
- const id = String(doc._id);
+ const id = String(doc._doc._id);
for (const item of populated) {
if (item.isVirtual) {
continue;
@@ -38,7 +38,9 @@ module.exports = function markArraySubdocsPopulated(doc, populated) {
if (utils.isMongooseDocumentArray(val)) {
for (let j = 0; j < val.length; ++j) {
- val[j].populated(rest, item._docs[id] == null ? void 0 : item._docs[id][j], item);
+ if (val[j]) {
+ val[j].populated(rest, item._docs[id] == null ? void 0 : item._docs[id][j], item);
+ }
}
break;
}
diff --git a/lib/helpers/populate/modelNamesFromRefPath.js b/lib/helpers/populate/modelNamesFromRefPath.js
index df643b234ae..a5b02859346 100644
--- a/lib/helpers/populate/modelNamesFromRefPath.js
+++ b/lib/helpers/populate/modelNamesFromRefPath.js
@@ -62,7 +62,5 @@ module.exports = function modelNamesFromRefPath(refPath, doc, populatedPath, mod
modelNames = Array.isArray(refValue) ? refValue : [refValue];
}
- modelNames = utils.array.flatten(modelNames);
-
return modelNames;
};
diff --git a/lib/helpers/populate/setPopulatedVirtualValue.js b/lib/helpers/populate/setPopulatedVirtualValue.js
new file mode 100644
index 00000000000..786e540051a
--- /dev/null
+++ b/lib/helpers/populate/setPopulatedVirtualValue.js
@@ -0,0 +1,33 @@
+'use strict';
+
+/**
+ * Set a populated virtual value on a document's `$$populatedVirtuals` value
+ *
+ * @param {*} populatedVirtuals A document's `$$populatedVirtuals`
+ * @param {*} name The virtual name
+ * @param {*} v The result of the populate query
+ * @param {*} options The populate options. This function handles `justOne` and `count` options.
+ * @returns {Array|Document|Object|Array} the populated virtual value that was set
+ */
+
+module.exports = function setPopulatedVirtualValue(populatedVirtuals, name, v, options) {
+ if (options.justOne || options.count) {
+ populatedVirtuals[name] = Array.isArray(v) ?
+ v[0] :
+ v;
+
+ if (typeof populatedVirtuals[name] !== 'object') {
+ populatedVirtuals[name] = options.count ? v : null;
+ }
+ } else {
+ populatedVirtuals[name] = Array.isArray(v) ?
+ v :
+ v == null ? [] : [v];
+
+ populatedVirtuals[name] = populatedVirtuals[name].filter(function(doc) {
+ return doc && typeof doc === 'object';
+ });
+ }
+
+ return populatedVirtuals[name];
+};
diff --git a/lib/helpers/populate/SkipPopulateValue.js b/lib/helpers/populate/skipPopulateValue.js
similarity index 100%
rename from lib/helpers/populate/SkipPopulateValue.js
rename to lib/helpers/populate/skipPopulateValue.js
diff --git a/lib/helpers/printStrictQueryWarning.js b/lib/helpers/printStrictQueryWarning.js
deleted file mode 100644
index 3cf4fdf4341..00000000000
--- a/lib/helpers/printStrictQueryWarning.js
+++ /dev/null
@@ -1,11 +0,0 @@
-'use strict';
-
-const util = require('util');
-
-module.exports = util.deprecate(
- function() { },
- 'Mongoose: the `strictQuery` option will be switched back to `false` by default ' +
- 'in Mongoose 7. Use `mongoose.set(\'strictQuery\', false);` if you want to prepare ' +
- 'for this change. Or use `mongoose.set(\'strictQuery\', true);` to suppress this warning.',
- 'MONGOOSE'
-);
diff --git a/lib/helpers/processConnectionOptions.js b/lib/helpers/processConnectionOptions.js
index a9d862b1030..1dbb767ebee 100644
--- a/lib/helpers/processConnectionOptions.js
+++ b/lib/helpers/processConnectionOptions.js
@@ -9,11 +9,12 @@ function processConnectionOptions(uri, options) {
? opts.readPreference
: getUriReadPreference(uri);
+ const clonedOpts = clone(opts);
const resolvedOpts = (readPreference && readPreference !== 'primary' && readPreference !== 'primaryPreferred')
- ? resolveOptsConflicts(readPreference, opts)
- : opts;
+ ? resolveOptsConflicts(readPreference, clonedOpts)
+ : clonedOpts;
- return clone(resolvedOpts);
+ return resolvedOpts;
}
function resolveOptsConflicts(pref, opts) {
diff --git a/lib/helpers/projection/applyProjection.js b/lib/helpers/projection/applyProjection.js
index 1552e07e686..7a35b128b24 100644
--- a/lib/helpers/projection/applyProjection.js
+++ b/lib/helpers/projection/applyProjection.js
@@ -35,6 +35,9 @@ function applyExclusiveProjection(doc, projection, hasIncludedChildren, projecti
if (doc == null || typeof doc !== 'object') {
return doc;
}
+ if (Array.isArray(doc)) {
+ return doc.map(el => applyExclusiveProjection(el, projection, hasIncludedChildren, projectionLimb, prefix));
+ }
const ret = { ...doc };
projectionLimb = prefix ? (projectionLimb || {}) : projection;
@@ -57,6 +60,9 @@ function applyInclusiveProjection(doc, projection, hasIncludedChildren, projecti
if (doc == null || typeof doc !== 'object') {
return doc;
}
+ if (Array.isArray(doc)) {
+ return doc.map(el => applyInclusiveProjection(el, projection, hasIncludedChildren, projectionLimb, prefix));
+ }
const ret = { ...doc };
projectionLimb = prefix ? (projectionLimb || {}) : projection;
diff --git a/lib/helpers/projection/hasIncludedChildren.js b/lib/helpers/projection/hasIncludedChildren.js
index d757b2c677e..50afc5adfb7 100644
--- a/lib/helpers/projection/hasIncludedChildren.js
+++ b/lib/helpers/projection/hasIncludedChildren.js
@@ -21,6 +21,7 @@ module.exports = function hasIncludedChildren(fields) {
const keys = Object.keys(fields);
for (const key of keys) {
+
if (key.indexOf('.') === -1) {
hasIncludedChildren[key] = 1;
continue;
diff --git a/lib/helpers/projection/isExclusive.js b/lib/helpers/projection/isExclusive.js
index a232857d601..e6ca3cad5ec 100644
--- a/lib/helpers/projection/isExclusive.js
+++ b/lib/helpers/projection/isExclusive.js
@@ -1,6 +1,7 @@
'use strict';
const isDefiningProjection = require('./isDefiningProjection');
+const isPOJO = require('../isPOJO');
/*!
* ignore
@@ -12,21 +13,22 @@ module.exports = function isExclusive(projection) {
}
const keys = Object.keys(projection);
- let ki = keys.length;
let exclude = null;
- if (ki === 1 && keys[0] === '_id') {
+ if (keys.length === 1 && keys[0] === '_id') {
exclude = !projection._id;
} else {
- while (ki--) {
+ for (let ki = 0; ki < keys.length; ++ki) {
// Does this projection explicitly define inclusion/exclusion?
// Explicitly avoid `$meta` and `$slice`
const key = keys[ki];
if (key !== '_id' && isDefiningProjection(projection[key])) {
- exclude = (projection[key] != null && typeof projection[key] === 'object') ?
- isExclusive(projection[key]) :
+ exclude = isPOJO(projection[key]) ?
+ (isExclusive(projection[key]) ?? exclude) :
!projection[key];
- break;
+ if (exclude != null) {
+ break;
+ }
}
}
}
diff --git a/lib/helpers/projection/isInclusive.js b/lib/helpers/projection/isInclusive.js
index eebb412c4a3..c53bac02873 100644
--- a/lib/helpers/projection/isInclusive.js
+++ b/lib/helpers/projection/isInclusive.js
@@ -1,6 +1,7 @@
'use strict';
const isDefiningProjection = require('./isDefiningProjection');
+const isPOJO = require('../isPOJO');
/*!
* ignore
@@ -26,7 +27,7 @@ module.exports = function isInclusive(projection) {
// If field is truthy (1, true, etc.) and not an object, then this
// projection must be inclusive. If object, assume its $meta, $slice, etc.
if (isDefiningProjection(projection[prop]) && !!projection[prop]) {
- if (projection[prop] != null && typeof projection[prop] === 'object') {
+ if (isPOJO(projection[prop])) {
return isInclusive(projection[prop]);
} else {
return !!projection[prop];
diff --git a/lib/helpers/projection/isNestedProjection.js b/lib/helpers/projection/isNestedProjection.js
new file mode 100644
index 00000000000..f53d9cddf3a
--- /dev/null
+++ b/lib/helpers/projection/isNestedProjection.js
@@ -0,0 +1,8 @@
+'use strict';
+
+module.exports = function isNestedProjection(val) {
+ if (val == null || typeof val !== 'object') {
+ return false;
+ }
+ return val.$slice == null && val.$elemMatch == null && val.$meta == null && val.$ == null;
+};
diff --git a/lib/helpers/promiseOrCallback.js b/lib/helpers/promiseOrCallback.js
index 4282a6ce498..952eecf4bf8 100644
--- a/lib/helpers/promiseOrCallback.js
+++ b/lib/helpers/promiseOrCallback.js
@@ -1,9 +1,8 @@
'use strict';
-const PromiseProvider = require('../promise_provider');
const immediate = require('./immediate');
-const emittedSymbol = Symbol('mongoose:emitted');
+const emittedSymbol = Symbol('mongoose#emitted');
module.exports = function promiseOrCallback(callback, fn, ee, Promise) {
if (typeof callback === 'function') {
@@ -35,7 +34,7 @@ module.exports = function promiseOrCallback(callback, fn, ee, Promise) {
}
}
- Promise = Promise || PromiseProvider.get();
+ Promise = Promise || global.Promise;
return new Promise((resolve, reject) => {
fn(function(error, res) {
diff --git a/lib/helpers/query/applyGlobalOption.js b/lib/helpers/query/applyGlobalOption.js
index 8888e368b9e..e93fa73d492 100644
--- a/lib/helpers/query/applyGlobalOption.js
+++ b/lib/helpers/query/applyGlobalOption.js
@@ -2,12 +2,12 @@
const utils = require('../../utils');
-function applyGlobalMaxTimeMS(options, model) {
- applyGlobalOption(options, model, 'maxTimeMS');
+function applyGlobalMaxTimeMS(options, connectionOptions, baseOptions) {
+ applyGlobalOption(options, connectionOptions, baseOptions, 'maxTimeMS');
}
-function applyGlobalDiskUse(options, model) {
- applyGlobalOption(options, model, 'allowDiskUse');
+function applyGlobalDiskUse(options, connectionOptions, baseOptions) {
+ applyGlobalOption(options, connectionOptions, baseOptions, 'allowDiskUse');
}
module.exports = {
@@ -16,14 +16,14 @@ module.exports = {
};
-function applyGlobalOption(options, model, optionName) {
+function applyGlobalOption(options, connectionOptions, baseOptions, optionName) {
if (utils.hasUserDefinedProperty(options, optionName)) {
return;
}
- if (utils.hasUserDefinedProperty(model.db.options, optionName)) {
- options[optionName] = model.db.options[optionName];
- } else if (utils.hasUserDefinedProperty(model.base.options, optionName)) {
- options[optionName] = model.base.options[optionName];
+ if (utils.hasUserDefinedProperty(connectionOptions, optionName)) {
+ options[optionName] = connectionOptions[optionName];
+ } else if (utils.hasUserDefinedProperty(baseOptions, optionName)) {
+ options[optionName] = baseOptions[optionName];
}
}
diff --git a/lib/helpers/query/applyQueryMiddleware.js b/lib/helpers/query/applyQueryMiddleware.js
deleted file mode 100644
index 692d69cee3a..00000000000
--- a/lib/helpers/query/applyQueryMiddleware.js
+++ /dev/null
@@ -1,79 +0,0 @@
-'use strict';
-
-/*!
- * ignore
- */
-
-module.exports = applyQueryMiddleware;
-
-const validOps = require('./validOps');
-
-/*!
- * ignore
- */
-
-applyQueryMiddleware.middlewareFunctions = validOps.concat([
- 'validate'
-]);
-
-/**
- * Apply query middleware
- *
- * @param {Query} Query constructor
- * @param {Model} model
- * @api private
- */
-
-function applyQueryMiddleware(Query, model) {
- const kareemOptions = {
- useErrorHandlers: true,
- numCallbackParams: 1,
- nullResultByDefault: true
- };
-
- const middleware = model.hooks.filter(hook => {
- const contexts = _getContexts(hook);
- if (hook.name === 'updateOne') {
- return contexts.query == null || !!contexts.query;
- }
- if (hook.name === 'deleteOne') {
- return !!contexts.query || Object.keys(contexts).length === 0;
- }
- if (hook.name === 'validate' || hook.name === 'remove') {
- return !!contexts.query;
- }
- if (hook.query != null || hook.document != null) {
- return !!hook.query;
- }
- return true;
- });
-
- // `update()` thunk has a different name because `_update` was already taken
- Query.prototype._execUpdate = middleware.createWrapper('update',
- Query.prototype._execUpdate, null, kareemOptions);
- // `distinct()` thunk has a different name because `_distinct` was already taken
- Query.prototype.__distinct = middleware.createWrapper('distinct',
- Query.prototype.__distinct, null, kareemOptions);
-
- // `validate()` doesn't have a thunk because it doesn't execute a query.
- Query.prototype.validate = middleware.createWrapper('validate',
- Query.prototype.validate, null, kareemOptions);
-
- applyQueryMiddleware.middlewareFunctions.
- filter(v => v !== 'update' && v !== 'distinct' && v !== 'validate').
- forEach(fn => {
- Query.prototype[`_${fn}`] = middleware.createWrapper(fn,
- Query.prototype[`_${fn}`], null, kareemOptions);
- });
-}
-
-function _getContexts(hook) {
- const ret = {};
- if (hook.hasOwnProperty('query')) {
- ret.query = hook.query;
- }
- if (hook.hasOwnProperty('document')) {
- ret.document = hook.document;
- }
- return ret;
-}
diff --git a/lib/helpers/query/cast$expr.js b/lib/helpers/query/cast$expr.js
index dd4102d80e0..8e84011b2c3 100644
--- a/lib/helpers/query/cast$expr.js
+++ b/lib/helpers/query/cast$expr.js
@@ -3,6 +3,7 @@
const CastError = require('../../error/cast');
const StrictModeError = require('../../error/strict');
const castNumber = require('../../cast/number');
+const omitUndefined = require('../omitUndefined');
const booleanComparison = new Set(['$and', '$or']);
const comparisonOperator = new Set(['$cmp', '$eq', '$lt', '$lte', '$gt', '$gte']);
@@ -29,7 +30,6 @@ const arithmeticOperatorNumber = new Set([
'$floor',
'$ln',
'$log10',
- '$round',
'$sqrt',
'$sin',
'$cos',
@@ -93,8 +93,12 @@ function _castExpression(val, schema, strictQuery) {
} else if (val.$ifNull != null) {
val.$ifNull.map(v => _castExpression(v, schema, strictQuery));
} else if (val.$switch != null) {
- val.branches.map(v => _castExpression(v, schema, strictQuery));
- val.default = _castExpression(val.default, schema, strictQuery);
+ if (Array.isArray(val.$switch.branches)) {
+ val.$switch.branches = val.$switch.branches.map(v => _castExpression(v, schema, strictQuery));
+ }
+ if ('default' in val.$switch) {
+ val.$switch.default = _castExpression(val.$switch.default, schema, strictQuery);
+ }
}
const keys = Object.keys(val);
@@ -118,19 +122,19 @@ function _castExpression(val, schema, strictQuery) {
if (val.$size) {
val.$size = castNumberOperator(val.$size, schema, strictQuery);
}
+ if (val.$round) {
+ const $round = val.$round;
+ if (!Array.isArray($round) || $round.length < 1 || $round.length > 2) {
+ throw new CastError('Array', $round, '$round');
+ }
+ val.$round = $round.map(v => castNumberOperator(v, schema, strictQuery));
+ }
- _omitUndefined(val);
+ omitUndefined(val);
return val;
}
-function _omitUndefined(val) {
- const keys = Object.keys(val);
- for (let i = 0, len = keys.length; i < len; ++i) {
- (val[keys[i]] === void 0) && delete val[keys[i]];
- }
-}
-
// { $op: }
function castNumberOperator(val) {
if (!isLiteral(val)) {
diff --git a/lib/helpers/query/castFilterPath.js b/lib/helpers/query/castFilterPath.js
index 8175145cb6d..c5c8d0fadfd 100644
--- a/lib/helpers/query/castFilterPath.js
+++ b/lib/helpers/query/castFilterPath.js
@@ -2,15 +2,15 @@
const isOperator = require('./isOperator');
-module.exports = function castFilterPath(query, schematype, val) {
- const ctx = query;
+module.exports = function castFilterPath(ctx, schematype, val) {
const any$conditionals = Object.keys(val).some(isOperator);
if (!any$conditionals) {
- return schematype.castForQueryWrapper({
- val: val,
- context: ctx
- });
+ return schematype.castForQuery(
+ null,
+ val,
+ ctx
+ );
}
const ks = Object.keys(val);
@@ -26,27 +26,27 @@ module.exports = function castFilterPath(query, schematype, val) {
const _keys = Object.keys(nested);
if (_keys.length && isOperator(_keys[0])) {
for (const key of Object.keys(nested)) {
- nested[key] = schematype.castForQueryWrapper({
- $conditional: key,
- val: nested[key],
- context: ctx
- });
+ nested[key] = schematype.castForQuery(
+ key,
+ nested[key],
+ ctx
+ );
}
} else {
- val[$cond] = schematype.castForQueryWrapper({
- $conditional: $cond,
- val: nested,
- context: ctx
- });
+ val[$cond] = schematype.castForQuery(
+ $cond,
+ nested,
+ ctx
+ );
}
continue;
}
} else {
- val[$cond] = schematype.castForQueryWrapper({
- $conditional: $cond,
- val: nested,
- context: ctx
- });
+ val[$cond] = schematype.castForQuery(
+ $cond,
+ nested,
+ ctx
+ );
}
}
diff --git a/lib/helpers/query/castUpdate.js b/lib/helpers/query/castUpdate.js
index e9c12c3ebce..3cf30cb0e17 100644
--- a/lib/helpers/query/castUpdate.js
+++ b/lib/helpers/query/castUpdate.js
@@ -2,17 +2,38 @@
const CastError = require('../../error/cast');
const MongooseError = require('../../error/mongooseError');
+const SchemaString = require('../../schema/string');
const StrictModeError = require('../../error/strict');
const ValidationError = require('../../error/validation');
const castNumber = require('../../cast/number');
const cast = require('../../cast');
const getConstructorName = require('../getConstructorName');
+const getDiscriminatorByValue = require('../discriminator/getDiscriminatorByValue');
const getEmbeddedDiscriminatorPath = require('./getEmbeddedDiscriminatorPath');
const handleImmutable = require('./handleImmutable');
const moveImmutableProperties = require('../update/moveImmutableProperties');
const schemaMixedSymbol = require('../../schema/symbols').schemaMixedSymbol;
const setDottedPath = require('../path/setDottedPath');
const utils = require('../../utils');
+const { internalToObjectOptions } = require('../../options');
+
+const mongodbUpdateOperators = new Set([
+ '$currentDate',
+ '$inc',
+ '$min',
+ '$max',
+ '$mul',
+ '$rename',
+ '$set',
+ '$setOnInsert',
+ '$unset',
+ '$addToSet',
+ '$pop',
+ '$pull',
+ '$push',
+ '$pullAll',
+ '$bit'
+]);
/**
* Casts an update op based on the given schema
@@ -20,7 +41,6 @@ const utils = require('../../utils');
* @param {Schema} schema
* @param {Object} obj
* @param {Object} [options]
- * @param {Boolean} [options.overwrite] defaults to false
* @param {Boolean|String} [options.strict] defaults to true
* @param {Query} context passed to setters
* @return {Boolean} true iff the update is non-empty
@@ -43,7 +63,28 @@ module.exports = function castUpdate(schema, obj, options, context, filter) {
return obj;
}
- if (options.upsert && !options.overwrite) {
+ if (schema != null &&
+ filter != null &&
+ utils.hasUserDefinedProperty(filter, schema.options.discriminatorKey) &&
+ typeof filter[schema.options.discriminatorKey] !== 'object' &&
+ schema.discriminators != null) {
+ const discriminatorValue = filter[schema.options.discriminatorKey];
+ const byValue = getDiscriminatorByValue(context.model.discriminators, discriminatorValue);
+ schema = schema.discriminators[discriminatorValue] ||
+ (byValue && byValue.schema) ||
+ schema;
+ } else if (schema != null &&
+ options.overwriteDiscriminatorKey &&
+ utils.hasUserDefinedProperty(obj, schema.options.discriminatorKey) &&
+ schema.discriminators != null) {
+ const discriminatorValue = obj[schema.options.discriminatorKey];
+ const byValue = getDiscriminatorByValue(context.model.discriminators, discriminatorValue);
+ schema = schema.discriminators[discriminatorValue] ||
+ (byValue && byValue.schema) ||
+ schema;
+ }
+
+ if (options.upsert) {
moveImmutableProperties(schema, obj, context);
}
@@ -52,13 +93,11 @@ module.exports = function castUpdate(schema, obj, options, context, filter) {
const ret = {};
let val;
let hasDollarKey = false;
- const overwrite = options.overwrite;
filter = filter || {};
while (i--) {
const op = ops[i];
- // if overwrite is set, don't do any of the special $set stuff
- if (op[0] !== '$' && !overwrite) {
+ if (!mongodbUpdateOperators.has(op)) {
// fix up $set sugar
if (!ret.$set) {
if (obj.$set) {
@@ -84,14 +123,15 @@ module.exports = function castUpdate(schema, obj, options, context, filter) {
const op = ops[i];
val = ret[op];
hasDollarKey = hasDollarKey || op.startsWith('$');
-
+ if (val != null && val.$__) {
+ val = val.toObject(internalToObjectOptions);
+ ret[op] = val;
+ }
if (val &&
typeof val === 'object' &&
!Buffer.isBuffer(val) &&
- (!overwrite || hasDollarKey)) {
+ mongodbUpdateOperators.has(op)) {
walkUpdatePath(schema, val, op, options, context, filter);
- } else if (overwrite && ret && typeof ret === 'object') {
- walkUpdatePath(schema, ret, '$set', options, context, filter);
} else {
const msg = 'Invalid atomic update value for ' + op + '. '
+ 'Expected an object, received ' + typeof val;
@@ -108,7 +148,8 @@ module.exports = function castUpdate(schema, obj, options, context, filter) {
Object.keys(filter).length > 0) {
// Trick the driver into allowing empty upserts to work around
// https://github.com/mongodb/node-mongodb-native/pull/2490
- return { $setOnInsert: filter };
+ // Shallow clone to avoid passing defaults in re: gh-13962
+ return { $setOnInsert: { ...filter } };
}
return ret;
};
@@ -184,6 +225,12 @@ function walkUpdatePath(schema, obj, op, options, context, filter, pref) {
// an update.
if (op === '$pull') {
schematype = schema._getSchema(prefix + key);
+ if (schematype == null) {
+ const _res = getEmbeddedDiscriminatorPath(schema, obj, filter, prefix + key, options);
+ if (_res.schematype != null) {
+ schematype = _res.schematype;
+ }
+ }
if (schematype != null && schematype.schema != null) {
obj[key] = cast(schematype.schema, obj[key], options, context);
hasKeys = true;
@@ -220,8 +267,7 @@ function walkUpdatePath(schema, obj, op, options, context, filter, pref) {
}
if (op !== '$setOnInsert' &&
- !options.overwrite &&
- handleImmutable(schematype, strict, obj, key, prefix + key, context)) {
+ handleImmutable(schematype, strict, obj, key, prefix + key, options, context)) {
continue;
}
@@ -252,7 +298,7 @@ function walkUpdatePath(schema, obj, op, options, context, filter, pref) {
if (schematype != null && schematype.$isSingleNested) {
const _strict = strict == null ? schematype.schema.options.strict : strict;
try {
- obj[key] = schematype.castForQuery(val, context, { strict: _strict });
+ obj[key] = schematype.castForQuery(null, val, context, { strict: _strict });
} catch (error) {
aggregatedError = _appendError(error, context, key, aggregatedError);
}
@@ -284,6 +330,20 @@ function walkUpdatePath(schema, obj, op, options, context, filter, pref) {
continue;
}
+ hasKeys = true;
+ } else if (op === '$rename') {
+ const schematype = new SchemaString(`${prefix}${key}.$rename`);
+ try {
+ obj[key] = castUpdateVal(schematype, val, op, key, context, prefix + key);
+ } catch (error) {
+ aggregatedError = _appendError(error, context, key, aggregatedError);
+ }
+
+ if (obj[key] === void 0) {
+ delete obj[key];
+ continue;
+ }
+
hasKeys = true;
} else {
const pathToCheck = (prefix + key);
@@ -315,8 +375,7 @@ function walkUpdatePath(schema, obj, op, options, context, filter, pref) {
// You can use `$setOnInsert` with immutable keys
if (op !== '$setOnInsert' &&
- !options.overwrite &&
- handleImmutable(schematype, strict, obj, key, prefix + key, context)) {
+ handleImmutable(schematype, strict, obj, key, prefix + key, options, context)) {
continue;
}
@@ -350,17 +409,19 @@ function walkUpdatePath(schema, obj, op, options, context, filter, pref) {
delete obj[key];
}
} else {
- // gh-1845 temporary fix: ignore $rename. See gh-3027 for tracking
- // improving this.
if (op === '$rename') {
- hasKeys = true;
+ if (obj[key] == null) {
+ throw new CastError('String', obj[key], `${prefix}${key}.$rename`);
+ }
+ const schematype = new SchemaString(`${prefix}${key}.$rename`);
+ obj[key] = schematype.castForQuery(null, obj[key], context);
continue;
}
try {
if (prefix.length === 0 || key.indexOf('.') === -1) {
obj[key] = castUpdateVal(schematype, val, op, key, context, prefix + key);
- } else {
+ } else if (isStrict !== false || schematype != null) {
// Setting a nested dotted path that's in the schema. We don't allow paths with '.' in
// a schema, so replace the dotted path with a nested object to avoid ending up with
// dotted properties in the updated object. See (gh-10200)
@@ -521,10 +582,11 @@ function castUpdateVal(schema, val, op, $conditional, context, path) {
}
if (op === '$inc') {
// Support `$inc` with long, int32, etc. (gh-4283)
- return schema.castForQueryWrapper({
- val: val,
- context: context
- });
+ return schema.castForQuery(
+ null,
+ val,
+ context
+ );
}
try {
return castNumber(val);
@@ -539,22 +601,26 @@ function castUpdateVal(schema, val, op, $conditional, context, path) {
return Boolean(val);
}
- if (/^\$/.test($conditional)) {
- return schema.castForQueryWrapper({
- $conditional: $conditional,
- val: val,
- context: context
- });
+ if (mongodbUpdateOperators.has($conditional)) {
+ return schema.castForQuery(
+ $conditional,
+ val,
+ context
+ );
}
if (overwriteOps[op]) {
- return schema.castForQueryWrapper({
- val: val,
- context: context,
- $skipQueryCastForUpdate: val != null && schema.$isMongooseArray && schema.$fullPath != null && !schema.$fullPath.match(/\d+$/),
- $applySetters: schema[schemaMixedSymbol] != null
- });
+ const skipQueryCastForUpdate = val != null && schema.$isMongooseArray && schema.$fullPath != null && !schema.$fullPath.match(/\d+$/);
+ const applySetters = schema[schemaMixedSymbol] != null;
+ if (skipQueryCastForUpdate || applySetters) {
+ return schema.applySetters(val, context);
+ }
+ return schema.castForQuery(
+ null,
+ val,
+ context
+ );
}
- return schema.castForQueryWrapper({ val: val, context: context });
+ return schema.castForQuery(null, val, context);
}
diff --git a/lib/helpers/query/completeMany.js b/lib/helpers/query/completeMany.js
deleted file mode 100644
index 4ba8c2b083e..00000000000
--- a/lib/helpers/query/completeMany.js
+++ /dev/null
@@ -1,52 +0,0 @@
-'use strict';
-
-const helpers = require('../../queryhelpers');
-const immediate = require('../immediate');
-
-module.exports = completeMany;
-
-/**
- * Given a model and an array of docs, hydrates all the docs to be instances
- * of the model. Used to initialize docs returned from the db from `find()`
- *
- * @param {Model} model
- * @param {Array} docs
- * @param {Object} fields the projection used, including `select` from schemas
- * @param {Object} userProvidedFields the user-specified projection
- * @param {Object} [opts]
- * @param {Array} [opts.populated]
- * @param {ClientSession} [opts.session]
- * @param {Function} callback
- * @api private
- */
-
-function completeMany(model, docs, fields, userProvidedFields, opts, callback) {
- const arr = [];
- let count = docs.length;
- const len = count;
- let error = null;
-
- function init(_error) {
- if (_error != null) {
- error = error || _error;
- }
- if (error != null) {
- --count || immediate(() => callback(error));
- return;
- }
- --count || immediate(() => callback(error, arr));
- }
-
- for (let i = 0; i < len; ++i) {
- arr[i] = helpers.createModel(model, docs[i], fields, userProvidedFields);
- try {
- arr[i].$init(docs[i], opts, init);
- } catch (error) {
- init(error);
- }
-
- if (opts.session != null) {
- arr[i].$session(opts.session);
- }
- }
-}
diff --git a/lib/helpers/query/getEmbeddedDiscriminatorPath.js b/lib/helpers/query/getEmbeddedDiscriminatorPath.js
index f376916b590..548cf3b0330 100644
--- a/lib/helpers/query/getEmbeddedDiscriminatorPath.js
+++ b/lib/helpers/query/getEmbeddedDiscriminatorPath.js
@@ -28,7 +28,8 @@ module.exports = function getEmbeddedDiscriminatorPath(schema, update, filter, p
const updatedPathsByFilter = updatedPathsByArrayFilter(update);
for (let i = 0; i < parts.length; ++i) {
- const subpath = cleanPositionalOperators(parts.slice(0, i + 1).join('.'));
+ const originalSubpath = parts.slice(0, i + 1).join('.');
+ const subpath = cleanPositionalOperators(originalSubpath);
schematype = schema.path(subpath);
if (schematype == null) {
continue;
@@ -56,6 +57,11 @@ module.exports = function getEmbeddedDiscriminatorPath(schema, update, filter, p
discriminatorKey = filter[wrapperPath].$elemMatch[key];
}
+ const discriminatorKeyUpdatePath = originalSubpath + '.' + key;
+ if (discriminatorKeyUpdatePath in update) {
+ discriminatorKey = update[discriminatorKeyUpdatePath];
+ }
+
if (discriminatorValuePath in update) {
discriminatorKey = update[discriminatorValuePath];
}
@@ -75,7 +81,11 @@ module.exports = function getEmbeddedDiscriminatorPath(schema, update, filter, p
continue;
}
- const discriminatorSchema = getDiscriminatorByValue(schematype.caster.discriminators, discriminatorKey).schema;
+ const discriminator = getDiscriminatorByValue(schematype.caster.discriminators, discriminatorKey);
+ const discriminatorSchema = discriminator && discriminator.schema;
+ if (discriminatorSchema == null) {
+ continue;
+ }
const rest = parts.slice(i + 1).join('.');
schematype = discriminatorSchema.path(rest);
diff --git a/lib/helpers/query/handleImmutable.js b/lib/helpers/query/handleImmutable.js
index 22adb3c50de..0102db42a46 100644
--- a/lib/helpers/query/handleImmutable.js
+++ b/lib/helpers/query/handleImmutable.js
@@ -2,7 +2,20 @@
const StrictModeError = require('../../error/strict');
-module.exports = function handleImmutable(schematype, strict, obj, key, fullPath, ctx) {
+/**
+ * Handle immutable option for a given path when casting updates based on options
+ *
+ * @param {SchemaType} schematype the resolved schematype for this path
+ * @param {Boolean | 'throw' | null} strict whether strict mode is set for this query
+ * @param {Object} obj the object containing the value being checked so we can delete
+ * @param {String} key the key in `obj` which we are checking for immutability
+ * @param {String} fullPath the full path being checked
+ * @param {Object} options the query options
+ * @param {Query} ctx the query. Passed as `this` and first param to the `immutable` option, if `immutable` is a function
+ * @returns true if field was removed, false otherwise
+ */
+
+module.exports = function handleImmutable(schematype, strict, obj, key, fullPath, options, ctx) {
if (schematype == null || !schematype.options || !schematype.options.immutable) {
return false;
}
@@ -15,6 +28,9 @@ module.exports = function handleImmutable(schematype, strict, obj, key, fullPath
return false;
}
+ if (options && options.overwriteImmutable) {
+ return false;
+ }
if (strict === false) {
return false;
}
diff --git a/lib/helpers/query/handleReadPreferenceAliases.js b/lib/helpers/query/handleReadPreferenceAliases.js
new file mode 100644
index 00000000000..c3af278b038
--- /dev/null
+++ b/lib/helpers/query/handleReadPreferenceAliases.js
@@ -0,0 +1,23 @@
+'use strict';
+
+module.exports = function handleReadPreferenceAliases(pref) {
+ switch (pref) {
+ case 'p':
+ pref = 'primary';
+ break;
+ case 'pp':
+ pref = 'primaryPreferred';
+ break;
+ case 's':
+ pref = 'secondary';
+ break;
+ case 'sp':
+ pref = 'secondaryPreferred';
+ break;
+ case 'n':
+ pref = 'nearest';
+ break;
+ }
+
+ return pref;
+};
diff --git a/lib/helpers/query/selectPopulatedFields.js b/lib/helpers/query/selectPopulatedFields.js
index 92f1d87e04b..dc96caf6fa4 100644
--- a/lib/helpers/query/selectPopulatedFields.js
+++ b/lib/helpers/query/selectPopulatedFields.js
@@ -21,12 +21,25 @@ module.exports = function selectPopulatedFields(fields, userProvidedFields, popu
} else if (userProvidedFields[path] === 0) {
delete fields[path];
}
+
+ const refPath = populateOptions[path]?.refPath;
+ if (typeof refPath === 'string') {
+ if (!isPathInFields(userProvidedFields, refPath)) {
+ fields[refPath] = 1;
+ } else if (userProvidedFields[refPath] === 0) {
+ delete fields[refPath];
+ }
+ }
}
} else if (isExclusive(fields)) {
for (const path of paths) {
if (userProvidedFields[path] == null) {
delete fields[path];
}
+ const refPath = populateOptions[path]?.refPath;
+ if (typeof refPath === 'string' && userProvidedFields[refPath] == null) {
+ delete fields[refPath];
+ }
}
}
};
diff --git a/lib/helpers/query/validOps.js b/lib/helpers/query/validOps.js
index f554ff2d9b0..2d1aa477b1c 100644
--- a/lib/helpers/query/validOps.js
+++ b/lib/helpers/query/validOps.js
@@ -1,24 +1,3 @@
'use strict';
-module.exports = Object.freeze([
- // Read
- 'count',
- 'countDocuments',
- 'distinct',
- 'estimatedDocumentCount',
- 'find',
- 'findOne',
- // Update
- 'findOneAndReplace',
- 'findOneAndUpdate',
- 'replaceOne',
- 'update',
- 'updateMany',
- 'updateOne',
- // Delete
- 'deleteMany',
- 'deleteOne',
- 'findOneAndDelete',
- 'findOneAndRemove',
- 'remove'
-]);
+module.exports = require('../../constants').queryMiddlewareFunctions;
diff --git a/lib/helpers/query/wrapThunk.js b/lib/helpers/query/wrapThunk.js
deleted file mode 100644
index 1303a4708d6..00000000000
--- a/lib/helpers/query/wrapThunk.js
+++ /dev/null
@@ -1,31 +0,0 @@
-'use strict';
-
-const MongooseError = require('../../error/mongooseError');
-
-/**
- * A query thunk is the function responsible for sending the query to MongoDB,
- * like `Query#_findOne()` or `Query#_execUpdate()`. The `Query#exec()` function
- * calls a thunk. The term "thunk" here is the traditional Node.js definition:
- * a function that takes exactly 1 parameter, a callback.
- *
- * This function defines common behavior for all query thunks.
- * @param {Function} fn
- * @api private
- */
-
-module.exports = function wrapThunk(fn) {
- return function _wrappedThunk(cb) {
- if (this._executionStack != null) {
- let str = this.toString();
- if (str.length > 60) {
- str = str.slice(0, 60) + '...';
- }
- const err = new MongooseError('Query was already executed: ' + str);
- err.originalStack = this._executionStack.stack;
- return cb(err);
- }
- this._executionStack = new Error();
-
- fn.call(this, cb);
- };
-};
diff --git a/lib/helpers/schema/applyReadConcern.js b/lib/helpers/schema/applyReadConcern.js
new file mode 100644
index 00000000000..050fa9c6df0
--- /dev/null
+++ b/lib/helpers/schema/applyReadConcern.js
@@ -0,0 +1,20 @@
+'use strict';
+
+module.exports = function applyReadConcern(schema, options) {
+ if (options.readConcern !== undefined) {
+ return;
+ }
+
+ // Don't apply default read concern to operations in transactions,
+ // because you shouldn't set read concern on individual operations
+ // within a transaction.
+ // See: https://www.mongodb.com/docs/manual/reference/read-concern/
+ if (options && options.session && options.session.transaction) {
+ return;
+ }
+
+ const level = schema.options?.readConcern?.level;
+ if (level != null) {
+ options.readConcern = { level };
+ }
+};
diff --git a/lib/helpers/schema/applyWriteConcern.js b/lib/helpers/schema/applyWriteConcern.js
index 4095bd94bc7..28338cf58c3 100644
--- a/lib/helpers/schema/applyWriteConcern.js
+++ b/lib/helpers/schema/applyWriteConcern.js
@@ -1,9 +1,16 @@
'use strict';
-const get = require('../get');
-
module.exports = function applyWriteConcern(schema, options) {
- const writeConcern = get(schema, 'options.writeConcern', {});
+ if (options.writeConcern != null) {
+ return;
+ }
+ // Don't apply default write concern to operations in transactions,
+ // because setting write concern on an operation in a transaction is an error
+ // See: https://www.mongodb.com/docs/manual/reference/write-concern/
+ if (options && options.session && options.session.transaction) {
+ return;
+ }
+ const writeConcern = schema.options.writeConcern ?? {};
if (Object.keys(writeConcern).length != 0) {
options.writeConcern = {};
if (!('w' in options) && writeConcern.w != null) {
diff --git a/lib/helpers/schema/getIndexes.js b/lib/helpers/schema/getIndexes.js
index 28e0a3e6f5b..706439d321d 100644
--- a/lib/helpers/schema/getIndexes.js
+++ b/lib/helpers/schema/getIndexes.js
@@ -39,6 +39,11 @@ module.exports = function getIndexes(schema) {
continue;
}
+ if (path._duplicateKeyErrorMessage != null) {
+ schema._duplicateKeyErrorMessagesByPath = schema._duplicateKeyErrorMessagesByPath || {};
+ schema._duplicateKeyErrorMessagesByPath[key] = path._duplicateKeyErrorMessage;
+ }
+
if (path.$isMongooseDocumentArray || path.$isSingleNested) {
if (get(path, 'options.excludeIndexes') !== true &&
get(path, 'schemaOptions.excludeIndexes') !== true &&
@@ -78,7 +83,15 @@ module.exports = function getIndexes(schema) {
field[prefix + key] = 'text';
delete options.text;
} else {
- const isDescendingIndex = Number(index) === -1;
+ let isDescendingIndex = false;
+ if (index === 'descending' || index === 'desc') {
+ isDescendingIndex = true;
+ } else if (index === 'ascending' || index === 'asc') {
+ isDescendingIndex = false;
+ } else {
+ isDescendingIndex = Number(index) === -1;
+ }
+
field[prefix + key] = isDescendingIndex ? -1 : 1;
}
diff --git a/lib/helpers/schema/getPath.js b/lib/helpers/schema/getPath.js
index b4022826e38..b881069546c 100644
--- a/lib/helpers/schema/getPath.js
+++ b/lib/helpers/schema/getPath.js
@@ -13,7 +13,6 @@ module.exports = function getPath(schema, path) {
if (schematype != null) {
return schematype;
}
-
const pieces = path.split('.');
let cur = '';
let isArray = false;
diff --git a/lib/helpers/schema/getSubdocumentStrictValue.js b/lib/helpers/schema/getSubdocumentStrictValue.js
new file mode 100644
index 00000000000..8a8f83e5bc6
--- /dev/null
+++ b/lib/helpers/schema/getSubdocumentStrictValue.js
@@ -0,0 +1,32 @@
+'use strict';
+
+/**
+ * Find the `strict` mode setting for the deepest subdocument along a given path
+ * to ensure we have the correct default value for `strict`. When setting values
+ * underneath a subdocument, we should use the subdocument's `strict` setting by
+ * default, not the top-level document's.
+ *
+ * @param {Schema} schema
+ * @param {String[]} parts
+ * @returns {boolean | 'throw' | undefined}
+ */
+
+module.exports = function getSubdocumentStrictValue(schema, parts) {
+ if (parts.length === 1) {
+ return undefined;
+ }
+ let cur = parts[0];
+ let strict = undefined;
+ for (let i = 0; i < parts.length - 1; ++i) {
+ const curSchemaType = schema.path(cur);
+ if (curSchemaType && curSchemaType.schema) {
+ strict = curSchemaType.schema.options.strict;
+ schema = curSchemaType.schema;
+ cur = curSchemaType.$isMongooseDocumentArray && !isNaN(parts[i + 1]) ? '' : parts[i + 1];
+ } else {
+ cur += cur.length ? ('.' + parts[i + 1]) : parts[i + 1];
+ }
+ }
+
+ return strict;
+};
diff --git a/lib/helpers/schema/idGetter.js b/lib/helpers/schema/idGetter.js
index 31ea2ec8659..5b576531272 100644
--- a/lib/helpers/schema/idGetter.js
+++ b/lib/helpers/schema/idGetter.js
@@ -12,7 +12,9 @@ module.exports = function addIdGetter(schema) {
if (!autoIdGetter) {
return schema;
}
-
+ if (schema.aliases && schema.aliases.id) {
+ return schema;
+ }
schema.virtual('id').get(idGetter);
return schema;
@@ -25,7 +27,7 @@ module.exports = function addIdGetter(schema) {
function idGetter() {
if (this._id != null) {
- return String(this._id);
+ return this._id.toString();
}
return null;
diff --git a/lib/helpers/schema/merge.js b/lib/helpers/schema/merge.js
index 55ed82466f7..e7bb91f6ae0 100644
--- a/lib/helpers/schema/merge.js
+++ b/lib/helpers/schema/merge.js
@@ -9,12 +9,20 @@ module.exports = function merge(s1, s2, skipConflictingPaths) {
}
pathsToAdd[key] = s2.tree[key];
}
- s1.add(pathsToAdd);
+ s1.options._isMerging = true;
+ s1.add(pathsToAdd, null);
+ delete s1.options._isMerging;
s1.callQueue = s1.callQueue.concat(s2.callQueue);
s1.method(s2.methods);
s1.static(s2.statics);
+ for (const [option, value] of Object.entries(s2._userProvidedOptions)) {
+ if (!(option in s1._userProvidedOptions)) {
+ s1.set(option, value);
+ }
+ }
+
for (const query in s2.query) {
s1.query[query] = s2.query[query];
}
diff --git a/lib/helpers/symbols.js b/lib/helpers/symbols.js
index f12db3d8272..a9af890a5d6 100644
--- a/lib/helpers/symbols.js
+++ b/lib/helpers/symbols.js
@@ -5,7 +5,7 @@ exports.arrayAtomicsSymbol = Symbol('mongoose#Array#_atomics');
exports.arrayParentSymbol = Symbol('mongoose#Array#_parent');
exports.arrayPathSymbol = Symbol('mongoose#Array#_path');
exports.arraySchemaSymbol = Symbol('mongoose#Array#_schema');
-exports.documentArrayParent = Symbol('mongoose:documentArrayParent');
+exports.documentArrayParent = Symbol('mongoose#documentArrayParent');
exports.documentIsSelected = Symbol('mongoose#Document#isSelected');
exports.documentIsModified = Symbol('mongoose#Document#isModified');
exports.documentModifiedPaths = Symbol('mongoose#Document#modifiedPaths');
@@ -13,8 +13,8 @@ exports.documentSchemaSymbol = Symbol('mongoose#Document#schema');
exports.getSymbol = Symbol('mongoose#Document#get');
exports.modelSymbol = Symbol('mongoose#Model');
exports.objectIdSymbol = Symbol('mongoose#ObjectId');
-exports.populateModelSymbol = Symbol('mongoose.PopulateOptions#Model');
+exports.populateModelSymbol = Symbol('mongoose#PopulateOptions#Model');
exports.schemaTypeSymbol = Symbol('mongoose#schemaType');
-exports.sessionNewDocuments = Symbol('mongoose:ClientSession#newDocuments');
+exports.sessionNewDocuments = Symbol('mongoose#ClientSession#newDocuments');
exports.scopeSymbol = Symbol('mongoose#Document#scope');
-exports.validatorErrorSymbol = Symbol('mongoose:validatorError');
+exports.validatorErrorSymbol = Symbol('mongoose#validatorError');
diff --git a/lib/helpers/timestamps/setupTimestamps.js b/lib/helpers/timestamps/setupTimestamps.js
index ed44e7d938c..f6ba12b98b6 100644
--- a/lib/helpers/timestamps/setupTimestamps.js
+++ b/lib/helpers/timestamps/setupTimestamps.js
@@ -7,17 +7,20 @@ const handleTimestampOption = require('../schema/handleTimestampOption');
const setDocumentTimestamps = require('./setDocumentTimestamps');
const symbols = require('../../schema/symbols');
+const replaceOps = new Set([
+ 'replaceOne',
+ 'findOneAndReplace'
+]);
+
module.exports = function setupTimestamps(schema, timestamps) {
const childHasTimestamp = schema.childSchemas.find(withTimestamp);
function withTimestamp(s) {
const ts = s.schema.options.timestamps;
return !!ts;
}
-
if (!timestamps && !childHasTimestamp) {
return;
}
-
const createdAt = handleTimestampOption(timestamps, 'createdAt');
const updatedAt = handleTimestampOption(timestamps, 'updatedAt');
const currentTime = timestamps != null && timestamps.hasOwnProperty('currentTime') ?
@@ -52,15 +55,15 @@ module.exports = function setupTimestamps(schema, timestamps) {
schema.methods.initializeTimestamps = function() {
const ts = currentTime != null ?
- currentTime() :
- this.constructor.base.now();
+ currentTime() : this.constructor.base.now();
+
+
if (createdAt && !this.get(createdAt)) {
this.$set(createdAt, ts);
}
if (updatedAt && !this.get(updatedAt)) {
this.$set(updatedAt, ts);
}
-
if (this.$isSubdocument) {
return this;
}
@@ -90,11 +93,17 @@ module.exports = function setupTimestamps(schema, timestamps) {
currentTime() :
this.model.base.now();
// Replacing with null update should still trigger timestamps
- if (this.op === 'findOneAndReplace' && this.getUpdate() == null) {
+ if (replaceOps.has(this.op) && this.getUpdate() == null) {
this.setUpdate({});
}
- applyTimestampsToUpdate(now, createdAt, updatedAt, this.getUpdate(),
- this.options, this.schema);
+ applyTimestampsToUpdate(
+ now,
+ createdAt,
+ updatedAt,
+ this.getUpdate(),
+ this._mongooseOptions,
+ replaceOps.has(this.op)
+ );
applyTimestampsToChildren(now, this.getUpdate(), this.model.schema);
next();
}
diff --git a/lib/helpers/update/applyTimestampsToUpdate.js b/lib/helpers/update/applyTimestampsToUpdate.js
index 3c48f965be5..e8d3217fbb9 100644
--- a/lib/helpers/update/applyTimestampsToUpdate.js
+++ b/lib/helpers/update/applyTimestampsToUpdate.js
@@ -12,10 +12,9 @@ module.exports = applyTimestampsToUpdate;
* ignore
*/
-function applyTimestampsToUpdate(now, createdAt, updatedAt, currentUpdate, options) {
+function applyTimestampsToUpdate(now, createdAt, updatedAt, currentUpdate, options, isReplace) {
const updates = currentUpdate;
let _updates = updates;
- const overwrite = get(options, 'overwrite', false);
const timestamps = get(options, 'timestamps', true);
// Support skipping timestamps at the query level, see gh-6980
@@ -26,7 +25,7 @@ function applyTimestampsToUpdate(now, createdAt, updatedAt, currentUpdate, optio
const skipCreatedAt = timestamps != null && timestamps.createdAt === false;
const skipUpdatedAt = timestamps != null && timestamps.updatedAt === false;
- if (overwrite) {
+ if (isReplace) {
if (currentUpdate && currentUpdate.$set) {
currentUpdate = currentUpdate.$set;
updates.$set = {};
@@ -44,11 +43,12 @@ function applyTimestampsToUpdate(now, createdAt, updatedAt, currentUpdate, optio
if (Array.isArray(updates)) {
// Update with aggregation pipeline
+ if (updatedAt == null) {
+ return updates;
+ }
updates.push({ $set: { [updatedAt]: now } });
-
return updates;
}
-
updates.$set = updates.$set || {};
if (!skipUpdatedAt && updatedAt &&
(!currentUpdate.$currentDate || !currentUpdate.$currentDate[updatedAt])) {
diff --git a/lib/helpers/update/castArrayFilters.js b/lib/helpers/update/castArrayFilters.js
index 163e33be14c..65af785cf1c 100644
--- a/lib/helpers/update/castArrayFilters.js
+++ b/lib/helpers/update/castArrayFilters.js
@@ -102,7 +102,7 @@ function _castArrayFilters(arrayFilters, schema, strictQuery, updatedPathsByFilt
if (typeof filter[key] === 'object') {
filter[key] = castFilterPath(query, schematype, filter[key]);
} else {
- filter[key] = schematype.castForQuery(filter[key]);
+ filter[key] = schematype.castForQuery(null, filter[key]);
}
}
}
diff --git a/lib/helpers/update/decorateUpdateWithVersionKey.js b/lib/helpers/update/decorateUpdateWithVersionKey.js
new file mode 100644
index 00000000000..161729844cc
--- /dev/null
+++ b/lib/helpers/update/decorateUpdateWithVersionKey.js
@@ -0,0 +1,26 @@
+'use strict';
+
+const modifiedPaths = require('./modifiedPaths');
+
+/**
+ * Decorate the update with a version key, if necessary
+ * @api private
+ */
+
+module.exports = function decorateUpdateWithVersionKey(update, options, versionKey) {
+ if (!versionKey || !(options && options.upsert || false)) {
+ return;
+ }
+
+ const updatedPaths = modifiedPaths(update);
+ if (!updatedPaths[versionKey]) {
+ if (options.overwrite) {
+ update[versionKey] = 0;
+ } else {
+ if (!update.$setOnInsert) {
+ update.$setOnInsert = {};
+ }
+ update.$setOnInsert[versionKey] = 0;
+ }
+ }
+};
diff --git a/lib/index.js b/lib/index.js
index d071cd9e4bb..67534e9e793 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -4,1333 +4,14 @@
* Module dependencies.
*/
-require('./driver').set(require('./drivers/node-mongodb-native'));
+const mongodbDriver = require('./drivers/node-mongodb-native');
-const Document = require('./document');
-const EventEmitter = require('events').EventEmitter;
-const Kareem = require('kareem');
-const Schema = require('./schema');
-const SchemaType = require('./schematype');
-const SchemaTypes = require('./schema/index');
-const VirtualType = require('./virtualtype');
-const STATES = require('./connectionstate');
-const VALID_OPTIONS = require('./validoptions');
-const Types = require('./types');
-const Query = require('./query');
-const Model = require('./model');
-const applyPlugins = require('./helpers/schema/applyPlugins');
-const builtinPlugins = require('./plugins');
-const driver = require('./driver');
-const promiseOrCallback = require('./helpers/promiseOrCallback');
-const legacyPluralize = require('./helpers/pluralize');
-const utils = require('./utils');
-const pkg = require('../package.json');
-const cast = require('./cast');
+require('./driver').set(mongodbDriver);
-const Aggregate = require('./aggregate');
-const PromiseProvider = require('./promise_provider');
-const printStrictQueryWarning = require('./helpers/printStrictQueryWarning');
-const trusted = require('./helpers/query/trusted').trusted;
-const sanitizeFilter = require('./helpers/query/sanitizeFilter');
-const isBsonType = require('./helpers/isBsonType');
-const MongooseError = require('./error/mongooseError');
-const SetOptionError = require('./error/setOptionError');
+const mongoose = require('./mongoose');
-const defaultMongooseSymbol = Symbol.for('mongoose:default');
+mongoose.setDriver(mongodbDriver);
-require('./helpers/printJestWarning');
+mongoose.Mongoose.prototype.mongo = require('mongodb');
-const objectIdHexRegexp = /^[0-9A-Fa-f]{24}$/;
-
-/**
- * Mongoose constructor.
- *
- * The exports object of the `mongoose` module is an instance of this class.
- * Most apps will only use this one instance.
- *
- * #### Example:
- *
- * const mongoose = require('mongoose');
- * mongoose instanceof mongoose.Mongoose; // true
- *
- * // Create a new Mongoose instance with its own `connect()`, `set()`, `model()`, etc.
- * const m = new mongoose.Mongoose();
- *
- * @api public
- * @param {Object} options see [`Mongoose#set()` docs](/docs/api/mongoose.html#mongoose_Mongoose-set)
- */
-function Mongoose(options) {
- this.connections = [];
- this.nextConnectionId = 0;
- this.models = {};
- this.events = new EventEmitter();
- this.__driver = driver.get();
- // default global options
- this.options = Object.assign({
- pluralization: true,
- autoIndex: true,
- autoCreate: true
- }, options);
- const createInitialConnection = utils.getOption('createInitialConnection', this.options);
- if (createInitialConnection == null || createInitialConnection) {
- const conn = this.createConnection(); // default connection
- conn.models = this.models;
- }
-
- if (this.options.pluralization) {
- this._pluralize = legacyPluralize;
- }
-
- // If a user creates their own Mongoose instance, give them a separate copy
- // of the `Schema` constructor so they get separate custom types. (gh-6933)
- if (!options || !options[defaultMongooseSymbol]) {
- const _this = this;
- this.Schema = function() {
- this.base = _this;
- return Schema.apply(this, arguments);
- };
- this.Schema.prototype = Object.create(Schema.prototype);
-
- Object.assign(this.Schema, Schema);
- this.Schema.base = this;
- this.Schema.Types = Object.assign({}, Schema.Types);
- } else {
- // Hack to work around babel's strange behavior with
- // `import mongoose, { Schema } from 'mongoose'`. Because `Schema` is not
- // an own property of a Mongoose global, Schema will be undefined. See gh-5648
- for (const key of ['Schema', 'model']) {
- this[key] = Mongoose.prototype[key];
- }
- }
- this.Schema.prototype.base = this;
-
- Object.defineProperty(this, 'plugins', {
- configurable: false,
- enumerable: true,
- writable: false,
- value: Object.values(builtinPlugins).map(plugin => ([plugin, { deduplicate: true }]))
- });
-}
-
-Mongoose.prototype.cast = cast;
-/**
- * Expose connection states for user-land
- *
- * @memberOf Mongoose
- * @property STATES
- * @api public
- */
-Mongoose.prototype.STATES = STATES;
-
-/**
- * Expose connection states for user-land
- *
- * @memberOf Mongoose
- * @property ConnectionStates
- * @api public
- */
-Mongoose.prototype.ConnectionStates = STATES;
-
-/**
- * Object with `get()` and `set()` containing the underlying driver this Mongoose instance
- * uses to communicate with the database. A driver is a Mongoose-specific interface that defines functions
- * like `find()`.
- *
- * @deprecated
- * @memberOf Mongoose
- * @property driver
- * @api public
- */
-
-Mongoose.prototype.driver = driver;
-
-/**
- * Overwrites the current driver used by this Mongoose instance. A driver is a
- * Mongoose-specific interface that defines functions like `find()`.
- *
- * @memberOf Mongoose
- * @method setDriver
- * @api public
- */
-
-Mongoose.prototype.setDriver = function setDriver(driver) {
- const _mongoose = this instanceof Mongoose ? this : mongoose;
-
- if (_mongoose.__driver === driver) {
- return _mongoose;
- }
-
- const openConnection = _mongoose.connections && _mongoose.connections.find(conn => conn.readyState !== STATES.disconnected);
- if (openConnection) {
- const msg = 'Cannot modify Mongoose driver if a connection is already open. ' +
- 'Call `mongoose.disconnect()` before modifying the driver';
- throw new MongooseError(msg);
- }
- _mongoose.__driver = driver;
-
- const Connection = driver.getConnection();
- _mongoose.connections = [new Connection(_mongoose)];
- _mongoose.connections[0].models = _mongoose.models;
-
- return _mongoose;
-};
-
-/**
- * Sets mongoose options
- *
- * `key` can be used a object to set multiple options at once.
- * If a error gets thrown for one option, other options will still be evaluated.
- *
- * #### Example:
- *
- * mongoose.set('test', value) // sets the 'test' option to `value`
- *
- * mongoose.set('debug', true) // enable logging collection methods + arguments to the console/file
- *
- * mongoose.set('debug', function(collectionName, methodName, ...methodArgs) {}); // use custom function to log collection methods + arguments
- *
- * mongoose.set({ debug: true, autoIndex: false }); // set multiple options at once
- *
- * Currently supported options are:
- * - `allowDiskUse`: Set to `true` to set `allowDiskUse` to true to all aggregation operations by default.
- * - `applyPluginsToChildSchemas`: `true` by default. Set to false to skip applying global plugins to child schemas
- * - `applyPluginsToDiscriminators`: `false` by default. Set to true to apply global plugins to discriminator schemas. This typically isn't necessary because plugins are applied to the base schema and discriminators copy all middleware, methods, statics, and properties from the base schema.
- * - `autoCreate`: Set to `true` to make Mongoose call [`Model.createCollection()`](/docs/api/model.html#model_Model-createCollection) automatically when you create a model with `mongoose.model()` or `conn.model()`. This is useful for testing transactions, change streams, and other features that require the collection to exist.
- * - `autoIndex`: `true` by default. Set to false to disable automatic index creation for all models associated with this Mongoose instance.
- * - `bufferCommands`: enable/disable mongoose's buffering mechanism for all connections and models
- * - `bufferTimeoutMS`: If bufferCommands is on, this option sets the maximum amount of time Mongoose buffering will wait before throwing an error. If not specified, Mongoose will use 10000 (10 seconds).
- * - `cloneSchemas`: `false` by default. Set to `true` to `clone()` all schemas before compiling into a model.
- * - `debug`: If `true`, prints the operations mongoose sends to MongoDB to the console. If a writable stream is passed, it will log to that stream, without colorization. If a callback function is passed, it will receive the collection name, the method name, then all arguments passed to the method. For example, if you wanted to replicate the default logging, you could output from the callback `Mongoose: ${collectionName}.${methodName}(${methodArgs.join(', ')})`.
- * - `id`: If `true`, adds a `id` virtual to all schemas unless overwritten on a per-schema basis.
- * - `timestamps.createdAt.immutable`: `true` by default. If `false`, it will change the `createdAt` field to be [`immutable: false`](https://mongoosejs.com/docs/api/schematype.html#schematype_SchemaType-immutable) which means you can update the `createdAt`
- * - `maxTimeMS`: If set, attaches [maxTimeMS](https://www.mongodb.com/docs/manual/reference/operator/meta/maxTimeMS/) to every query
- * - `objectIdGetter`: `true` by default. Mongoose adds a getter to MongoDB ObjectId's called `_id` that returns `this` for convenience with populate. Set this to false to remove the getter.
- * - `overwriteModels`: Set to `true` to default to overwriting models with the same name when calling `mongoose.model()`, as opposed to throwing an `OverwriteModelError`.
- * - `returnOriginal`: If `false`, changes the default `returnOriginal` option to `findOneAndUpdate()`, `findByIdAndUpdate`, and `findOneAndReplace()` to false. This is equivalent to setting the `new` option to `true` for `findOneAndX()` calls by default. Read our [`findOneAndUpdate()` tutorial](/docs/tutorials/findoneandupdate.html) for more information.
- * - `runValidators`: `false` by default. Set to true to enable [update validators](/docs/validation.html#update-validators) for all validators by default.
- * - `sanitizeFilter`: `false` by default. Set to true to enable the [sanitization of the query filters](/docs/api/mongoose.html#mongoose_Mongoose-sanitizeFilter) against query selector injection attacks by wrapping any nested objects that have a property whose name starts with `$` in a `$eq`.
- * - `selectPopulatedPaths`: `true` by default. Set to false to opt out of Mongoose adding all fields that you `populate()` to your `select()`. The schema-level option `selectPopulatedPaths` overwrites this one.
- * - `strict`: `true` by default, may be `false`, `true`, or `'throw'`. Sets the default strict mode for schemas.
- * - `strictQuery`: same value as 'strict' by default (`true`), may be `false`, `true`, or `'throw'`. Sets the default [strictQuery](/docs/guide.html#strictQuery) mode for schemas. The default value will be switched back to `false` in Mongoose 7, use `mongoose.set('strictQuery', false);` if you want to prepare for the change.
- * - `toJSON`: `{ transform: true, flattenDecimals: true }` by default. Overwrites default objects to [`toJSON()`](/docs/api/document.html#document_Document-toJSON), for determining how Mongoose documents get serialized by `JSON.stringify()`
- * - `toObject`: `{ transform: true, flattenDecimals: true }` by default. Overwrites default objects to [`toObject()`](/docs/api/document.html#document_Document-toObject)
- *
- * @param {String|Object} key The name of the option or a object of multiple key-value pairs
- * @param {String|Function|Boolean} value The value of the option, unused if "key" is a object
- * @returns {Mongoose} The used Mongoose instnace
- * @api public
- */
-
-Mongoose.prototype.set = function(key, value) {
- const _mongoose = this instanceof Mongoose ? this : mongoose;
-
- if (arguments.length === 1 && typeof key !== 'object') {
- if (VALID_OPTIONS.indexOf(key) === -1) {
- const error = new SetOptionError();
- error.addError(key, new SetOptionError.SetOptionInnerError(key));
- throw error;
- }
-
- return _mongoose.options[key];
- }
-
- let options = {};
-
- if (arguments.length === 2) {
- options = { [key]: value };
- }
-
- if (arguments.length === 1 && typeof key === 'object') {
- options = key;
- }
-
- // array for errors to collect all errors for all key-value pairs, like ".validate"
- let error = undefined;
-
- for (const [optionKey, optionValue] of Object.entries(options)) {
- if (VALID_OPTIONS.indexOf(optionKey) === -1) {
- if (!error) {
- error = new SetOptionError();
- }
- error.addError(optionKey, new SetOptionError.SetOptionInnerError(optionKey));
- continue;
- }
-
- _mongoose.options[optionKey] = optionValue;
-
- if (optionKey === 'objectIdGetter') {
- if (optionValue) {
- Object.defineProperty(mongoose.Types.ObjectId.prototype, '_id', {
- enumerable: false,
- configurable: true,
- get: function() {
- return this;
- }
- });
- } else {
- delete mongoose.Types.ObjectId.prototype._id;
- }
- }
- }
-
- if (error) {
- throw error;
- }
-
- return _mongoose;
-};
-
-/**
- * Gets mongoose options
- *
- * #### Example:
- *
- * mongoose.get('test') // returns the 'test' value
- *
- * @param {String} key
- * @method get
- * @api public
- */
-
-Mongoose.prototype.get = Mongoose.prototype.set;
-
-/**
- * Creates a Connection instance.
- *
- * Each `connection` instance maps to a single database. This method is helpful when managing multiple db connections.
- *
- *
- * _Options passed take precedence over options included in connection strings._
- *
- * #### Example:
- *
- * // with mongodb:// URI
- * db = mongoose.createConnection('mongodb://user:pass@127.0.0.1:port/database');
- *
- * // and options
- * const opts = { db: { native_parser: true }}
- * db = mongoose.createConnection('mongodb://user:pass@127.0.0.1:port/database', opts);
- *
- * // replica sets
- * db = mongoose.createConnection('mongodb://user:pass@127.0.0.1:port,anotherhost:port,yetanother:port/database');
- *
- * // and options
- * const opts = { replset: { strategy: 'ping', rs_name: 'testSet' }}
- * db = mongoose.createConnection('mongodb://user:pass@127.0.0.1:port,anotherhost:port,yetanother:port/database', opts);
- *
- * // initialize now, connect later
- * db = mongoose.createConnection();
- * db.openUri('127.0.0.1', 'database', port, [opts]);
- *
- * @param {String} uri mongodb URI to connect to
- * @param {Object} [options] passed down to the [MongoDB driver's `connect()` function](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/MongoClientOptions.html), except for 4 mongoose-specific options explained below.
- * @param {Boolean} [options.bufferCommands=true] Mongoose specific option. Set to false to [disable buffering](https://mongoosejs.com/docs/faq.html#callback_never_executes) on all models associated with this connection.
- * @param {String} [options.dbName] The name of the database you want to use. If not provided, Mongoose uses the database name from connection string.
- * @param {String} [options.user] username for authentication, equivalent to `options.auth.user`. Maintained for backwards compatibility.
- * @param {String} [options.pass] password for authentication, equivalent to `options.auth.password`. Maintained for backwards compatibility.
- * @param {Boolean} [options.autoIndex=true] Mongoose-specific option. Set to false to disable automatic index creation for all models associated with this connection.
- * @param {Class} [options.promiseLibrary] Sets the [underlying driver's promise library](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/MongoClientOptions.html#promiseLibrary).
- * @param {Number} [options.maxPoolSize=5] The maximum number of sockets the MongoDB driver will keep open for this connection. Keep in mind that MongoDB only allows one operation per socket at a time, so you may want to increase this if you find you have a few slow queries that are blocking faster queries from proceeding. See [Slow Trains in MongoDB and Node.js](https://thecodebarbarian.com/slow-trains-in-mongodb-and-nodejs).
- * @param {Number} [options.minPoolSize=1] The minimum number of sockets the MongoDB driver will keep open for this connection. Keep in mind that MongoDB only allows one operation per socket at a time, so you may want to increase this if you find you have a few slow queries that are blocking faster queries from proceeding. See [Slow Trains in MongoDB and Node.js](https://thecodebarbarian.com/slow-trains-in-mongodb-and-nodejs).
- * @param {Number} [options.connectTimeoutMS=30000] How long the MongoDB driver will wait before killing a socket due to inactivity _during initial connection_. Defaults to 30000. This option is passed transparently to [Node.js' `socket#setTimeout()` function](https://nodejs.org/api/net.html#net_socket_settimeout_timeout_callback).
- * @param {Number} [options.socketTimeoutMS=30000] How long the MongoDB driver will wait before killing a socket due to inactivity _after initial connection_. A socket may be inactive because of either no activity or a long-running operation. This is set to `30000` by default, you should set this to 2-3x your longest running operation if you expect some of your database operations to run longer than 20 seconds. This option is passed to [Node.js `socket#setTimeout()` function](https://nodejs.org/api/net.html#net_socket_settimeout_timeout_callback) after the MongoDB driver successfully completes.
- * @param {Number} [options.family=0] Passed transparently to [Node.js' `dns.lookup()`](https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback) function. May be either `0`, `4`, or `6`. `4` means use IPv4 only, `6` means use IPv6 only, `0` means try both.
- * @return {Connection} the created Connection object. Connections are not thenable, so you can't do `await mongoose.createConnection()`. To await use `mongoose.createConnection(uri).asPromise()` instead.
- * @api public
- */
-
-Mongoose.prototype.createConnection = function(uri, options, callback) {
- const _mongoose = this instanceof Mongoose ? this : mongoose;
-
- const Connection = _mongoose.__driver.getConnection();
- const conn = new Connection(_mongoose);
- if (typeof options === 'function') {
- callback = options;
- options = null;
- }
- _mongoose.connections.push(conn);
- _mongoose.nextConnectionId++;
- _mongoose.events.emit('createConnection', conn);
-
- if (arguments.length > 0) {
- conn.openUri(uri, options, callback);
- }
-
- return conn;
-};
-
-/**
- * Opens the default mongoose connection.
- *
- * #### Example:
- *
- * mongoose.connect('mongodb://user:pass@127.0.0.1:port/database');
- *
- * // replica sets
- * const uri = 'mongodb://user:pass@127.0.0.1:port,anotherhost:port,yetanother:port/mydatabase';
- * mongoose.connect(uri);
- *
- * // with options
- * mongoose.connect(uri, options);
- *
- * // optional callback that gets fired when initial connection completed
- * const uri = 'mongodb://nonexistent.domain:27000';
- * mongoose.connect(uri, function(error) {
- * // if error is truthy, the initial connection failed.
- * })
- *
- * @param {String} uri mongodb URI to connect to
- * @param {Object} [options] passed down to the [MongoDB driver's `connect()` function](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/MongoClientOptions.html), except for 4 mongoose-specific options explained below.
- * @param {Boolean} [options.bufferCommands=true] Mongoose specific option. Set to false to [disable buffering](https://mongoosejs.com/docs/faq.html#callback_never_executes) on all models associated with this connection.
- * @param {Number} [options.bufferTimeoutMS=10000] Mongoose specific option. If `bufferCommands` is true, Mongoose will throw an error after `bufferTimeoutMS` if the operation is still buffered.
- * @param {String} [options.dbName] The name of the database we want to use. If not provided, use database name from connection string.
- * @param {String} [options.user] username for authentication, equivalent to `options.auth.user`. Maintained for backwards compatibility.
- * @param {String} [options.pass] password for authentication, equivalent to `options.auth.password`. Maintained for backwards compatibility.
- * @param {Number} [options.maxPoolSize=100] The maximum number of sockets the MongoDB driver will keep open for this connection. Keep in mind that MongoDB only allows one operation per socket at a time, so you may want to increase this if you find you have a few slow queries that are blocking faster queries from proceeding. See [Slow Trains in MongoDB and Node.js](https://thecodebarbarian.com/slow-trains-in-mongodb-and-nodejs).
- * @param {Number} [options.minPoolSize=0] The minimum number of sockets the MongoDB driver will keep open for this connection.
- * @param {Number} [options.serverSelectionTimeoutMS] If `useUnifiedTopology = true`, the MongoDB driver will try to find a server to send any given operation to, and keep retrying for `serverSelectionTimeoutMS` milliseconds before erroring out. If not set, the MongoDB driver defaults to using `30000` (30 seconds).
- * @param {Number} [options.heartbeatFrequencyMS] If `useUnifiedTopology = true`, the MongoDB driver sends a heartbeat every `heartbeatFrequencyMS` to check on the status of the connection. A heartbeat is subject to `serverSelectionTimeoutMS`, so the MongoDB driver will retry failed heartbeats for up to 30 seconds by default. Mongoose only emits a `'disconnected'` event after a heartbeat has failed, so you may want to decrease this setting to reduce the time between when your server goes down and when Mongoose emits `'disconnected'`. We recommend you do **not** set this setting below 1000, too many heartbeats can lead to performance degradation.
- * @param {Boolean} [options.autoIndex=true] Mongoose-specific option. Set to false to disable automatic index creation for all models associated with this connection.
- * @param {Class} [options.promiseLibrary] Sets the [underlying driver's promise library](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/MongoClientOptions.html#promiseLibrary).
- * @param {Number} [options.connectTimeoutMS=30000] How long the MongoDB driver will wait before killing a socket due to inactivity _during initial connection_. Defaults to 30000. This option is passed transparently to [Node.js' `socket#setTimeout()` function](https://nodejs.org/api/net.html#net_socket_settimeout_timeout_callback).
- * @param {Number} [options.socketTimeoutMS=30000] How long the MongoDB driver will wait before killing a socket due to inactivity _after initial connection_. A socket may be inactive because of either no activity or a long-running operation. This is set to `30000` by default, you should set this to 2-3x your longest running operation if you expect some of your database operations to run longer than 20 seconds. This option is passed to [Node.js `socket#setTimeout()` function](https://nodejs.org/api/net.html#net_socket_settimeout_timeout_callback) after the MongoDB driver successfully completes.
- * @param {Number} [options.family=0] Passed transparently to [Node.js' `dns.lookup()`](https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback) function. May be either `0`, `4`, or `6`. `4` means use IPv4 only, `6` means use IPv6 only, `0` means try both.
- * @param {Boolean} [options.autoCreate=false] Set to `true` to make Mongoose automatically call `createCollection()` on every model created on this connection.
- * @param {Function} [callback]
- * @see Mongoose#createConnection /docs/api/mongoose.html#mongoose_Mongoose-createConnection
- * @api public
- * @return {Promise} resolves to `this` if connection succeeded
- */
-
-Mongoose.prototype.connect = function(uri, options, callback) {
- const _mongoose = this instanceof Mongoose ? this : mongoose;
- const conn = _mongoose.connection;
-
- if (_mongoose.options.strictQuery === undefined) {
- printStrictQueryWarning();
- }
-
- return _mongoose._promiseOrCallback(callback, cb => {
- conn.openUri(uri, options, err => {
- if (err != null) {
- return cb(err);
- }
- return cb(null, _mongoose);
- });
- });
-};
-
-/**
- * Runs `.close()` on all connections in parallel.
- *
- * @param {Function} [callback] called after all connection close, or when first error occurred.
- * @return {Promise} resolves when all connections are closed, or rejects with the first error that occurred.
- * @api public
- */
-
-Mongoose.prototype.disconnect = function(callback) {
- const _mongoose = this instanceof Mongoose ? this : mongoose;
-
- return _mongoose._promiseOrCallback(callback, cb => {
- let remaining = _mongoose.connections.length;
- if (remaining <= 0) {
- return cb(null);
- }
- _mongoose.connections.forEach(conn => {
- conn.close(function(error) {
- if (error) {
- return cb(error);
- }
- if (!--remaining) {
- cb(null);
- }
- });
- });
- });
-};
-
-/**
- * _Requires MongoDB >= 3.6.0._ Starts a [MongoDB session](https://www.mongodb.com/docs/manual/release-notes/3.6/#client-sessions)
- * for benefits like causal consistency, [retryable writes](https://www.mongodb.com/docs/manual/core/retryable-writes/),
- * and [transactions](https://thecodebarbarian.com/a-node-js-perspective-on-mongodb-4-transactions.html).
- *
- * Calling `mongoose.startSession()` is equivalent to calling `mongoose.connection.startSession()`.
- * Sessions are scoped to a connection, so calling `mongoose.startSession()`
- * starts a session on the [default mongoose connection](/docs/api/mongoose.html#mongoose_Mongoose-connection).
- *
- * @param {Object} [options] see the [mongodb driver options](https://mongodb.github.io/node-mongodb-native/4.9/classes/MongoClient.html#startSession)
- * @param {Boolean} [options.causalConsistency=true] set to false to disable causal consistency
- * @param {Function} [callback]
- * @return {Promise} promise that resolves to a MongoDB driver `ClientSession`
- * @api public
- */
-
-Mongoose.prototype.startSession = function() {
- const _mongoose = this instanceof Mongoose ? this : mongoose;
-
- return _mongoose.connection.startSession.apply(_mongoose.connection, arguments);
-};
-
-/**
- * Getter/setter around function for pluralizing collection names.
- *
- * @param {Function|null} [fn] overwrites the function used to pluralize collection names
- * @return {Function|null} the current function used to pluralize collection names, defaults to the legacy function from `mongoose-legacy-pluralize`.
- * @api public
- */
-
-Mongoose.prototype.pluralize = function(fn) {
- const _mongoose = this instanceof Mongoose ? this : mongoose;
-
- if (arguments.length > 0) {
- _mongoose._pluralize = fn;
- }
- return _mongoose._pluralize;
-};
-
-/**
- * Defines a model or retrieves it.
- *
- * Models defined on the `mongoose` instance are available to all connection
- * created by the same `mongoose` instance.
- *
- * If you call `mongoose.model()` with twice the same name but a different schema,
- * you will get an `OverwriteModelError`. If you call `mongoose.model()` with
- * the same name and same schema, you'll get the same schema back.
- *
- * #### Example:
- *
- * const mongoose = require('mongoose');
- *
- * // define an Actor model with this mongoose instance
- * const schema = new Schema({ name: String });
- * mongoose.model('Actor', schema);
- *
- * // create a new connection
- * const conn = mongoose.createConnection(..);
- *
- * // create Actor model
- * const Actor = conn.model('Actor', schema);
- * conn.model('Actor') === Actor; // true
- * conn.model('Actor', schema) === Actor; // true, same schema
- * conn.model('Actor', schema, 'actors') === Actor; // true, same schema and collection name
- *
- * // This throws an `OverwriteModelError` because the schema is different.
- * conn.model('Actor', new Schema({ name: String }));
- *
- * _When no `collection` argument is passed, Mongoose uses the model name. If you don't like this behavior, either pass a collection name, use `mongoose.pluralize()`, or set your schemas collection name option._
- *
- * #### Example:
- *
- * const schema = new Schema({ name: String }, { collection: 'actor' });
- *
- * // or
- *
- * schema.set('collection', 'actor');
- *
- * // or
- *
- * const collectionName = 'actor'
- * const M = mongoose.model('Actor', schema, collectionName)
- *
- * @param {String|Function} name model name or class extending Model
- * @param {Schema} [schema] the schema to use.
- * @param {String} [collection] name (optional, inferred from model name)
- * @return {Model} The model associated with `name`. Mongoose will create the model if it doesn't already exist.
- * @api public
- */
-
-Mongoose.prototype.model = function(name, schema, collection, options) {
- const _mongoose = this instanceof Mongoose ? this : mongoose;
-
- if (typeof schema === 'string') {
- collection = schema;
- schema = false;
- }
-
- if (arguments.length === 1) {
- const model = _mongoose.models[name];
- if (!model) {
- throw new MongooseError.MissingSchemaError(name);
- }
- return model;
- }
-
- if (utils.isObject(schema) && !(schema instanceof Schema)) {
- schema = new Schema(schema);
- }
- if (schema && !(schema instanceof Schema)) {
- throw new Error('The 2nd parameter to `mongoose.model()` should be a ' +
- 'schema or a POJO');
- }
-
- // handle internal options from connection.model()
- options = options || {};
-
- const originalSchema = schema;
- if (schema) {
- if (_mongoose.get('cloneSchemas')) {
- schema = schema.clone();
- }
- _mongoose._applyPlugins(schema);
- }
-
- // connection.model() may be passing a different schema for
- // an existing model name. in this case don't read from cache.
- const overwriteModels = _mongoose.options.hasOwnProperty('overwriteModels') ?
- _mongoose.options.overwriteModels :
- options.overwriteModels;
- if (_mongoose.models.hasOwnProperty(name) && options.cache !== false && overwriteModels !== true) {
- if (originalSchema &&
- originalSchema.instanceOfSchema &&
- originalSchema !== _mongoose.models[name].schema) {
- throw new _mongoose.Error.OverwriteModelError(name);
- }
- if (collection && collection !== _mongoose.models[name].collection.name) {
- // subclass current model with alternate collection
- const model = _mongoose.models[name];
- schema = model.prototype.schema;
- const sub = model.__subclass(_mongoose.connection, schema, collection);
- // do not cache the sub model
- return sub;
- }
- return _mongoose.models[name];
- }
- if (schema == null) {
- throw new _mongoose.Error.MissingSchemaError(name);
- }
-
- const model = _mongoose._model(name, schema, collection, options);
- _mongoose.connection.models[name] = model;
- _mongoose.models[name] = model;
-
- return model;
-};
-
-/*!
- * ignore
- */
-
-Mongoose.prototype._model = function(name, schema, collection, options) {
- const _mongoose = this instanceof Mongoose ? this : mongoose;
-
- let model;
- if (typeof name === 'function') {
- model = name;
- name = model.name;
- if (!(model.prototype instanceof Model)) {
- throw new _mongoose.Error('The provided class ' + name + ' must extend Model');
- }
- }
-
- if (schema) {
- if (_mongoose.get('cloneSchemas')) {
- schema = schema.clone();
- }
- _mongoose._applyPlugins(schema);
- }
-
- // Apply relevant "global" options to the schema
- if (schema == null || !('pluralization' in schema.options)) {
- schema.options.pluralization = _mongoose.options.pluralization;
- }
-
- if (!collection) {
- collection = schema.get('collection') ||
- utils.toCollectionName(name, _mongoose.pluralize());
- }
-
- const connection = options.connection || _mongoose.connection;
- model = _mongoose.Model.compile(model || name, schema, collection, connection, _mongoose);
- // Errors handled internally, so safe to ignore error
- model.init(function $modelInitNoop() {});
-
- connection.emit('model', model);
-
- if (schema._applyDiscriminators != null) {
- for (const disc of Object.keys(schema._applyDiscriminators)) {
- model.discriminator(disc, schema._applyDiscriminators[disc]);
- }
- }
-
- return model;
-};
-
-/**
- * Removes the model named `name` from the default connection, if it exists.
- * You can use this function to clean up any models you created in your tests to
- * prevent OverwriteModelErrors.
- *
- * Equivalent to `mongoose.connection.deleteModel(name)`.
- *
- * #### Example:
- *
- * mongoose.model('User', new Schema({ name: String }));
- * console.log(mongoose.model('User')); // Model object
- * mongoose.deleteModel('User');
- * console.log(mongoose.model('User')); // undefined
- *
- * // Usually useful in a Mocha `afterEach()` hook
- * afterEach(function() {
- * mongoose.deleteModel(/.+/); // Delete every model
- * });
- *
- * @api public
- * @param {String|RegExp} name if string, the name of the model to remove. If regexp, removes all models whose name matches the regexp.
- * @return {Mongoose} this
- */
-
-Mongoose.prototype.deleteModel = function(name) {
- const _mongoose = this instanceof Mongoose ? this : mongoose;
-
- _mongoose.connection.deleteModel(name);
- delete _mongoose.models[name];
- return _mongoose;
-};
-
-/**
- * Returns an array of model names created on this instance of Mongoose.
- *
- * #### Note:
- *
- * _Does not include names of models created using `connection.model()`._
- *
- * @api public
- * @return {Array}
- */
-
-Mongoose.prototype.modelNames = function() {
- const _mongoose = this instanceof Mongoose ? this : mongoose;
-
- const names = Object.keys(_mongoose.models);
- return names;
-};
-
-/**
- * Applies global plugins to `schema`.
- *
- * @param {Schema} schema
- * @api private
- */
-
-Mongoose.prototype._applyPlugins = function(schema, options) {
- const _mongoose = this instanceof Mongoose ? this : mongoose;
-
- options = options || {};
- options.applyPluginsToDiscriminators = _mongoose.options && _mongoose.options.applyPluginsToDiscriminators || false;
- options.applyPluginsToChildSchemas = typeof (_mongoose.options && _mongoose.options.applyPluginsToDiscriminators) === 'boolean' ? _mongoose.options.applyPluginsToDiscriminators : true;
- applyPlugins(schema, _mongoose.plugins, options, '$globalPluginsApplied');
-};
-
-/**
- * Declares a global plugin executed on all Schemas.
- *
- * Equivalent to calling `.plugin(fn)` on each Schema you create.
- *
- * @param {Function} fn plugin callback
- * @param {Object} [opts] optional options
- * @return {Mongoose} this
- * @see plugins /docs/plugins.html
- * @api public
- */
-
-Mongoose.prototype.plugin = function(fn, opts) {
- const _mongoose = this instanceof Mongoose ? this : mongoose;
-
- _mongoose.plugins.push([fn, opts]);
- return _mongoose;
-};
-
-/**
- * The Mongoose module's default connection. Equivalent to `mongoose.connections[0]`, see [`connections`](#mongoose_Mongoose-connections).
- *
- * #### Example:
- *
- * const mongoose = require('mongoose');
- * mongoose.connect(...);
- * mongoose.connection.on('error', cb);
- *
- * This is the connection used by default for every model created using [mongoose.model](#index_Mongoose-model).
- *
- * To create a new connection, use [`createConnection()`](#mongoose_Mongoose-createConnection).
- *
- * @memberOf Mongoose
- * @instance
- * @property {Connection} connection
- * @api public
- */
-
-Mongoose.prototype.__defineGetter__('connection', function() {
- return this.connections[0];
-});
-
-Mongoose.prototype.__defineSetter__('connection', function(v) {
- if (v instanceof this.__driver.getConnection()) {
- this.connections[0] = v;
- this.models = v.models;
- }
-});
-
-/**
- * An array containing all [connections](connection.html) associated with this
- * Mongoose instance. By default, there is 1 connection. Calling
- * [`createConnection()`](#mongoose_Mongoose-createConnection) adds a connection
- * to this array.
- *
- * #### Example:
- *
- * const mongoose = require('mongoose');
- * mongoose.connections.length; // 1, just the default connection
- * mongoose.connections[0] === mongoose.connection; // true
- *
- * mongoose.createConnection('mongodb://127.0.0.1:27017/test');
- * mongoose.connections.length; // 2
- *
- * @memberOf Mongoose
- * @instance
- * @property {Array} connections
- * @api public
- */
-
-Mongoose.prototype.connections;
-
-/**
- * An integer containing the value of the next connection id. Calling
- * [`createConnection()`](#mongoose_Mongoose-createConnection) increments
- * this value.
- *
- * #### Example:
- *
- * const mongoose = require('mongoose');
- * mongoose.createConnection(); // id `0`, `nextConnectionId` becomes `1`
- * mongoose.createConnection(); // id `1`, `nextConnectionId` becomes `2`
- * mongoose.connections[0].destroy() // Removes connection with id `0`
- * mongoose.createConnection(); // id `2`, `nextConnectionId` becomes `3`
- *
- * @memberOf Mongoose
- * @instance
- * @property {Number} nextConnectionId
- * @api private
- */
-
-Mongoose.prototype.nextConnectionId;
-
-/**
- * The Mongoose Aggregate constructor
- *
- * @method Aggregate
- * @api public
- */
-
-Mongoose.prototype.Aggregate = Aggregate;
-
-/**
- * The Mongoose Collection constructor
- *
- * @memberOf Mongoose
- * @instance
- * @method Collection
- * @api public
- */
-
-Object.defineProperty(Mongoose.prototype, 'Collection', {
- get: function() {
- return this.__driver.Collection;
- },
- set: function(Collection) {
- this.__driver.Collection = Collection;
- }
-});
-
-/**
- * The Mongoose [Connection](#connection_Connection) constructor
- *
- * @memberOf Mongoose
- * @instance
- * @method Connection
- * @api public
- */
-
-Object.defineProperty(Mongoose.prototype, 'Connection', {
- get: function() {
- return this.__driver.getConnection();
- },
- set: function(Connection) {
- if (Connection === this.__driver.getConnection()) {
- return;
- }
-
- this.__driver.getConnection = () => Connection;
- }
-});
-
-/**
- * The Mongoose version
- *
- * #### Example:
- *
- * console.log(mongoose.version); // '5.x.x'
- *
- * @property version
- * @api public
- */
-
-Mongoose.prototype.version = pkg.version;
-
-/**
- * The Mongoose constructor
- *
- * The exports of the mongoose module is an instance of this class.
- *
- * #### Example:
- *
- * const mongoose = require('mongoose');
- * const mongoose2 = new mongoose.Mongoose();
- *
- * @method Mongoose
- * @api public
- */
-
-Mongoose.prototype.Mongoose = Mongoose;
-
-/**
- * The Mongoose [Schema](#schema_Schema) constructor
- *
- * #### Example:
- *
- * const mongoose = require('mongoose');
- * const Schema = mongoose.Schema;
- * const CatSchema = new Schema(..);
- *
- * @method Schema
- * @api public
- */
-
-Mongoose.prototype.Schema = Schema;
-
-/**
- * The Mongoose [SchemaType](#schematype_SchemaType) constructor
- *
- * @method SchemaType
- * @api public
- */
-
-Mongoose.prototype.SchemaType = SchemaType;
-
-/**
- * The various Mongoose SchemaTypes.
- *
- * #### Note:
- *
- * _Alias of mongoose.Schema.Types for backwards compatibility._
- *
- * @property SchemaTypes
- * @see Schema.SchemaTypes /docs/schematypes.html
- * @api public
- */
-
-Mongoose.prototype.SchemaTypes = Schema.Types;
-
-/**
- * The Mongoose [VirtualType](#virtualtype_VirtualType) constructor
- *
- * @method VirtualType
- * @api public
- */
-
-Mongoose.prototype.VirtualType = VirtualType;
-
-/**
- * The various Mongoose Types.
- *
- * #### Example:
- *
- * const mongoose = require('mongoose');
- * const array = mongoose.Types.Array;
- *
- * #### Types:
- *
- * - [Array](/docs/schematypes.html#arrays)
- * - [Buffer](/docs/schematypes.html#buffers)
- * - [Embedded](/docs/schematypes.html#schemas)
- * - [DocumentArray](/docs/api/documentarraypath.html)
- * - [Decimal128](/docs/api/mongoose.html#mongoose_Mongoose-Decimal128)
- * - [ObjectId](/docs/schematypes.html#objectids)
- * - [Map](/docs/schematypes.html#maps)
- * - [Subdocument](/docs/schematypes.html#schemas)
- *
- * Using this exposed access to the `ObjectId` type, we can construct ids on demand.
- *
- * const ObjectId = mongoose.Types.ObjectId;
- * const id1 = new ObjectId;
- *
- * @property Types
- * @api public
- */
-
-Mongoose.prototype.Types = Types;
-
-/**
- * The Mongoose [Query](#query_Query) constructor.
- *
- * @method Query
- * @api public
- */
-
-Mongoose.prototype.Query = Query;
-
-/**
- * The Mongoose [Promise](#promise_Promise) constructor.
- *
- * @memberOf Mongoose
- * @instance
- * @property Promise
- * @api public
- */
-
-Object.defineProperty(Mongoose.prototype, 'Promise', {
- get: function() {
- return PromiseProvider.get();
- },
- set: function(lib) {
- PromiseProvider.set(lib);
- }
-});
-
-/**
- * Storage layer for mongoose promises
- *
- * @method PromiseProvider
- * @api public
- */
-
-Mongoose.prototype.PromiseProvider = PromiseProvider;
-
-/**
- * The Mongoose [Model](#model_Model) constructor.
- *
- * @method Model
- * @api public
- */
-
-Mongoose.prototype.Model = Model;
-
-/**
- * The Mongoose [Document](/docs/api/document.html#Document) constructor.
- *
- * @method Document
- * @api public
- */
-
-Mongoose.prototype.Document = Document;
-
-/**
- * The Mongoose DocumentProvider constructor. Mongoose users should not have to
- * use this directly
- *
- * @method DocumentProvider
- * @api public
- */
-
-Mongoose.prototype.DocumentProvider = require('./document_provider');
-
-/**
- * The Mongoose ObjectId [SchemaType](/docs/schematypes.html). Used for
- * declaring paths in your schema that should be
- * [MongoDB ObjectIds](https://www.mongodb.com/docs/manual/reference/method/ObjectId/).
- * Do not use this to create a new ObjectId instance, use `mongoose.Types.ObjectId`
- * instead.
- *
- * #### Example:
- *
- * const childSchema = new Schema({ parentId: mongoose.ObjectId });
- *
- * @property ObjectId
- * @api public
- */
-
-Mongoose.prototype.ObjectId = SchemaTypes.ObjectId;
-
-/**
- * Returns true if Mongoose can cast the given value to an ObjectId, or
- * false otherwise.
- *
- * #### Example:
- *
- * mongoose.isValidObjectId(new mongoose.Types.ObjectId()); // true
- * mongoose.isValidObjectId('0123456789ab'); // true
- * mongoose.isValidObjectId(6); // true
- * mongoose.isValidObjectId(new User({ name: 'test' })); // true
- *
- * mongoose.isValidObjectId({ test: 42 }); // false
- *
- * @method isValidObjectId
- * @param {Any} v
- * @returns {boolean} true if `v` is something Mongoose can coerce to an ObjectId
- * @api public
- */
-
-Mongoose.prototype.isValidObjectId = function(v) {
- const _mongoose = this instanceof Mongoose ? this : mongoose;
- return _mongoose.Types.ObjectId.isValid(v);
-};
-
-/**
- * Returns true if the given value is a Mongoose ObjectId (using `instanceof`) or if the
- * given value is a 24 character hex string, which is the most commonly used string representation
- * of an ObjectId.
- *
- * This function is similar to `isValidObjectId()`, but considerably more strict, because
- * `isValidObjectId()` will return `true` for _any_ value that Mongoose can convert to an
- * ObjectId. That includes Mongoose documents, any string of length 12, and any number.
- * `isObjectIdOrHexString()` returns true only for `ObjectId` instances or 24 character hex
- * strings, and will return false for numbers, documents, and strings of length 12.
- *
- * #### Example:
- *
- * mongoose.isObjectIdOrHexString(new mongoose.Types.ObjectId()); // true
- * mongoose.isObjectIdOrHexString('62261a65d66c6be0a63c051f'); // true
- *
- * mongoose.isObjectIdOrHexString('0123456789ab'); // false
- * mongoose.isObjectIdOrHexString(6); // false
- * mongoose.isObjectIdOrHexString(new User({ name: 'test' })); // false
- * mongoose.isObjectIdOrHexString({ test: 42 }); // false
- *
- * @method isObjectIdOrHexString
- * @param {Any} v
- * @returns {boolean} true if `v` is an ObjectId instance _or_ a 24 char hex string
- * @api public
- */
-
-Mongoose.prototype.isObjectIdOrHexString = function(v) {
- return isBsonType(v, 'ObjectID') || (typeof v === 'string' && objectIdHexRegexp.test(v));
-};
-
-/**
- *
- * Syncs all the indexes for the models registered with this connection.
- *
- * @param {Object} options
- * @param {Boolean} options.continueOnError `false` by default. If set to `true`, mongoose will not throw an error if one model syncing failed, and will return an object where the keys are the names of the models, and the values are the results/errors for each model.
- * @return {Promise} Returns a Promise, when the Promise resolves the value is a list of the dropped indexes.
- */
-Mongoose.prototype.syncIndexes = function(options) {
- const _mongoose = this instanceof Mongoose ? this : mongoose;
- return _mongoose.connection.syncIndexes(options);
-};
-
-/**
- * The Mongoose Decimal128 [SchemaType](/docs/schematypes.html). Used for
- * declaring paths in your schema that should be
- * [128-bit decimal floating points](https://thecodebarbarian.com/a-nodejs-perspective-on-mongodb-34-decimal.html).
- * Do not use this to create a new Decimal128 instance, use `mongoose.Types.Decimal128`
- * instead.
- *
- * #### Example:
- *
- * const vehicleSchema = new Schema({ fuelLevel: mongoose.Decimal128 });
- *
- * @property Decimal128
- * @api public
- */
-
-Mongoose.prototype.Decimal128 = SchemaTypes.Decimal128;
-
-/**
- * The Mongoose Mixed [SchemaType](/docs/schematypes.html). Used for
- * declaring paths in your schema that Mongoose's change tracking, casting,
- * and validation should ignore.
- *
- * #### Example:
- *
- * const schema = new Schema({ arbitrary: mongoose.Mixed });
- *
- * @property Mixed
- * @api public
- */
-
-Mongoose.prototype.Mixed = SchemaTypes.Mixed;
-
-/**
- * The Mongoose Date [SchemaType](/docs/schematypes.html).
- *
- * #### Example:
- *
- * const schema = new Schema({ test: Date });
- * schema.path('test') instanceof mongoose.Date; // true
- *
- * @property Date
- * @api public
- */
-
-Mongoose.prototype.Date = SchemaTypes.Date;
-
-/**
- * The Mongoose Number [SchemaType](/docs/schematypes.html). Used for
- * declaring paths in your schema that Mongoose should cast to numbers.
- *
- * #### Example:
- *
- * const schema = new Schema({ num: mongoose.Number });
- * // Equivalent to:
- * const schema = new Schema({ num: 'number' });
- *
- * @property Number
- * @api public
- */
-
-Mongoose.prototype.Number = SchemaTypes.Number;
-
-/**
- * The [MongooseError](#error_MongooseError) constructor.
- *
- * @method Error
- * @api public
- */
-
-Mongoose.prototype.Error = require('./error/index');
-
-/**
- * Mongoose uses this function to get the current time when setting
- * [timestamps](/docs/guide.html#timestamps). You may stub out this function
- * using a tool like [Sinon](https://www.npmjs.com/package/sinon) for testing.
- *
- * @method now
- * @returns Date the current time
- * @api public
- */
-
-Mongoose.prototype.now = function now() { return new Date(); };
-
-/**
- * The Mongoose CastError constructor
- *
- * @method CastError
- * @param {String} type The name of the type
- * @param {Any} value The value that failed to cast
- * @param {String} path The path `a.b.c` in the doc where this cast error occurred
- * @param {Error} [reason] The original error that was thrown
- * @api public
- */
-
-Mongoose.prototype.CastError = require('./error/cast');
-
-/**
- * The constructor used for schematype options
- *
- * @method SchemaTypeOptions
- * @api public
- */
-
-Mongoose.prototype.SchemaTypeOptions = require('./options/SchemaTypeOptions');
-
-/**
- * The [node-mongodb-native](https://github.com/mongodb/node-mongodb-native) driver Mongoose uses.
- *
- * @property mongo
- * @api public
- */
-
-Mongoose.prototype.mongo = require('mongodb');
-
-/**
- * The [mquery](https://github.com/aheckmann/mquery) query builder Mongoose uses.
- *
- * @property mquery
- * @api public
- */
-
-Mongoose.prototype.mquery = require('mquery');
-
-/**
- * Sanitizes query filters against [query selector injection attacks](https://thecodebarbarian.com/2014/09/04/defending-against-query-selector-injection-attacks.html)
- * by wrapping any nested objects that have a property whose name starts with `$` in a `$eq`.
- *
- * ```javascript
- * const obj = { username: 'val', pwd: { $ne: null } };
- * sanitizeFilter(obj);
- * obj; // { username: 'val', pwd: { $eq: { $ne: null } } });
- * ```
- *
- * @method sanitizeFilter
- * @param {Object} filter
- * @returns Object the sanitized object
- * @api public
- */
-
-Mongoose.prototype.sanitizeFilter = sanitizeFilter;
-
-/**
- * Tells `sanitizeFilter()` to skip the given object when filtering out potential [query selector injection attacks](https://thecodebarbarian.com/2014/09/04/defending-against-query-selector-injection-attacks.html).
- * Use this method when you have a known query selector that you want to use.
- *
- * ```javascript
- * const obj = { username: 'val', pwd: trusted({ $type: 'string', $eq: 'my secret' }) };
- * sanitizeFilter(obj);
- *
- * // Note that `sanitizeFilter()` did not add `$eq` around `$type`.
- * obj; // { username: 'val', pwd: { $type: 'string', $eq: 'my secret' } });
- * ```
- *
- * @method trusted
- * @param {Object} obj
- * @returns Object the passed in object
- * @api public
- */
-
-Mongoose.prototype.trusted = trusted;
-
-/*!
- * ignore
- */
-
-Mongoose.prototype._promiseOrCallback = function(callback, fn, ee) {
- return promiseOrCallback(callback, fn, ee, this.Promise);
-};
-
-/**
- * Use this function in `pre()` middleware to skip calling the wrapped function.
- *
- * #### Example:
- *
- * schema.pre('save', function() {
- * // Will skip executing `save()`, but will execute post hooks as if
- * // `save()` had executed with the result `{ matchedCount: 0 }`
- * return mongoose.skipMiddlewareFunction({ matchedCount: 0 });
- * });
- *
- * @method skipMiddlewareFunction
- * @param {any} result
- * @api public
- */
-
-Mongoose.prototype.skipMiddlewareFunction = Kareem.skipWrappedFunction;
-
-/**
- * Use this function in `post()` middleware to replace the result
- *
- * #### Example:
- *
- * schema.post('find', function(res) {
- * // Normally you have to modify `res` in place. But with
- * // `overwriteMiddlewarResult()`, you can make `find()` return a
- * // completely different value.
- * return mongoose.overwriteMiddlewareResult(res.filter(doc => !doc.isDeleted));
- * });
- *
- * @method overwriteMiddlewareResult
- * @param {any} result
- * @api public
- */
-
-Mongoose.prototype.overwriteMiddlewareResult = Kareem.overwriteResult;
-
-/**
- * The exports object is an instance of Mongoose.
- *
- * @api private
- */
-
-const mongoose = module.exports = exports = new Mongoose({
- [defaultMongooseSymbol]: true
-});
+module.exports = mongoose;
diff --git a/lib/internal.js b/lib/internal.js
index c4445c254d6..8de1b278773 100644
--- a/lib/internal.js
+++ b/lib/internal.js
@@ -4,7 +4,7 @@
'use strict';
-const StateMachine = require('./statemachine');
+const StateMachine = require('./stateMachine');
const ActiveRoster = StateMachine.ctor('require', 'modify', 'init', 'default', 'ignore');
module.exports = exports = InternalCache;
diff --git a/lib/model.js b/lib/model.js
index 07bd9ef43b0..8d38d9ee083 100644
--- a/lib/model.js
+++ b/lib/model.js
@@ -5,40 +5,43 @@
*/
const Aggregate = require('./aggregate');
-const ChangeStream = require('./cursor/ChangeStream');
+const ChangeStream = require('./cursor/changeStream');
const Document = require('./document');
const DocumentNotFoundError = require('./error/notFound');
-const DivergentArrayError = require('./error/divergentArray');
const EventEmitter = require('events').EventEmitter;
-const MongooseBuffer = require('./types/buffer');
+const Kareem = require('kareem');
+const { MongoBulkWriteError } = require('mongodb');
+const MongooseBulkWriteError = require('./error/bulkWriteError');
const MongooseError = require('./error/index');
+const ObjectParameterError = require('./error/objectParameter');
const OverwriteModelError = require('./error/overwriteModel');
-const PromiseProvider = require('./promise_provider');
const Query = require('./query');
-const RemoveOptions = require('./options/removeOptions');
const SaveOptions = require('./options/saveOptions');
const Schema = require('./schema');
-const ServerSelectionError = require('./error/serverSelection');
const ValidationError = require('./error/validation');
const VersionError = require('./error/version');
const ParallelSaveError = require('./error/parallelSave');
const applyDefaultsHelper = require('./helpers/document/applyDefaults');
const applyDefaultsToPOJO = require('./helpers/model/applyDefaultsToPOJO');
-const applyQueryMiddleware = require('./helpers/query/applyQueryMiddleware');
+const applyEmbeddedDiscriminators = require('./helpers/discriminator/applyEmbeddedDiscriminators');
const applyHooks = require('./helpers/model/applyHooks');
const applyMethods = require('./helpers/model/applyMethods');
const applyProjection = require('./helpers/projection/applyProjection');
+const applyReadConcern = require('./helpers/schema/applyReadConcern');
const applySchemaCollation = require('./helpers/indexes/applySchemaCollation');
const applyStaticHooks = require('./helpers/model/applyStaticHooks');
const applyStatics = require('./helpers/model/applyStatics');
+const applyTimestampsHelper = require('./helpers/document/applyTimestamps');
const applyWriteConcern = require('./helpers/schema/applyWriteConcern');
+const applyVirtualsHelper = require('./helpers/document/applyVirtuals');
const assignVals = require('./helpers/populate/assignVals');
const castBulkWrite = require('./helpers/model/castBulkWrite');
+const clone = require('./helpers/clone');
const createPopulateQueryFilter = require('./helpers/populate/createPopulateQueryFilter');
+const decorateUpdateWithVersionKey = require('./helpers/update/decorateUpdateWithVersionKey');
const getDefaultBulkwriteResult = require('./helpers/getDefaultBulkwriteResult');
const getSchemaDiscriminatorByValue = require('./helpers/discriminator/getSchemaDiscriminatorByValue');
const discriminator = require('./helpers/model/discriminator');
-const firstKey = require('./helpers/firstKey');
const each = require('./helpers/each');
const get = require('./helpers/get');
const getConstructorName = require('./helpers/getConstructorName');
@@ -48,47 +51,45 @@ const immediate = require('./helpers/immediate');
const internalToObjectOptions = require('./options').internalToObjectOptions;
const isDefaultIdIndex = require('./helpers/indexes/isDefaultIdIndex');
const isIndexEqual = require('./helpers/indexes/isIndexEqual');
+const isTimeseriesIndex = require('./helpers/indexes/isTimeseriesIndex');
const {
getRelatedDBIndexes,
getRelatedSchemaIndexes
} = require('./helpers/indexes/getRelatedIndexes');
-const isPathExcluded = require('./helpers/projection/isPathExcluded');
const decorateDiscriminatorIndexOptions = require('./helpers/indexes/decorateDiscriminatorIndexOptions');
const isPathSelectedInclusive = require('./helpers/projection/isPathSelectedInclusive');
const leanPopulateMap = require('./helpers/populate/leanPopulateMap');
-const modifiedPaths = require('./helpers/update/modifiedPaths');
const parallelLimit = require('./helpers/parallelLimit');
-const parentPaths = require('./helpers/path/parentPaths');
const prepareDiscriminatorPipeline = require('./helpers/aggregate/prepareDiscriminatorPipeline');
const pushNestedArrayPaths = require('./helpers/model/pushNestedArrayPaths');
const removeDeselectedForeignField = require('./helpers/populate/removeDeselectedForeignField');
const setDottedPath = require('./helpers/path/setDottedPath');
-const STATES = require('./connectionstate');
+const STATES = require('./connectionState');
const util = require('util');
const utils = require('./utils');
+const minimize = require('./helpers/minimize');
+const MongooseBulkSaveIncompleteError = require('./error/bulkSaveIncompleteError');
+const ObjectExpectedError = require('./error/objectExpected');
-const VERSION_WHERE = 1;
-const VERSION_INC = 2;
-const VERSION_ALL = VERSION_WHERE | VERSION_INC;
-
-const arrayAtomicsSymbol = require('./helpers/symbols').arrayAtomicsSymbol;
const modelCollectionSymbol = Symbol('mongoose#Model#collection');
const modelDbSymbol = Symbol('mongoose#Model#db');
const modelSymbol = require('./helpers/symbols').modelSymbol;
const subclassedSymbol = Symbol('mongoose#Model#subclassed');
+const { VERSION_INC, VERSION_WHERE, VERSION_ALL } = Document;
+
const saveToObjectOptions = Object.assign({}, internalToObjectOptions, {
bson: true
});
/**
* A Model is a class that's your primary tool for interacting with MongoDB.
- * An instance of a Model is called a [Document](/docs/api/document.html#Document).
+ * An instance of a Model is called a [Document](https://mongoosejs.com/docs/api/document.html#Document).
*
* In Mongoose, the term "Model" refers to subclasses of the `mongoose.Model`
* class. You should not use the `mongoose.Model` class directly. The
- * [`mongoose.model()`](/docs/api/mongoose.html#mongoose_Mongoose-model) and
- * [`connection.model()`](/docs/api/connection.html#connection_Connection-model) functions
+ * [`mongoose.model()`](https://mongoosejs.com/docs/api/mongoose.html#Mongoose.prototype.model()) and
+ * [`connection.model()`](https://mongoosejs.com/docs/api/connection.html#Connection.prototype.model()) functions
* create subclasses of `mongoose.Model` as shown below.
*
* #### Example:
@@ -104,10 +105,10 @@ const saveToObjectOptions = Object.assign({}, internalToObjectOptions, {
* const userFromDb = await UserModel.findOne({ name: 'Foo' });
*
* @param {Object} doc values for initial set
- * @param {Object} [fields] optional object containing the fields that were selected in the query which returned this document. You do **not** need to set this parameter to ensure Mongoose handles your [query projection](/docs/api/query.html#query_Query-select).
+ * @param {Object} [fields] optional object containing the fields that were selected in the query which returned this document. You do **not** need to set this parameter to ensure Mongoose handles your [query projection](https://mongoosejs.com/docs/api/query.html#Query.prototype.select()).
* @param {Boolean} [skipId=false] optional boolean. If true, mongoose doesn't add an `_id` field to the document.
* @inherits Document https://mongoosejs.com/docs/api/document.html
- * @event `error`: If listening to this event, 'error' is emitted when a document was saved without passing a callback and an `error` occurred. If not listening, the event bubbles to the connection used to create this Model.
+ * @event `error`: If listening to this event, 'error' is emitted when a document was saved and an `error` occurred. If not listening, the event bubbles to the connection used to create this Model.
* @event `index`: Emitted after `Model#ensureIndexes` completes. If an error occurred it is passed with the event.
* @event `index-single-start`: Emitted when an individual index starts within `Model#ensureIndexes`. The fields and options being used to build the index are also passed with the event.
* @event `index-single-done`: Emitted when an individual index finishes within `Model#ensureIndexes`. If an error occurred it is passed with the event. The fields, options, and index name are also passed.
@@ -116,10 +117,15 @@ const saveToObjectOptions = Object.assign({}, internalToObjectOptions, {
function Model(doc, fields, skipId) {
if (fields instanceof Schema) {
- throw new TypeError('2nd argument to `Model` must be a POJO or string, ' +
+ throw new TypeError('2nd argument to `Model` constructor must be a POJO or string, ' +
'**not** a schema. Make sure you\'re calling `mongoose.model()`, not ' +
'`mongoose.Model()`.');
}
+ if (typeof doc === 'string') {
+ throw new TypeError('First argument to `Model` constructor must be an object, ' +
+ '**not** a string. Make sure you\'re calling `mongoose.model()`, not ' +
+ '`mongoose.Model()`.');
+ }
Document.call(this, doc, fields, skipId);
}
@@ -146,7 +152,9 @@ Model.prototype.$isMongooseModelPrototype = true;
Model.prototype.db;
/**
- * Collection the model uses.
+ * The collection instance this model uses.
+ * A Mongoose collection is a thin wrapper around a [MongoDB Node.js driver collection]([MongoDB Node.js driver collection](https://mongodb.github.io/node-mongodb-native/Next/classes/Collection.html)).
+ * Using `Model.collection` means you bypass Mongoose middleware, validation, and casting.
*
* This property is read-only. Modifying this property is a no-op.
*
@@ -286,10 +294,13 @@ Model.prototype.$__handleSave = function(options, callback) {
}
const session = this.$session();
- if (!saveOptions.hasOwnProperty('session') && session != null) {
+ const asyncLocalStorage = this[modelDbSymbol].base.transactionAsyncLocalStorage?.getStore();
+ if (session != null) {
saveOptions.session = session;
+ } else if (!options.hasOwnProperty('session') && asyncLocalStorage?.session != null) {
+ // Only set session from asyncLocalStorage if `session` option wasn't originally passed in options
+ saveOptions.session = asyncLocalStorage.session;
}
-
if (this.$isNew) {
// send entire doc
const obj = this.toObject(saveToObjectOptions);
@@ -306,30 +317,38 @@ Model.prototype.$__handleSave = function(options, callback) {
}
this.$__version(true, obj);
- this[modelCollectionSymbol].insertOne(obj, saveOptions, (err, ret) => {
- if (err) {
+ this[modelCollectionSymbol].insertOne(obj, saveOptions).then(
+ ret => callback(null, ret),
+ err => {
_setIsNew(this, true);
callback(err, null);
- return;
}
-
- callback(null, ret);
- });
+ );
this.$__reset();
_setIsNew(this, false);
// Make it possible to retry the insert
this.$__.inserting = true;
-
return;
}
// Make sure we don't treat it as a new object on error,
// since it already exists
this.$__.inserting = false;
-
const delta = this.$__delta();
+
+ if (options.pathsToSave) {
+ for (const key in delta[1]['$set']) {
+ if (options.pathsToSave.includes(key)) {
+ continue;
+ } else if (options.pathsToSave.some(pathToSave => key.slice(0, pathToSave.length) === pathToSave && key.charAt(pathToSave.length) === '.')) {
+ continue;
+ } else {
+ delete delta[1]['$set'][key];
+ }
+ }
+ }
if (delta) {
if (delta instanceof MongooseError) {
callback(delta);
@@ -343,41 +362,76 @@ Model.prototype.$__handleSave = function(options, callback) {
}
_applyCustomWhere(this, where);
- this[modelCollectionSymbol].updateOne(where, delta[1], saveOptions, (err, ret) => {
- if (err) {
+
+ const update = delta[1];
+ if (this.$__schema.options.minimize) {
+ for (const updateOp of Object.values(update)) {
+ if (updateOp == null) {
+ continue;
+ }
+ for (const key of Object.keys(updateOp)) {
+ if (updateOp[key] == null || typeof updateOp[key] !== 'object') {
+ continue;
+ }
+ if (!utils.isPOJO(updateOp[key])) {
+ continue;
+ }
+ minimize(updateOp[key]);
+ if (Object.keys(updateOp[key]).length === 0) {
+ delete updateOp[key];
+ update.$unset = update.$unset || {};
+ update.$unset[key] = 1;
+ }
+ }
+ }
+ }
+
+ this[modelCollectionSymbol].updateOne(where, update, saveOptions).then(
+ ret => {
+ if (ret == null) {
+ ret = { $where: where };
+ } else {
+ ret.$where = where;
+ }
+ callback(null, ret);
+ },
+ err => {
this.$__undoReset();
callback(err);
- return;
}
- ret.$where = where;
- callback(null, ret);
- });
+ );
} else {
+ handleEmptyUpdate.call(this);
+ return;
+ }
+
+ // store the modified paths before the document is reset
+ this.$__.modifiedPaths = this.modifiedPaths();
+ this.$__reset();
+
+ _setIsNew(this, false);
+
+ function handleEmptyUpdate() {
const optionsWithCustomValues = Object.assign({}, options, saveOptions);
const where = this.$__where();
const optimisticConcurrency = this.$__schema.options.optimisticConcurrency;
- if (optimisticConcurrency) {
+ if (optimisticConcurrency && !Array.isArray(optimisticConcurrency)) {
const key = this.$__schema.options.versionKey;
const val = this.$__getValue(key);
if (val != null) {
where[key] = val;
}
}
- this.constructor.exists(where, optionsWithCustomValues)
+
+ applyReadConcern(this.$__schema, optionsWithCustomValues);
+ this.constructor.collection.findOne(where, optionsWithCustomValues)
.then(documentExists => {
const matchedCount = !documentExists ? 0 : 1;
callback(null, { $where: where, matchedCount });
})
.catch(callback);
- return;
}
-
- // store the modified paths before the document is reset
- this.$__.modifiedPaths = this.modifiedPaths();
- this.$__reset();
-
- _setIsNew(this, false);
};
/*!
@@ -387,6 +441,7 @@ Model.prototype.$__handleSave = function(options, callback) {
Model.prototype.$__save = function(options, callback) {
this.$__handleSave(options, (error, result) => {
if (error) {
+ error = this.$__schema._transformDuplicateKeyError(error);
const hooks = this.$__schema.s.hooks;
return hooks.execPost('save:error', this, [this], { error: error }, (error) => {
callback(error, this);
@@ -463,8 +518,8 @@ function generateVersionError(doc, modifiedPaths) {
}
/**
- * Saves this document by inserting a new document into the database if [document.isNew](/docs/api/document.html#document_Document-isNew) is `true`,
- * or sends an [updateOne](/docs/api/document.html#document_Document-updateOne) operation with just the modified paths if `isNew` is `false`.
+ * Saves this document by inserting a new document into the database if [document.isNew](https://mongoosejs.com/docs/api/document.html#Document.prototype.isNew) is `true`,
+ * or sends an [updateOne](https://mongoosejs.com/docs/api/document.html#Document.prototype.updateOne()) operation with just the modified paths if `isNew` is `false`.
*
* #### Example:
*
@@ -480,23 +535,27 @@ function generateVersionError(doc, modifiedPaths) {
* newProduct === product; // true
*
* @param {Object} [options] options optional options
- * @param {Session} [options.session=null] the [session](https://www.mongodb.com/docs/manual/reference/server-sessions/) associated with this save operation. If not specified, defaults to the [document's associated session](/docs/api/document.html#document_Document-$session).
- * @param {Object} [options.safe] (DEPRECATED) overrides [schema's safe option](https://mongoosejs.com//docs/guide.html#safe). Use the `w` option instead.
+ * @param {Session} [options.session=null] the [session](https://www.mongodb.com/docs/manual/reference/server-sessions/) associated with this save operation. If not specified, defaults to the [document's associated session](https://mongoosejs.com/docs/api/document.html#Document.prototype.session()).
+ * @param {Object} [options.safe] (DEPRECATED) overrides [schema's safe option](https://mongoosejs.com/docs/guide.html#safe). Use the `w` option instead.
* @param {Boolean} [options.validateBeforeSave] set to false to save without validating.
* @param {Boolean} [options.validateModifiedOnly=false] if `true`, Mongoose will only validate modified paths, as opposed to modified paths and `required` paths.
- * @param {Number|String} [options.w] set the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/#w-option). Overrides the [schema-level `writeConcern` option](/docs/guide.html#writeConcern)
- * @param {Boolean} [options.j] set to true for MongoDB to wait until this `save()` has been [journaled before resolving the returned promise](https://www.mongodb.com/docs/manual/reference/write-concern/#j-option). Overrides the [schema-level `writeConcern` option](/docs/guide.html#writeConcern)
- * @param {Number} [options.wtimeout] sets a [timeout for the write concern](https://www.mongodb.com/docs/manual/reference/write-concern/#wtimeout). Overrides the [schema-level `writeConcern` option](/docs/guide.html#writeConcern).
- * @param {Boolean} [options.checkKeys=true] the MongoDB driver prevents you from saving keys that start with '$' or contain '.' by default. Set this option to `false` to skip that check. See [restrictions on field names](https://www.mongodb.com/docs/manual/reference/limits/#Restrictions-on-Field-Names)
- * @param {Boolean} [options.timestamps=true] if `false` and [timestamps](/docs/guide.html#timestamps) are enabled, skip timestamps for this `save()`.
- * @param {Function} [fn] optional callback
- * @throws {DocumentNotFoundError} if this [save updates an existing document](/docs/api/document.html#document_Document-isNew) but the document doesn't exist in the database. For example, you will get this error if the document is [deleted between when you retrieved the document and when you saved it](documents.html#updating).
- * @return {Promise|undefined} Returns undefined if used with callback or a Promise otherwise.
+ * @param {Number|String} [options.w] set the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/#w-option). Overrides the [schema-level `writeConcern` option](https://mongoosejs.com/docs/guide.html#writeConcern)
+ * @param {Boolean} [options.j] set to true for MongoDB to wait until this `save()` has been [journaled before resolving the returned promise](https://www.mongodb.com/docs/manual/reference/write-concern/#j-option). Overrides the [schema-level `writeConcern` option](https://mongoosejs.com/docs/guide.html#writeConcern)
+ * @param {Number} [options.wtimeout] sets a [timeout for the write concern](https://www.mongodb.com/docs/manual/reference/write-concern/#wtimeout). Overrides the [schema-level `writeConcern` option](https://mongoosejs.com/docs/guide.html#writeConcern).
+ * @param {Boolean} [options.checkKeys=true] the MongoDB driver prevents you from saving keys that start with '$' or contain '.' by default. Set this option to `false` to skip that check. See [restrictions on field names](https://docs.mongodb.com/manual/reference/limits/#mongodb-limit-Restrictions-on-Field-Names)
+ * @param {Boolean} [options.timestamps=true] if `false` and [timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this `save()`.
+ * @param {Array} [options.pathsToSave] An array of paths that tell mongoose to only validate and save the paths in `pathsToSave`.
+ * @throws {DocumentNotFoundError} if this [save updates an existing document](https://mongoosejs.com/docs/api/document.html#Document.prototype.isNew) but the document doesn't exist in the database. For example, you will get this error if the document is [deleted between when you retrieved the document and when you saved it](documents.html#updating).
+ * @return {Promise}
* @api public
* @see middleware https://mongoosejs.com/docs/middleware.html
*/
-Model.prototype.save = function(options, fn) {
+Model.prototype.save = async function save(options) {
+ if (typeof options === 'function' || typeof arguments[1] === 'function') {
+ throw new MongooseError('Model.prototype.save() no longer accepts a callback');
+ }
+
let parallelSave;
this.$op = 'save';
@@ -506,11 +565,6 @@ Model.prototype.save = function(options, fn) {
this.$__.saving = new ParallelSaveError(this);
}
- if (typeof options === 'function') {
- fn = options;
- options = undefined;
- }
-
options = new SaveOptions(options);
if (options.hasOwnProperty('session')) {
this.$session(options.session);
@@ -520,364 +574,32 @@ Model.prototype.save = function(options, fn) {
}
this.$__.$versionError = generateVersionError(this, this.modifiedPaths());
- fn = this.constructor.$handleCallbackError(fn);
- return this.constructor.db.base._promiseOrCallback(fn, cb => {
- cb = this.constructor.$wrapCallback(cb);
-
- if (parallelSave) {
- this.$__handleReject(parallelSave);
- return cb(parallelSave);
- }
+ if (parallelSave) {
+ this.$__handleReject(parallelSave);
+ throw parallelSave;
+ }
- this.$__.saveOptions = options;
+ this.$__.saveOptions = options;
+ await new Promise((resolve, reject) => {
this.$__save(options, error => {
this.$__.saving = null;
this.$__.saveOptions = null;
this.$__.$versionError = null;
this.$op = null;
-
- if (error) {
+ if (error != null) {
this.$__handleReject(error);
- return cb(error);
+ return reject(error);
}
- cb(null, this);
- });
- }, this.constructor.events);
-};
-Model.prototype.$save = Model.prototype.save;
-
-/**
- * Determines whether versioning should be skipped for the given path
- *
- * @param {Document} self
- * @param {String} path
- * @return {Boolean} true if versioning should be skipped for the given path
- * @api private
- */
-function shouldSkipVersioning(self, path) {
- const skipVersioning = self.$__schema.options.skipVersioning;
- if (!skipVersioning) return false;
-
- // Remove any array indexes from the path
- path = path.replace(/\.\d+\./, '.');
-
- return skipVersioning[path];
-}
-
-/**
- * Apply the operation to the delta (update) clause as
- * well as track versioning for our where clause.
- *
- * @param {Document} self
- * @param {Object} where Unused
- * @param {Object} delta
- * @param {Object} data
- * @param {Mixed} val
- * @param {String} [op]
- * @api private
- */
-
-function operand(self, where, delta, data, val, op) {
- // delta
- op || (op = '$set');
- if (!delta[op]) delta[op] = {};
- delta[op][data.path] = val;
- // disabled versioning?
- if (self.$__schema.options.versionKey === false) return;
-
- // path excluded from versioning?
- if (shouldSkipVersioning(self, data.path)) return;
-
- // already marked for versioning?
- if (VERSION_ALL === (VERSION_ALL & self.$__.version)) return;
-
- if (self.$__schema.options.optimisticConcurrency) {
- return;
- }
-
- switch (op) {
- case '$set':
- case '$unset':
- case '$pop':
- case '$pull':
- case '$pullAll':
- case '$push':
- case '$addToSet':
- case '$inc':
- break;
- default:
- // nothing to do
- return;
- }
-
- // ensure updates sent with positional notation are
- // editing the correct array element.
- // only increment the version if an array position changes.
- // modifying elements of an array is ok if position does not change.
- if (op === '$push' || op === '$addToSet' || op === '$pullAll' || op === '$pull') {
- if (/\.\d+\.|\.\d+$/.test(data.path)) {
- increment.call(self);
- } else {
- self.$__.version = VERSION_INC;
- }
- } else if (/^\$p/.test(op)) {
- // potentially changing array positions
- increment.call(self);
- } else if (Array.isArray(val)) {
- // $set an array
- increment.call(self);
- } else if (/\.\d+\.|\.\d+$/.test(data.path)) {
- // now handling $set, $unset
- // subpath of array
- self.$__.version = VERSION_WHERE;
- }
-}
-
-/**
- * Compiles an update and where clause for a `val` with _atomics.
- *
- * @param {Document} self
- * @param {Object} where
- * @param {Object} delta
- * @param {Object} data
- * @param {Array} value
- * @api private
- */
-
-function handleAtomics(self, where, delta, data, value) {
- if (delta.$set && delta.$set[data.path]) {
- // $set has precedence over other atomics
- return;
- }
-
- if (typeof value.$__getAtomics === 'function') {
- value.$__getAtomics().forEach(function(atomic) {
- const op = atomic[0];
- const val = atomic[1];
- operand(self, where, delta, data, val, op);
+ resolve();
});
- return;
- }
-
- // legacy support for plugins
-
- const atomics = value[arrayAtomicsSymbol];
- const ops = Object.keys(atomics);
- let i = ops.length;
- let val;
- let op;
-
- if (i === 0) {
- // $set
-
- if (utils.isMongooseObject(value)) {
- value = value.toObject({ depopulate: 1, _isNested: true });
- } else if (value.valueOf) {
- value = value.valueOf();
- }
-
- return operand(self, where, delta, data, value);
- }
-
- function iter(mem) {
- return utils.isMongooseObject(mem)
- ? mem.toObject({ depopulate: 1, _isNested: true })
- : mem;
- }
-
- while (i--) {
- op = ops[i];
- val = atomics[op];
-
- if (utils.isMongooseObject(val)) {
- val = val.toObject({ depopulate: true, transform: false, _isNested: true });
- } else if (Array.isArray(val)) {
- val = val.map(iter);
- } else if (val.valueOf) {
- val = val.valueOf();
- }
-
- if (op === '$addToSet') {
- val = { $each: val };
- }
-
- operand(self, where, delta, data, val, op);
- }
-}
-
-/**
- * Produces a special query document of the modified properties used in updates.
- *
- * @api private
- * @method $__delta
- * @memberOf Model
- * @instance
- */
-
-Model.prototype.$__delta = function() {
- const dirty = this.$__dirty();
-
- const optimisticConcurrency = this.$__schema.options.optimisticConcurrency;
- if (optimisticConcurrency) {
- this.$__.version = dirty.length ? VERSION_ALL : VERSION_WHERE;
- }
-
- if (!dirty.length && VERSION_ALL !== this.$__.version) {
- return;
- }
- const where = {};
- const delta = {};
- const len = dirty.length;
- const divergent = [];
- let d = 0;
-
- where._id = this._doc._id;
- // If `_id` is an object, need to depopulate, but also need to be careful
- // because `_id` can technically be null (see gh-6406)
- if ((where && where._id && where._id.$__ || null) != null) {
- where._id = where._id.toObject({ transform: false, depopulate: true });
- }
- for (; d < len; ++d) {
- const data = dirty[d];
- let value = data.value;
- const match = checkDivergentArray(this, data.path, value);
- if (match) {
- divergent.push(match);
- continue;
- }
-
- const pop = this.$populated(data.path, true);
- if (!pop && this.$__.selected) {
- // If any array was selected using an $elemMatch projection, we alter the path and where clause
- // NOTE: MongoDB only supports projected $elemMatch on top level array.
- const pathSplit = data.path.split('.');
- const top = pathSplit[0];
- if (this.$__.selected[top] && this.$__.selected[top].$elemMatch) {
- // If the selected array entry was modified
- if (pathSplit.length > 1 && pathSplit[1] == 0 && typeof where[top] === 'undefined') {
- where[top] = this.$__.selected[top];
- pathSplit[1] = '$';
- data.path = pathSplit.join('.');
- }
- // if the selected array was modified in any other way throw an error
- else {
- divergent.push(data.path);
- continue;
- }
- }
- }
-
- // If this path is set to default, and either this path or one of
- // its parents is excluded, don't treat this path as dirty.
- if (this.$isDefault(data.path) && this.$__.selected) {
- if (data.path.indexOf('.') === -1 && isPathExcluded(this.$__.selected, data.path)) {
- continue;
- }
-
- const pathsToCheck = parentPaths(data.path);
- if (pathsToCheck.find(path => isPathExcluded(this.$__.isSelected, path))) {
- continue;
- }
- }
-
- if (divergent.length) continue;
- if (value === undefined) {
- operand(this, where, delta, data, 1, '$unset');
- } else if (value === null) {
- operand(this, where, delta, data, null);
- } else if (utils.isMongooseArray(value) && value.$path() && value[arrayAtomicsSymbol]) {
- // arrays and other custom types (support plugins etc)
- handleAtomics(this, where, delta, data, value);
- } else if (value[MongooseBuffer.pathSymbol] && Buffer.isBuffer(value)) {
- // MongooseBuffer
- value = value.toObject();
- operand(this, where, delta, data, value);
- } else {
- if (this.$__.primitiveAtomics && this.$__.primitiveAtomics[data.path] != null) {
- const val = this.$__.primitiveAtomics[data.path];
- const op = firstKey(val);
- operand(this, where, delta, data, val[op], op);
- } else {
- value = utils.clone(value, {
- depopulate: true,
- transform: false,
- virtuals: false,
- getters: false,
- omitUndefined: true,
- _isNested: true
- });
- operand(this, where, delta, data, value);
- }
- }
- }
-
- if (divergent.length) {
- return new DivergentArrayError(divergent);
- }
-
- if (this.$__.version) {
- this.$__version(where, delta);
- }
-
- if (Object.keys(delta).length === 0) {
- return [where, null];
- }
+ });
- return [where, delta];
+ return this;
};
-/**
- * Determine if array was populated with some form of filter and is now
- * being updated in a manner which could overwrite data unintentionally.
- *
- * @see https://github.com/Automattic/mongoose/issues/1334
- * @param {Document} doc
- * @param {String} path
- * @param {Any} array
- * @return {String|undefined}
- * @api private
- */
-
-function checkDivergentArray(doc, path, array) {
- // see if we populated this path
- const pop = doc.$populated(path, true);
-
- if (!pop && doc.$__.selected) {
- // If any array was selected using an $elemMatch projection, we deny the update.
- // NOTE: MongoDB only supports projected $elemMatch on top level array.
- const top = path.split('.')[0];
- if (doc.$__.selected[top + '.$']) {
- return top;
- }
- }
-
- if (!(pop && utils.isMongooseArray(array))) return;
-
- // If the array was populated using options that prevented all
- // documents from being returned (match, skip, limit) or they
- // deselected the _id field, $pop and $set of the array are
- // not safe operations. If _id was deselected, we do not know
- // how to remove elements. $pop will pop off the _id from the end
- // of the array in the db which is not guaranteed to be the
- // same as the last element we have here. $set of the entire array
- // would be similarly destructive as we never received all
- // elements of the array and potentially would overwrite data.
- const check = pop.options.match ||
- pop.options.options && utils.object.hasOwnProperty(pop.options.options, 'limit') || // 0 is not permitted
- pop.options.options && pop.options.options.skip || // 0 is permitted
- pop.options.select && // deselected _id?
- (pop.options.select._id === 0 ||
- /\s?-_id\s?/.test(pop.options.select));
-
- if (check) {
- const atomics = array[arrayAtomicsSymbol];
- if (Object.keys(atomics).length === 0 || atomics.$set || atomics.$pop) {
- return path;
- }
- }
-}
+Model.prototype.$save = Model.prototype.save;
/**
* Appends versioning to the where and update clauses.
@@ -933,15 +655,6 @@ Model.prototype.$__version = function(where, delta) {
}
};
-/*!
- * ignore
- */
-
-function increment() {
- this.$__.version = VERSION_ALL;
- return this;
-}
-
/**
* Signal that we desire an increment of this documents version.
*
@@ -957,7 +670,10 @@ function increment() {
* @api public
*/
-Model.prototype.increment = increment;
+Model.prototype.increment = function increment() {
+ this.$__.version = VERSION_ALL;
+ return this;
+};
/**
* Returns a query object
@@ -983,164 +699,117 @@ Model.prototype.$__where = function _where(where) {
};
/**
- * Removes this document from the db.
+ * Delete this document from the db. Returns a Query instance containing a `deleteOne` operation by this document's `_id`.
*
* #### Example:
*
- * const product = await product.remove().catch(function (err) {
- * assert.ok(err);
- * });
- * const foundProduct = await Product.findById(product._id);
- * console.log(foundProduct) // null
- *
- * @param {Object} [options]
- * @param {Session} [options.session=null] the [session](https://www.mongodb.com/docs/manual/reference/server-sessions/) associated with this operation. If not specified, defaults to the [document's associated session](/docs/api/document.html#document_Document-$session).
- * @param {function(err,product)} [fn] optional callback
- * @return {Promise} Promise
- * @api public
- */
-
-Model.prototype.remove = function remove(options, fn) {
- if (typeof options === 'function') {
- fn = options;
- options = undefined;
- }
-
- options = new RemoveOptions(options);
- if (options.hasOwnProperty('session')) {
- this.$session(options.session);
- }
- this.$op = 'remove';
-
- fn = this.constructor.$handleCallbackError(fn);
-
- return this.constructor.db.base._promiseOrCallback(fn, cb => {
- cb = this.constructor.$wrapCallback(cb);
- this.$__remove(options, (err, res) => {
- this.$op = null;
- cb(err, res);
- });
- }, this.constructor.events);
-};
-
-/**
- * Alias for remove
+ * await product.deleteOne();
+ * await Product.findById(product._id); // null
*
- * @method $remove
- * @memberOf Model
- * @instance
- * @api public
- * @see Model.remove #model_Model-remove
- */
-
-Model.prototype.$remove = Model.prototype.remove;
-Model.prototype.delete = Model.prototype.remove;
-
-/**
- * Removes this document from the db. Equivalent to `.remove()`.
+ * Since `deleteOne()` returns a Query, the `deleteOne()` will **not** execute unless you use either `await`, `.then()`, `.catch()`, or [`.exec()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.exec())
*
* #### Example:
*
- * product = await product.deleteOne();
- * await Product.findById(product._id); // null
+ * product.deleteOne(); // Doesn't do anything
+ * product.deleteOne().exec(); // Deletes the document, returns a promise
*
- * @param {function(err,product)} [fn] optional callback
- * @return {Promise} Promise
+ * @return {Query} Query
* @api public
*/
-Model.prototype.deleteOne = function deleteOne(options, fn) {
- if (typeof options === 'function') {
- fn = options;
- options = undefined;
+Model.prototype.deleteOne = function deleteOne(options) {
+ if (typeof options === 'function' ||
+ typeof arguments[1] === 'function') {
+ throw new MongooseError('Model.prototype.deleteOne() no longer accepts a callback');
}
if (!options) {
options = {};
}
- fn = this.constructor.$handleCallbackError(fn);
-
- return this.constructor.db.base._promiseOrCallback(fn, cb => {
- cb = this.constructor.$wrapCallback(cb);
- this.$__deleteOne(options, cb);
- }, this.constructor.events);
-};
-
-/*!
- * ignore
- */
-
-Model.prototype.$__remove = function $__remove(options, cb) {
- if (this.$__.isDeleted) {
- return immediate(() => cb(null, this));
+ if (options.hasOwnProperty('session')) {
+ this.$session(options.session);
}
+ const self = this;
const where = this.$__where();
- if (where instanceof MongooseError) {
- return cb(where);
+ if (where instanceof Error) {
+ throw where;
}
+ const query = self.constructor.deleteOne(where, options);
- _applyCustomWhere(this, where);
-
- const session = this.$session();
- if (!options.hasOwnProperty('session')) {
- options.session = session;
+ if (this.$session() != null) {
+ if (!('session' in query.options)) {
+ query.options.session = this.$session();
+ }
}
- this[modelCollectionSymbol].deleteOne(where, options, err => {
- if (!err) {
- this.$__.isDeleted = true;
- this.$emit('remove', this);
- this.constructor.emit('remove', this);
- return cb(null, this);
+ query.pre(function queryPreDeleteOne(cb) {
+ self.constructor._middleware.execPre('deleteOne', self, [self], cb);
+ });
+ query.pre(function callSubdocPreHooks(cb) {
+ each(self.$getAllSubdocs(), (subdoc, cb) => {
+ subdoc.constructor._middleware.execPre('deleteOne', subdoc, [subdoc], cb);
+ }, cb);
+ });
+ query.pre(function skipIfAlreadyDeleted(cb) {
+ if (self.$__.isDeleted) {
+ return cb(Kareem.skipWrappedFunction());
}
- this.$__.isDeleted = false;
- cb(err);
+ return cb();
+ });
+ query.post(function callSubdocPostHooks(cb) {
+ each(self.$getAllSubdocs(), (subdoc, cb) => {
+ subdoc.constructor._middleware.execPost('deleteOne', subdoc, [subdoc], {}, cb);
+ }, cb);
+ });
+ query.post(function queryPostDeleteOne(cb) {
+ self.constructor._middleware.execPost('deleteOne', self, [self], {}, cb);
});
-};
-
-/*!
- * ignore
- */
-Model.prototype.$__deleteOne = Model.prototype.$__remove;
+ return query;
+};
/**
- * Returns another Model instance.
+ * Returns the model instance used to create this document if no `name` specified.
+ * If `name` specified, returns the model with the given `name`.
*
* #### Example:
*
- * const doc = new Tank;
- * doc.model('User').findById(id, callback);
+ * const doc = new Tank({});
+ * doc.$model() === Tank; // true
+ * await doc.$model('User').findById(id);
*
- * @param {String} name model name
- * @method model
+ * @param {String} [name] model name
+ * @method $model
* @api public
* @return {Model}
*/
-Model.prototype.model = function model(name) {
+Model.prototype.$model = function $model(name) {
+ if (arguments.length === 0) {
+ return this.constructor;
+ }
return this[modelDbSymbol].model(name);
};
/**
- * Returns another Model instance.
+ * Returns the model instance used to create this document if no `name` specified.
+ * If `name` specified, returns the model with the given `name`.
*
* #### Example:
*
- * const doc = new Tank;
- * doc.model('User').findById(id, callback);
+ * const doc = new Tank({});
+ * doc.$model() === Tank; // true
+ * await doc.$model('User').findById(id);
*
- * @param {String} name model name
- * @method $model
+ * @param {String} [name] model name
+ * @method model
* @api public
* @return {Model}
*/
-Model.prototype.$model = function $model(name) {
- return this[modelDbSymbol].model(name);
-};
+Model.prototype.model = Model.prototype.$model;
/**
* Returns a document with `_id` only if at least one document exists in the database that matches
@@ -1162,16 +831,14 @@ Model.prototype.$model = function $model(name) {
* - `findOne()`
*
* @param {Object} filter
- * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#query_Query-setOptions)
- * @param {Function} [callback] callback
+ * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions())
* @return {Query}
*/
-Model.exists = function exists(filter, options, callback) {
+Model.exists = function exists(filter, options) {
_checkContext(this, 'exists');
- if (typeof options === 'function') {
- callback = options;
- options = null;
+ if (typeof arguments[2] === 'function') {
+ throw new MongooseError('Model.exists() no longer accepts a callback');
}
const query = this.findOne(filter).
@@ -1179,10 +846,6 @@ Model.exists = function exists(filter, options, callback) {
lean().
setOptions(options);
- if (typeof callback === 'function') {
- return query.exec(callback);
- }
-
return query;
};
@@ -1238,6 +901,7 @@ Model.discriminator = function(name, schema, options) {
const value = utils.isPOJO(options) ? options.value : options;
const clone = typeof options.clone === 'boolean' ? options.clone : true;
const mergePlugins = typeof options.mergePlugins === 'boolean' ? options.mergePlugins : true;
+ const overwriteModels = typeof options.overwriteModels === 'boolean' ? options.overwriteModels : false;
_checkContext(this, 'discriminator');
@@ -1248,8 +912,8 @@ Model.discriminator = function(name, schema, options) {
schema = schema.clone();
}
- schema = discriminator(this, name, schema, value, mergePlugins, options.mergeHooks);
- if (this.db.models[name] && !schema.options.overwriteModels) {
+ schema = discriminator(this, name, schema, value, mergePlugins, options.mergeHooks, overwriteModels);
+ if (this.db.models[name] && !schema.options.overwriteModels && !overwriteModels) {
throw new OverwriteModelError(name);
}
@@ -1311,12 +975,16 @@ for (const i in EventEmitter.prototype) {
}
/**
- * This function is responsible for building [indexes](https://www.mongodb.com/docs/manual/indexes/),
- * unless [`autoIndex`](https://mongoosejs.com/docs/guide.html#autoIndex) is turned off.
+ * This function is responsible for initializing the underlying connection in MongoDB based on schema options.
+ * This function performs the following operations:
*
- * Mongoose calls this function automatically when a model is created using
- * [`mongoose.model()`](/docs/api/mongoose.html#mongoose_Mongoose-model) or
- * [`connection.model()`](/docs/api/connection.html#connection_Connection-model), so you
+ * - `createCollection()` unless [`autoCreate`](https://mongoosejs.com/docs/guide.html#autoCreate) option is turned off
+ * - `ensureIndexes()` unless [`autoIndex`](https://mongoosejs.com/docs/guide.html#autoIndex) option is turned off
+ * - `createSearchIndex()` on all schema search indexes if `autoSearchIndex` is enabled.
+ *
+ * Mongoose calls this function automatically when a model is a created using
+ * [`mongoose.model()`](https://mongoosejs.com/docs/api/mongoose.html#Mongoose.prototype.model()) or
+ * [`connection.model()`](https://mongoosejs.com/docs/api/connection.html#Connection.prototype.model()), so you
* don't need to call `init()` to trigger index builds.
*
* However, you _may_ need to call `init()` to get back a promise that will resolve when your indexes are finished.
@@ -1334,75 +1002,79 @@ for (const i in EventEmitter.prototype) {
* console.log('Indexes are done building!');
*
* @api public
- * @param {Function} [callback]
* @returns {Promise}
*/
-Model.init = function init(callback) {
+Model.init = function init() {
_checkContext(this, 'init');
+ if (typeof arguments[0] === 'function') {
+ throw new MongooseError('Model.init() no longer accepts a callback');
+ }
this.schema.emit('init', this);
if (this.$init != null) {
- if (callback) {
- this.$init.then(() => callback(), err => callback(err));
- return null;
- }
return this.$init;
}
- const Promise = PromiseProvider.get();
-
- const model = this;
- const _ensureIndexes = function(cb) {
- const autoIndex = utils.getOption('autoIndex',
- model.schema.options, model.db.config, model.db.base.options);
- if (autoIndex) model.ensureIndexes({ _automatic: true }, cb);
- else cb();
+ const conn = this.db;
+ const _ensureIndexes = async() => {
+ const autoIndex = utils.getOption(
+ 'autoIndex',
+ this.schema.options,
+ conn.config,
+ conn.base.options
+ );
+ if (!autoIndex) {
+ return;
+ }
+ return await this.ensureIndexes({ _automatic: true });
};
- const _createCollection = function(cb) {
- const conn = model.db;
+ const _createSearchIndexes = async() => {
+ const autoSearchIndex = utils.getOption(
+ 'autoSearchIndex',
+ this.schema.options,
+ conn.config,
+ conn.base.options
+ );
+ if (!autoSearchIndex) {
+ return;
+ }
+ const results = [];
+ for (const searchIndex of this.schema._searchIndexes) {
+ results.push(await this.createSearchIndex(searchIndex));
+ }
+ return results;
+ };
+ const _createCollection = async() => {
if ((conn.readyState === STATES.connecting || conn.readyState === STATES.disconnected) && conn._shouldBufferCommands()) {
- conn._queue.push({ fn: _createCollection, ctx: conn, args: [cb] });
- } else {
- try {
- const autoCreate = utils.getOption('autoCreate',
- model.schema.options, model.db.config, model.db.base.options);
- if (autoCreate) model.createCollection({}, cb);
- else cb();
- } catch (err) {
- return cb(err);
- }
+ await new Promise(resolve => {
+ conn._queue.push({ fn: resolve });
+ });
}
+ const autoCreate = utils.getOption(
+ 'autoCreate',
+ this.schema.options,
+ conn.config,
+ conn.base.options
+ );
+ if (!autoCreate) {
+ return;
+ }
+ return await this.createCollection();
};
- this.$init = new Promise((resolve, reject) => {
- _createCollection(error => {
- if (error) {
- return reject(error);
- }
- _ensureIndexes(error => {
- if (error) {
- return reject(error);
- }
- resolve(this);
- });
- });
- });
+ this.$init = _createCollection().
+ then(() => _ensureIndexes()).
+ then(() => _createSearchIndexes());
- if (callback) {
- this.$init.then(() => callback(), err => callback(err));
- this.$caught = true;
- return null;
- } else {
- const _catch = this.$init.catch;
- const _this = this;
- this.$init.catch = function() {
- this.$caught = true;
- return _catch.apply(_this.$init, arguments);
- };
- }
+ const _catch = this.$init.catch;
+ const _this = this;
+ this.$init.catch = function() {
+ _this.$caught = true;
+ return _catch.apply(_this.$init, arguments);
+ };
return this.$init;
};
@@ -1430,19 +1102,33 @@ Model.init = function init(callback) {
*
* @api public
* @param {Object} [options] see [MongoDB driver docs](https://mongodb.github.io/node-mongodb-native/4.9/classes/Db.html#createCollection)
- * @param {Function} [callback]
* @returns {Promise}
*/
-Model.createCollection = function createCollection(options, callback) {
+Model.createCollection = async function createCollection(options) {
_checkContext(this, 'createCollection');
+ if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function') {
+ throw new MongooseError('Model.createCollection() no longer accepts a callback');
+ }
- if (typeof options === 'string') {
- throw new MongooseError('You can\'t specify a new collection name in Model.createCollection.' +
- 'This is not like Connection.createCollection. Only options are accepted here.');
- } else if (typeof options === 'function') {
- callback = options;
- options = void 0;
+ const shouldSkip = await new Promise((resolve, reject) => {
+ this.hooks.execPre('createCollection', this, [options], (err) => {
+ if (err != null) {
+ if (err instanceof Kareem.skipWrappedFunction) {
+ return resolve(true);
+ }
+ return reject(err);
+ }
+ resolve();
+ });
+ });
+
+ const collectionOptions = this &&
+ this.schema &&
+ this.schema.options &&
+ this.schema.options.collectionOptions;
+ if (collectionOptions != null) {
+ options = Object.assign({}, collectionOptions, options);
}
const schemaCollation = this &&
@@ -1481,19 +1167,42 @@ Model.createCollection = function createCollection(options, callback) {
}
}
- callback = this.$handleCallbackError(callback);
+ const clusteredIndex = this &&
+ this.schema &&
+ this.schema.options &&
+ this.schema.options.clusteredIndex;
+ if (clusteredIndex != null) {
+ options = Object.assign({ clusteredIndex: { ...clusteredIndex, unique: true } }, options);
+ }
- return this.db.base._promiseOrCallback(callback, cb => {
- cb = this.$wrapCallback(cb);
+ try {
+ if (!shouldSkip) {
+ await this.db.createCollection(this.$__collection.collectionName, options);
+ }
+ } catch (err) {
+ if (err != null && (err.name !== 'MongoServerError' || err.code !== 48)) {
+ await new Promise((resolve, reject) => {
+ const _opts = { error: err };
+ this.hooks.execPost('createCollection', this, [null], _opts, (err) => {
+ if (err != null) {
+ return reject(err);
+ }
+ resolve();
+ });
+ });
+ }
+ }
- this.db.createCollection(this.$__collection.collectionName, options, utils.tick((err) => {
- if (err != null && (err.name !== 'MongoServerError' || err.code !== 48)) {
- return cb(err);
+ await new Promise((resolve, reject) => {
+ this.hooks.execPost('createCollection', this, [this.$__collection], (err) => {
+ if (err != null) {
+ return reject(err);
}
- this.$__collection = this.db.collection(this.$__collection.collectionName, options);
- cb(null, this.$__collection);
- }));
- }, this.events);
+ resolve();
+ });
+ });
+
+ return this.$__collection;
};
/**
@@ -1513,84 +1222,176 @@ Model.createCollection = function createCollection(options, callback) {
* // Will drop the 'age' index and create an index on `name`
* await Customer.syncIndexes();
*
+ * You should be careful about running `syncIndexes()` on production applications under heavy load,
+ * because index builds are expensive operations, and unexpected index drops can lead to degraded
+ * performance. Before running `syncIndexes()`, you can use the [`diffIndexes()` function](#Model.diffIndexes())
+ * to check what indexes `syncIndexes()` will drop and create.
+ *
+ * #### Example:
+ *
+ * const { toDrop, toCreate } = await Model.diffIndexes();
+ * toDrop; // Array of strings containing names of indexes that `syncIndexes()` will drop
+ * toCreate; // Array of strings containing names of indexes that `syncIndexes()` will create
+ *
* @param {Object} [options] options to pass to `ensureIndexes()`
* @param {Boolean} [options.background=null] if specified, overrides each index's `background` property
- * @param {Function} [callback] optional callback
- * @return {Promise|undefined} Returns `undefined` if callback is specified, returns a promise if no callback, when the Promise resolves the value is a list of the dropped indexes.
+ * @param {Boolean} [options.hideIndexes=false] set to `true` to hide indexes instead of dropping. Requires MongoDB server 4.4 or higher
+ * @return {Promise}
* @api public
*/
-Model.syncIndexes = function syncIndexes(options, callback) {
+Model.syncIndexes = async function syncIndexes(options) {
_checkContext(this, 'syncIndexes');
+ if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function') {
+ throw new MongooseError('Model.syncIndexes() no longer accepts a callback');
+ }
const model = this;
- callback = model.$handleCallbackError(callback);
- return model.db.base._promiseOrCallback(callback, cb => {
- cb = model.$wrapCallback(cb);
- model.createCollection(err => {
- if (err != null && (err.name !== 'MongoServerError' || err.code !== 48)) {
- return cb(err);
- }
- model.diffIndexes(err, (err, diffIndexesResult) => {
- if (err != null) {
- return cb(err);
- }
- model.cleanIndexes({ ...options, toDrop: diffIndexesResult.toDrop }, (err, dropped) => {
- if (err != null) {
- return cb(err);
- }
- model.createIndexes({ ...options, toCreate: diffIndexesResult.toCreate }, err => {
- if (err != null) {
- return cb(err);
- }
- cb(null, dropped);
- });
- });
- });
+ try {
+ await model.createCollection();
+ } catch (err) {
+ if (err != null && (err.name !== 'MongoServerError' || err.code !== 48)) {
+ throw err;
+ }
+ }
- });
- }, this.events);
+ const diffIndexesResult = await model.diffIndexes();
+ const dropped = await model.cleanIndexes({ ...options, toDrop: diffIndexesResult.toDrop });
+ await model.createIndexes({ ...options, toCreate: diffIndexesResult.toCreate });
+
+ return dropped;
+};
+
+/**
+ * Create an [Atlas search index](https://www.mongodb.com/docs/atlas/atlas-search/create-index/).
+ * This function only works when connected to MongoDB Atlas.
+ *
+ * #### Example:
+ *
+ * const schema = new Schema({ name: { type: String, unique: true } });
+ * const Customer = mongoose.model('Customer', schema);
+ * await Customer.createSearchIndex({ name: 'test', definition: { mappings: { dynamic: true } } });
+ *
+ * @param {Object} description index options, including `name` and `definition`
+ * @param {String} description.name
+ * @param {Object} description.definition
+ * @return {Promise}
+ * @api public
+ */
+
+Model.createSearchIndex = async function createSearchIndex(description) {
+ _checkContext(this, 'createSearchIndex');
+
+ return await this.$__collection.createSearchIndex(description);
+};
+
+/**
+ * Update an existing [Atlas search index](https://www.mongodb.com/docs/atlas/atlas-search/create-index/).
+ * This function only works when connected to MongoDB Atlas.
+ *
+ * #### Example:
+ *
+ * const schema = new Schema({ name: { type: String, unique: true } });
+ * const Customer = mongoose.model('Customer', schema);
+ * await Customer.updateSearchIndex('test', { mappings: { dynamic: true } });
+ *
+ * @param {String} name
+ * @param {Object} definition
+ * @return {Promise}
+ * @api public
+ */
+
+Model.updateSearchIndex = async function updateSearchIndex(name, definition) {
+ _checkContext(this, 'updateSearchIndex');
+
+ return await this.$__collection.updateSearchIndex(name, definition);
+};
+
+/**
+ * Delete an existing [Atlas search index](https://www.mongodb.com/docs/atlas/atlas-search/create-index/) by name.
+ * This function only works when connected to MongoDB Atlas.
+ *
+ * #### Example:
+ *
+ * const schema = new Schema({ name: { type: String, unique: true } });
+ * const Customer = mongoose.model('Customer', schema);
+ * await Customer.dropSearchIndex('test');
+ *
+ * @param {String} name
+ * @return {Promise}
+ * @api public
+ */
+
+Model.dropSearchIndex = async function dropSearchIndex(name) {
+ _checkContext(this, 'dropSearchIndex');
+
+ return await this.$__collection.dropSearchIndex(name);
};
/**
- * Does a dry-run of Model.syncIndexes(), meaning that
- * the result of this function would be the result of
- * Model.syncIndexes().
+ * List all [Atlas search indexes](https://www.mongodb.com/docs/atlas/atlas-search/create-index/) on this model's collection.
+ * This function only works when connected to MongoDB Atlas.
+ *
+ * #### Example:
+ *
+ * const schema = new Schema({ name: { type: String, unique: true } });
+ * const Customer = mongoose.model('Customer', schema);
+ *
+ * await Customer.createSearchIndex({ name: 'test', definition: { mappings: { dynamic: true } } });
+ * const res = await Customer.listSearchIndexes(); // Includes `[{ name: 'test' }]`
*
* @param {Object} [options]
- * @param {Function} [callback] optional callback
- * @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.
+ * @return {Promise}
+ * @api public
*/
-Model.diffIndexes = function diffIndexes(options, callback) {
- if (typeof options === 'function') {
- callback = options;
- options = null;
- }
+Model.listSearchIndexes = async function listSearchIndexes(options) {
+ _checkContext(this, 'listSearchIndexes');
- const model = this;
+ const cursor = await this.$__collection.listSearchIndexes(options);
- callback = model.$handleCallbackError(callback);
+ return await cursor.toArray();
+};
- return model.db.base._promiseOrCallback(callback, cb => {
- cb = model.$wrapCallback(cb);
- model.listIndexes((err, dbIndexes) => {
- if (dbIndexes === undefined) {
- dbIndexes = [];
- }
- dbIndexes = getRelatedDBIndexes(model, dbIndexes);
+/**
+ * Does a dry-run of `Model.syncIndexes()`, returning the indexes that `syncIndexes()` would drop and create if you were to run `syncIndexes()`.
+ *
+ * #### Example:
+ *
+ * const { toDrop, toCreate } = await Model.diffIndexes();
+ * toDrop; // Array of strings containing names of indexes that `syncIndexes()` will drop
+ * toCreate; // Array of strings containing names of indexes that `syncIndexes()` will create
+ *
+ * @param {Object} [options]
+ * @return {Promise} contains the indexes that would be dropped in MongoDB and indexes that would be created in MongoDB as `{ toDrop: string[], toCreate: string[] }`.
+ */
- const schema = model.schema;
- const schemaIndexes = getRelatedSchemaIndexes(model, schema.indexes());
+Model.diffIndexes = async function diffIndexes() {
+ if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function') {
+ throw new MongooseError('Model.syncIndexes() no longer accepts a callback');
+ }
- const toDrop = getIndexesToDrop(schema, schemaIndexes, dbIndexes);
- const toCreate = getIndexesToCreate(schema, schemaIndexes, dbIndexes, toDrop);
+ const model = this;
- cb(null, { toDrop, toCreate });
- });
+ let dbIndexes = await model.listIndexes().catch(err => {
+ if (err.codeName == 'NamespaceNotFound') {
+ return undefined;
+ }
+ throw err;
});
+ if (dbIndexes === undefined) {
+ dbIndexes = [];
+ }
+ dbIndexes = getRelatedDBIndexes(model, dbIndexes);
+
+ const schema = model.schema;
+ const schemaIndexes = getRelatedSchemaIndexes(model, schema.indexes());
+
+ const toDrop = getIndexesToDrop(schema, schemaIndexes, dbIndexes);
+ const toCreate = getIndexesToCreate(schema, schemaIndexes, dbIndexes, toDrop);
+
+ return { toDrop, toCreate };
};
function getIndexesToCreate(schema, schemaIndexes, dbIndexes, toDrop) {
@@ -1599,7 +1400,7 @@ function getIndexesToCreate(schema, schemaIndexes, dbIndexes, toDrop) {
for (const [schemaIndexKeysObject, schemaIndexOptions] of schemaIndexes) {
let found = false;
- const options = decorateDiscriminatorIndexOptions(schema, utils.clone(schemaIndexOptions));
+ const options = decorateDiscriminatorIndexOptions(schema, clone(schemaIndexOptions));
for (const index of dbIndexes) {
if (isDefaultIdIndex(index)) {
@@ -1631,9 +1432,13 @@ function getIndexesToDrop(schema, schemaIndexes, dbIndexes) {
if (isDefaultIdIndex(dbIndex)) {
continue;
}
+ // Timeseries collections have a default index on { timeField: 1, metaField: 1 }.
+ if (isTimeseriesIndex(dbIndex, schema.options)) {
+ continue;
+ }
for (const [schemaIndexKeysObject, schemaIndexOptions] of schemaIndexes) {
- const options = decorateDiscriminatorIndexOptions(schema, utils.clone(schemaIndexOptions));
+ const options = decorateDiscriminatorIndexOptions(schema, clone(schemaIndexOptions));
applySchemaCollation(schemaIndexKeysObject, options, schema.options);
if (isIndexEqual(schemaIndexKeysObject, options, dbIndex)) {
@@ -1642,9 +1447,11 @@ function getIndexesToDrop(schema, schemaIndexes, dbIndexes) {
}
}
- if (!found) {
- toDrop.push(dbIndex.name);
+ if (found) {
+ continue;
}
+
+ toDrop.push(dbIndex.name);
}
return toDrop;
@@ -1655,91 +1462,72 @@ function getIndexesToDrop(schema, schemaIndexes, dbIndexes) {
*
* The returned promise resolves to a list of the dropped indexes' names as an array
*
- * @param {Function} [callback] optional callback
- * @return {Promise|undefined} Returns `undefined` if callback is specified, returns a promise if no callback.
+ * @param {Object} [options]
+ * @param {Array} [options.toDrop] if specified, contains a list of index names to drop
+ * @param {Boolean} [options.hideIndexes=false] set to `true` to hide indexes instead of dropping. Requires MongoDB server 4.4 or higher
+ * @return {Promise} list of dropped or hidden index names
* @api public
*/
-Model.cleanIndexes = function cleanIndexes(options, callback) {
+Model.cleanIndexes = async function cleanIndexes(options) {
_checkContext(this, 'cleanIndexes');
+ if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function') {
+ throw new MongooseError('Model.cleanIndexes() no longer accepts a callback');
+ }
const model = this;
- if (typeof options === 'function') {
- callback = options;
- options = null;
+ if (Array.isArray(options && options.toDrop)) {
+ const res = await _dropIndexes(options.toDrop, model, options);
+ return res;
}
- callback = model.$handleCallbackError(callback);
-
- return model.db.base._promiseOrCallback(callback, cb => {
- const collection = model.$__collection;
-
- if (Array.isArray(options && options.toDrop)) {
- _dropIndexes(options.toDrop, collection, cb);
- return;
- }
- return model.diffIndexes((err, res) => {
- if (err != null) {
- return cb(err);
- }
-
- const toDrop = res.toDrop;
- _dropIndexes(toDrop, collection, cb);
- });
-
- });
+ const res = await model.diffIndexes();
+ return await _dropIndexes(res.toDrop, model, options);
};
-function _dropIndexes(toDrop, collection, cb) {
+async function _dropIndexes(toDrop, model, options) {
if (toDrop.length === 0) {
- return cb(null, []);
+ return [];
}
- let remaining = toDrop.length;
- let error = false;
- toDrop.forEach(indexName => {
- collection.dropIndex(indexName, err => {
- if (err != null) {
- error = true;
- return cb(err);
- }
- if (!error) {
- --remaining || cb(null, toDrop);
- }
- });
- });
+ const collection = model.$__collection;
+ if (options && options.hideIndexes) {
+ await Promise.all(toDrop.map(indexName => {
+ return model.db.db.command({
+ collMod: collection.collectionName,
+ index: { name: indexName, hidden: true }
+ });
+ }));
+ } else {
+ await Promise.all(toDrop.map(indexName => collection.dropIndex(indexName)));
+ }
+
+ return toDrop;
}
/**
* Lists the indexes currently defined in MongoDB. This may or may not be
* the same as the indexes defined in your schema depending on whether you
- * use the [`autoIndex` option](/docs/guide.html#autoIndex) and if you
+ * use the [`autoIndex` option](https://mongoosejs.com/docs/guide.html#autoIndex) and if you
* build indexes manually.
*
- * @param {Function} [cb] optional callback
- * @return {Promise|undefined} Returns `undefined` if callback is specified, returns a promise if no callback.
+ * @return {Promise}
* @api public
*/
-Model.listIndexes = function init(callback) {
+Model.listIndexes = async function listIndexes() {
_checkContext(this, 'listIndexes');
+ if (typeof arguments[0] === 'function') {
+ throw new MongooseError('Model.listIndexes() no longer accepts a callback');
+ }
- const _listIndexes = cb => {
- this.$__collection.listIndexes().toArray(cb);
- };
-
- callback = this.$handleCallbackError(callback);
-
- return this.db.base._promiseOrCallback(callback, cb => {
- cb = this.$wrapCallback(cb);
+ if (this.$__collection.buffer) {
+ await new Promise(resolve => {
+ this.$__collection.addQueue(resolve);
+ });
+ }
- // Buffering
- if (this.$__collection.buffer) {
- this.$__collection.addQueue(_listIndexes, [cb]);
- } else {
- _listIndexes(cb);
- }
- }, this.events);
+ return this.$__collection.listIndexes().toArray();
};
/**
@@ -1748,9 +1536,7 @@ Model.listIndexes = function init(callback) {
*
* #### Example:
*
- * Event.ensureIndexes(function (err) {
- * if (err) return handleError(err);
- * });
+ * await Event.ensureIndexes();
*
* After completion, an `index` event is emitted on this `Model` passing an error if one occurred.
*
@@ -1761,36 +1547,29 @@ Model.listIndexes = function init(callback) {
*
* Event.on('index', function (err) {
* if (err) console.error(err); // error occurred during index creation
- * })
+ * });
*
* _NOTE: It is not recommended that you run this in production. Index creation may impact database performance depending on your load. Use with caution._
*
* @param {Object} [options] internal options
- * @param {Function} [cb] optional callback
* @return {Promise}
* @api public
*/
-Model.ensureIndexes = function ensureIndexes(options, callback) {
+Model.ensureIndexes = async function ensureIndexes(options) {
_checkContext(this, 'ensureIndexes');
-
- if (typeof options === 'function') {
- callback = options;
- options = null;
+ if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function') {
+ throw new MongooseError('Model.ensureIndexes() no longer accepts a callback');
}
- callback = this.$handleCallbackError(callback);
-
- return this.db.base._promiseOrCallback(callback, cb => {
- cb = this.$wrapCallback(cb);
-
- _ensureIndexes(this, options || {}, error => {
- if (error) {
- return cb(error);
+ await new Promise((resolve, reject) => {
+ _ensureIndexes(this, options, (err) => {
+ if (err != null) {
+ return reject(err);
}
- cb(null);
+ resolve();
});
- }, this.events);
+ });
};
/**
@@ -1798,22 +1577,18 @@ Model.ensureIndexes = function ensureIndexes(options, callback) {
* function.
*
* @param {Object} [options] internal options
- * @param {Function} [cb] optional callback
* @return {Promise}
* @api public
*/
-Model.createIndexes = function createIndexes(options, callback) {
+Model.createIndexes = async function createIndexes(options) {
_checkContext(this, 'createIndexes');
- if (typeof options === 'function') {
- callback = options;
- options = {};
+ if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function') {
+ throw new MongooseError('Model.createIndexes() no longer accepts a callback');
}
- callback = this.$handleCallbackError(callback);
- options = options || {};
- return this.ensureIndexes(options, callback);
+ return this.ensureIndexes(options);
};
@@ -1892,8 +1667,8 @@ function _ensureIndexes(model, options, callback) {
return create();
}
- const indexFields = utils.clone(index[0]);
- const indexOptions = utils.clone(index[1]);
+ const indexFields = clone(index[0]);
+ const indexOptions = clone(index[1]);
delete indexOptions._autoIndex;
decorateDiscriminatorIndexOptions(model.schema, indexOptions);
@@ -1912,18 +1687,40 @@ function _ensureIndexes(model, options, callback) {
}
}
- model.collection.createIndex(indexFields, indexOptions, utils.tick(function(err, name) {
- indexSingleDone(err, indexFields, indexOptions, name);
- if (err) {
+ // Just in case `createIndex()` throws a sync error
+ let promise = null;
+ try {
+ promise = model.collection.createIndex(indexFields, indexOptions);
+ } catch (err) {
+ if (!indexError) {
+ indexError = err;
+ }
+ if (!model.$caught) {
+ model.emit('error', err);
+ }
+
+ indexSingleDone(err, indexFields, indexOptions);
+ create();
+ return;
+ }
+
+ promise.then(
+ name => {
+ indexSingleDone(null, indexFields, indexOptions, name);
+ create();
+ },
+ err => {
if (!indexError) {
indexError = err;
}
if (!model.$caught) {
model.emit('error', err);
}
+
+ indexSingleDone(err, indexFields, indexOptions);
+ create();
}
- create();
- }));
+ );
}
}
@@ -1993,20 +1790,24 @@ Model.discriminators;
*
* #### Example:
*
- * Character
- * .find(Character.translateAliases({
- * '名': 'Eddard Stark' // Alias for 'name'
- * })
- * .exec(function(err, characters) {})
+ * await Character.find(Character.translateAliases({
+ * '名': 'Eddard Stark' // Alias for 'name'
+ * });
+ *
+ * By default, `translateAliases()` overwrites raw fields with aliased fields.
+ * So if `n` is an alias for `name`, `{ n: 'alias', name: 'raw' }` will resolve to `{ name: 'alias' }`.
+ * However, you can set the `errorOnDuplicates` option to throw an error if there are potentially conflicting paths.
+ * The `translateAliases` option for queries uses `errorOnDuplicates`.
*
* #### Note:
*
* Only translate arguments of object type anything else is returned raw
*
* @param {Object} fields fields/conditions that may contain aliased keys
+ * @param {Boolean} [errorOnDuplicates] if true, throw an error if there's both a key and an alias for that key in `fields`
* @return {Object} the translated 'pure' fields/conditions
*/
-Model.translateAliases = function translateAliases(fields) {
+Model.translateAliases = function translateAliases(fields, errorOnDuplicates) {
_checkContext(this, 'translateAliases');
const translate = (key, value) => {
@@ -2018,6 +1819,9 @@ Model.translateAliases = function translateAliases(fields) {
const name = fieldKeys[i];
if (currentSchema && currentSchema.aliases[name]) {
alias = currentSchema.aliases[name];
+ if (errorOnDuplicates && alias in fields) {
+ throw new MongooseError(`Provided object has both field "${name}" and its alias "${alias}"`);
+ }
// Alias found,
translated.push(alias);
} else {
@@ -2070,6 +1874,8 @@ Model.translateAliases = function translateAliases(fields) {
// Recursively translate nested queries
fields[key][i] = this.translateAliases(fields[key][i]);
}
+ } else {
+ this.translateAliases(fields[key]);
}
}
}
@@ -2082,59 +1888,9 @@ Model.translateAliases = function translateAliases(fields) {
}
};
-/**
- * Removes all documents that match `conditions` from the collection.
- * To remove just the first document that matches `conditions`, set the `single`
- * option to true.
- *
- * This method is deprecated. See [Deprecation Warnings](../deprecations.html#remove) for details.
- *
- * #### Example:
- *
- * const res = await Character.remove({ name: 'Eddard Stark' });
- * res.deletedCount; // Number of documents removed
- *
- * #### Note:
- *
- * This method sends a remove command directly to MongoDB, no Mongoose documents
- * are involved. Because no Mongoose documents are involved, Mongoose does
- * not execute [document middleware](/docs/middleware.html#types-of-middleware).
- *
- * @deprecated
- * @param {Object} conditions
- * @param {Object} [options]
- * @param {Session} [options.session=null] the [session](https://www.mongodb.com/docs/manual/reference/server-sessions/) associated with this operation.
- * @param {Function} [callback]
- * @return {Query}
- * @api public
- */
-
-Model.remove = function remove(conditions, options, callback) {
- _checkContext(this, 'remove');
-
- if (typeof conditions === 'function') {
- callback = conditions;
- conditions = {};
- options = null;
- } else if (typeof options === 'function') {
- callback = options;
- options = null;
- }
-
- // get the mongodb collection object
- const mq = new this.Query({}, {}, this, this.$__collection);
- mq.setOptions(options);
-
- callback = this.$handleCallbackError(callback);
-
- return mq.remove(conditions, callback);
-};
-
/**
* Deletes the first document that matches `conditions` from the collection.
* It returns an object with the property `deletedCount` indicating how many documents were deleted.
- * Behaves like `remove()`, but deletes at most one document regardless of the
- * `single` option.
*
* #### Example:
*
@@ -2143,41 +1899,31 @@ Model.remove = function remove(conditions, options, callback) {
* #### Note:
*
* This function triggers `deleteOne` query hooks. Read the
- * [middleware docs](/docs/middleware.html#naming) to learn more.
+ * [middleware docs](https://mongoosejs.com/docs/middleware.html#naming) to learn more.
*
* @param {Object} conditions
- * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#query_Query-setOptions)
- * @param {Function} [callback]
+ * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions())
+ * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object.
* @return {Query}
* @api public
*/
-Model.deleteOne = function deleteOne(conditions, options, callback) {
+Model.deleteOne = function deleteOne(conditions, options) {
_checkContext(this, 'deleteOne');
- if (typeof conditions === 'function') {
- callback = conditions;
- conditions = {};
- options = null;
- }
- else if (typeof options === 'function') {
- callback = options;
- options = null;
+ if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function' || typeof arguments[2] === 'function') {
+ throw new MongooseError('Model.prototype.deleteOne() no longer accepts a callback');
}
const mq = new this.Query({}, {}, this, this.$__collection);
mq.setOptions(options);
- callback = this.$handleCallbackError(callback);
-
- return mq.deleteOne(conditions, callback);
+ return mq.deleteOne(conditions);
};
/**
* Deletes all of the documents that match `conditions` from the collection.
* It returns an object with the property `deletedCount` containing the number of documents deleted.
- * Behaves like `remove()`, but deletes all documents that match `conditions`
- * regardless of the `single` option.
*
* #### Example:
*
@@ -2186,40 +1932,33 @@ Model.deleteOne = function deleteOne(conditions, options, callback) {
* #### Note:
*
* This function triggers `deleteMany` query hooks. Read the
- * [middleware docs](/docs/middleware.html#naming) to learn more.
+ * [middleware docs](https://mongoosejs.com/docs/middleware.html#naming) to learn more.
*
* @param {Object} conditions
- * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#query_Query-setOptions)
- * @param {Function} [callback]
+ * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions())
+ * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object.
* @return {Query}
* @api public
*/
-Model.deleteMany = function deleteMany(conditions, options, callback) {
+Model.deleteMany = function deleteMany(conditions, options) {
_checkContext(this, 'deleteMany');
- if (typeof conditions === 'function') {
- callback = conditions;
- conditions = {};
- options = null;
- } else if (typeof options === 'function') {
- callback = options;
- options = null;
+ if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function' || typeof arguments[2] === 'function') {
+ throw new MongooseError('Model.deleteMany() no longer accepts a callback');
}
const mq = new this.Query({}, {}, this, this.$__collection);
mq.setOptions(options);
- callback = this.$handleCallbackError(callback);
-
- return mq.deleteMany(conditions, callback);
+ return mq.deleteMany(conditions);
};
/**
* Finds documents.
*
* Mongoose casts the `filter` to match the model's schema before the command is sent.
- * See our [query casting tutorial](/docs/tutorials/query_casting.html) for
+ * See our [query casting tutorial](https://mongoosejs.com/docs/tutorials/query_casting.html) for
* more information on how Mongoose casts `filter`.
*
* #### Example:
@@ -2230,9 +1969,6 @@ Model.deleteMany = function deleteMany(conditions, options, callback) {
* // find all documents named john and at least 18
* await MyModel.find({ name: 'john', age: { $gte: 18 } }).exec();
*
- * // executes, passing results to callback
- * MyModel.find({ name: 'john', age: { $gte: 18 }}, function (err, docs) {});
- *
* // executes, name LIKE john and only selecting the "name" and "friends" fields
* await MyModel.find({ name: /john/i }, 'name friends').exec();
*
@@ -2240,39 +1976,26 @@ Model.deleteMany = function deleteMany(conditions, options, callback) {
* await MyModel.find({ name: /john/i }, null, { skip: 10 }).exec();
*
* @param {Object|ObjectId} filter
- * @param {Object|String|String[]} [projection] optional fields to return, see [`Query.prototype.select()`](https://mongoosejs.com/docs/api/query.html#query_Query-select)
- * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#query_Query-setOptions)
- * @param {Function} [callback]
+ * @param {Object|String|String[]} [projection] optional fields to return, see [`Query.prototype.select()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.select())
+ * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions())
+ * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object.
* @return {Query}
- * @see field selection #query_Query-select
- * @see query casting /docs/tutorials/query_casting.html
+ * @see field selection https://mongoosejs.com/docs/api/query.html#Query.prototype.select()
+ * @see query casting https://mongoosejs.com/docs/tutorials/query_casting.html
* @api public
*/
-Model.find = function find(conditions, projection, options, callback) {
+Model.find = function find(conditions, projection, options) {
_checkContext(this, 'find');
-
- if (typeof conditions === 'function') {
- callback = conditions;
- conditions = {};
- projection = null;
- options = null;
- } else if (typeof projection === 'function') {
- callback = projection;
- projection = null;
- options = null;
- } else if (typeof options === 'function') {
- callback = options;
- options = null;
+ if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function' || typeof arguments[2] === 'function' || typeof arguments[3] === 'function') {
+ throw new MongooseError('Model.find() no longer accepts a callback');
}
const mq = new this.Query({}, {}, this, this.$__collection);
mq.select(projection);
mq.setOptions(options);
- callback = this.$handleCallbackError(callback);
-
- return mq.find(conditions, callback);
+ return mq.find(conditions);
};
/**
@@ -2296,33 +2019,30 @@ Model.find = function find(conditions, projection, options, callback) {
* // Find the adventure with the given `id`, or `null` if not found
* await Adventure.findById(id).exec();
*
- * // using callback
- * Adventure.findById(id, function (err, adventure) {});
- *
* // select only the adventures name and length
* await Adventure.findById(id, 'name length').exec();
*
* @param {Any} id value of `_id` to query by
- * @param {Object|String|String[]} [projection] optional fields to return, see [`Query.prototype.select()`](#query_Query-select)
- * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#query_Query-setOptions)
- * @param {Function} [callback]
+ * @param {Object|String|String[]} [projection] optional fields to return, see [`Query.prototype.select()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.select())
+ * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions())
* @return {Query}
- * @see field selection #query_Query-select
- * @see lean queries /docs/tutorials/lean.html
+ * @see field selection https://mongoosejs.com/docs/api/query.html#Query.prototype.select()
+ * @see lean queries https://mongoosejs.com/docs/tutorials/lean.html
* @see findById in Mongoose https://masteringjs.io/tutorials/mongoose/find-by-id
* @api public
*/
-Model.findById = function findById(id, projection, options, callback) {
+Model.findById = function findById(id, projection, options) {
_checkContext(this, 'findById');
+ if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function' || typeof arguments[2] === 'function') {
+ throw new MongooseError('Model.findById() no longer accepts a callback');
+ }
if (typeof id === 'undefined') {
id = null;
}
- callback = this.$handleCallbackError(callback);
-
- return this.findOne({ _id: id }, projection, options, callback);
+ return this.findOne({ _id: id }, projection, options);
};
/**
@@ -2339,44 +2059,32 @@ Model.findById = function findById(id, projection, options, callback) {
* // Find one adventure whose `country` is 'Croatia', otherwise `null`
* await Adventure.findOne({ country: 'Croatia' }).exec();
*
- * // using callback
- * Adventure.findOne({ country: 'Croatia' }, function (err, adventure) {});
+ * // Model.findOne() no longer accepts a callback
*
- * // select only the adventures name and length
+ * // Select only the adventures name and length
* await Adventure.findOne({ country: 'Croatia' }, 'name length').exec();
*
* @param {Object} [conditions]
- * @param {Object|String|String[]} [projection] optional fields to return, see [`Query.prototype.select()`](#query_Query-select)
- * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#query_Query-setOptions)
- * @param {Function} [callback]
+ * @param {Object|String|String[]} [projection] optional fields to return, see [`Query.prototype.select()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.select())
+ * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions())
+ * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object.
* @return {Query}
- * @see field selection #query_Query-select
- * @see lean queries /docs/tutorials/lean.html
+ * @see field selection https://mongoosejs.com/docs/api/query.html#Query.prototype.select()
+ * @see lean queries https://mongoosejs.com/docs/tutorials/lean.html
* @api public
*/
-Model.findOne = function findOne(conditions, projection, options, callback) {
+Model.findOne = function findOne(conditions, projection, options) {
_checkContext(this, 'findOne');
- if (typeof options === 'function') {
- callback = options;
- options = null;
- } else if (typeof projection === 'function') {
- callback = projection;
- projection = null;
- options = null;
- } else if (typeof conditions === 'function') {
- callback = conditions;
- conditions = {};
- projection = null;
- options = null;
+ if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function' || typeof arguments[2] === 'function') {
+ throw new MongooseError('Model.findOne() no longer accepts a callback');
}
const mq = new this.Query({}, {}, this, this.$__collection);
mq.select(projection);
mq.setOptions(options);
- callback = this.$handleCallbackError(callback);
- return mq.findOne(conditions, callback);
+ return mq.findOne(conditions);
};
/**
@@ -2390,19 +2098,16 @@ Model.findOne = function findOne(conditions, projection, options, callback) {
* const numAdventures = await Adventure.estimatedDocumentCount();
*
* @param {Object} [options]
- * @param {Function} [callback]
* @return {Query}
* @api public
*/
-Model.estimatedDocumentCount = function estimatedDocumentCount(options, callback) {
+Model.estimatedDocumentCount = function estimatedDocumentCount(options) {
_checkContext(this, 'estimatedDocumentCount');
const mq = new this.Query({}, {}, this, this.$__collection);
- callback = this.$handleCallbackError(callback);
-
- return mq.estimatedDocumentCount(options, callback);
+ return mq.estimatedDocumentCount(options);
};
/**
@@ -2415,7 +2120,7 @@ Model.estimatedDocumentCount = function estimatedDocumentCount(options, callback
* });
*
* If you want to count all documents in a large collection,
- * use the [`estimatedDocumentCount()` function](#model_Model-estimatedDocumentCount)
+ * use the [`estimatedDocumentCount()` function](https://mongoosejs.com/docs/api/model.html#Model.estimatedDocumentCount())
* instead. If you call `countDocuments({})`, MongoDB will always execute
* a full collection scan and **not** use any indexes.
*
@@ -2429,21 +2134,14 @@ Model.estimatedDocumentCount = function estimatedDocumentCount(options, callback
* - `$nearSphere`: [`$geoWithin`](https://www.mongodb.com/docs/manual/reference/operator/query/geoWithin/) with [`$centerSphere`](https://www.mongodb.com/docs/manual/reference/operator/query/centerSphere/#op._S_centerSphere)
*
* @param {Object} filter
- * @param {Function} [callback]
* @return {Query}
* @api public
*/
-Model.countDocuments = function countDocuments(conditions, options, callback) {
+Model.countDocuments = function countDocuments(conditions, options) {
_checkContext(this, 'countDocuments');
-
- if (typeof conditions === 'function') {
- callback = conditions;
- conditions = {};
- }
- if (typeof options === 'function') {
- callback = options;
- options = null;
+ if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function' || typeof arguments[2] === 'function') {
+ throw new MongooseError('Model.countDocuments() no longer accepts a callback');
}
const mq = new this.Query({}, {}, this, this.$__collection);
@@ -2451,81 +2149,37 @@ Model.countDocuments = function countDocuments(conditions, options, callback) {
mq.setOptions(options);
}
- callback = this.$handleCallbackError(callback);
-
- return mq.countDocuments(conditions, callback);
+ return mq.countDocuments(conditions);
};
-/**
- * Counts number of documents that match `filter` in a database collection.
- *
- * This method is deprecated. If you want to count the number of documents in
- * a collection, e.g. `count({})`, use the [`estimatedDocumentCount()` function](#model_Model-estimatedDocumentCount)
- * instead. Otherwise, use the [`countDocuments()`](#model_Model-countDocuments) function instead.
- *
- * #### Example:
- *
- * const count = await Adventure.count({ type: 'jungle' });
- * console.log('there are %d jungle adventures', count);
- *
- * @deprecated
- * @param {Object} filter
- * @param {Function} [callback]
- * @return {Query}
- * @api public
- */
-
-Model.count = function count(conditions, callback) {
- _checkContext(this, 'count');
-
- if (typeof conditions === 'function') {
- callback = conditions;
- conditions = {};
- }
-
- const mq = new this.Query({}, {}, this, this.$__collection);
-
- callback = this.$handleCallbackError(callback);
-
- return mq.count(conditions, callback);
-};
/**
* Creates a Query for a `distinct` operation.
*
- * Passing a `callback` executes the query.
- *
* #### Example:
*
- * Link.distinct('url', { clicks: { $gt: 100 } }, function (err, result) {
- * if (err) return handleError(err);
- *
- * assert(Array.isArray(result));
- * console.log('unique urls with more than 100 clicks', result);
- * })
- *
* const query = Link.distinct('url');
- * query.exec(callback);
+ * query.exec();
*
* @param {String} field
* @param {Object} [conditions] optional
- * @param {Function} [callback]
+ * @param {Object} [options] optional
* @return {Query}
* @api public
*/
-Model.distinct = function distinct(field, conditions, callback) {
+Model.distinct = function distinct(field, conditions, options) {
_checkContext(this, 'distinct');
+ if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function' || typeof arguments[2] === 'function') {
+ throw new MongooseError('Model.distinct() no longer accepts a callback');
+ }
const mq = new this.Query({}, {}, this, this.$__collection);
-
- if (typeof conditions === 'function') {
- callback = conditions;
- conditions = {};
+ if (options != null) {
+ mq.setOptions(options);
}
- callback = this.$handleCallbackError(callback);
- return mq.distinct(field, conditions, callback);
+ return mq.distinct(field, conditions);
};
/**
@@ -2533,11 +2187,11 @@ Model.distinct = function distinct(field, conditions, callback) {
*
* For example, instead of writing:
*
- * User.find({ age: { $gte: 21, $lte: 65 } }, callback);
+ * User.find({ age: { $gte: 21, $lte: 65 } });
*
* we can instead write:
*
- * User.where('age').gte(21).lte(65).exec(callback);
+ * User.where('age').gte(21).lte(65).exec();
*
* Since the Query class also supports `where` you can continue chaining
*
@@ -2571,7 +2225,7 @@ Model.where = function where(path, val) {
* @method $where
* @memberOf Model
* @return {Query}
- * @see Query.$where #query_Query-%24where
+ * @see Query.$where https://mongoosejs.com/docs/api/query.html#Query.prototype.$where
* @api public
*/
@@ -2583,15 +2237,13 @@ Model.$where = function $where() {
};
/**
- * Issues a mongodb findAndModify update command.
+ * Issues a mongodb findOneAndUpdate command.
*
- * Finds a matching document, updates it according to the `update` arg, passing any `options`, and returns the found document (if any) to the callback. The query executes if `callback` is passed else a Query object is returned.
+ * Finds a matching document, updates it according to the `update` arg, passing any `options`. A Query object is returned.
*
* #### Example:
*
- * A.findOneAndUpdate(conditions, update, options, callback) // executes
* A.findOneAndUpdate(conditions, update, options) // returns Query
- * A.findOneAndUpdate(conditions, update, callback) // executes
* A.findOneAndUpdate(conditions, update) // returns Query
* A.findOneAndUpdate() // returns Query
*
@@ -2602,13 +2254,10 @@ Model.$where = function $where() {
* #### Example:
*
* const query = { name: 'borne' };
- * Model.findOneAndUpdate(query, { name: 'jason bourne' }, options, callback)
+ * Model.findOneAndUpdate(query, { name: 'jason bourne' }, options)
*
* // is sent as
- * Model.findOneAndUpdate(query, { $set: { name: 'jason bourne' }}, options, callback)
- *
- * This helps prevent accidentally overwriting your document with `{ name: 'jason bourne' }`.
- * To prevent this behaviour, see the `overwrite` option
+ * Model.findOneAndUpdate(query, { $set: { name: 'jason bourne' }}, options)
*
* #### Note:
*
@@ -2624,98 +2273,65 @@ Model.$where = function $where() {
*
* @param {Object} [conditions]
* @param {Object} [update]
- * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#query_Query-setOptions)
+ * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions())
* @param {String} [options.returnDocument='before'] Has two possible values, `'before'` and `'after'`. By default, it will return the document before the update was applied.
- * @param {Object} [options.lean] if truthy, mongoose will return the document as a plain JavaScript object rather than a mongoose document. See [`Query.lean()`](/docs/api/query.html#query_Query-lean) and [the Mongoose lean tutorial](/docs/tutorials/lean.html).
- * @param {ClientSession} [options.session=null] The session associated with this query. See [transactions docs](/docs/transactions.html).
+ * @param {Object} [options.lean] if truthy, mongoose will return the document as a plain JavaScript object rather than a mongoose document. See [`Query.lean()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.lean()) and [the Mongoose lean tutorial](https://mongoosejs.com/docs/tutorials/lean.html).
+ * @param {ClientSession} [options.session=null] The session associated with this query. See [transactions docs](https://mongoosejs.com/docs/transactions.html).
* @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict)
- * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Note that this allows you to overwrite timestamps. Does nothing if schema-level timestamps are not set.
- * @param {Boolean} [options.overwrite=false] By default, if you don't include any [update operators](https://www.mongodb.com/docs/manual/reference/operator/update/) in `update`, Mongoose will wrap `update` in `$set` for you. This prevents you from accidentally overwriting the document. This option tells Mongoose to skip adding `$set`. An alternative to this would be using [Model.findOneAndReplace(conditions, update, options, callback)](#model_Model-findOneAndReplace).
+ * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Note that this allows you to overwrite timestamps. Does nothing if schema-level timestamps are not set.
* @param {Boolean} [options.upsert=false] if true, and no documents found, insert a new document
- * @param {Object|String|String[]} [options.projection=null] optional fields to return, see [`Query.prototype.select()`](#query_Query-select)
+ * @param {Object|String|String[]} [options.projection=null] optional fields to return, see [`Query.prototype.select()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.select())
* @param {Boolean} [options.new=false] if true, return the modified document rather than the original
* @param {Object|String} [options.fields] Field selection. Equivalent to `.select(fields).findOneAndUpdate()`
* @param {Number} [options.maxTimeMS] puts a time limit on the query - requires mongodb >= 2.6.0
* @param {Object|String} [options.sort] if multiple docs are found by the conditions, sets the sort order to choose which doc to update.
- * @param {Boolean} [options.runValidators] if true, runs [update validators](/docs/validation.html#update-validators) on this command. Update validators validate the update operation against the model's schema
+ * @param {Boolean} [options.runValidators] if true, runs [update validators](https://mongoosejs.com/docs/validation.html#update-validators) on this command. Update validators validate the update operation against the model's schema
* @param {Boolean} [options.setDefaultsOnInsert=true] If `setDefaultsOnInsert` and `upsert` are true, mongoose will apply the [defaults](https://mongoosejs.com/docs/defaults.html) specified in the model's schema if a new document is created
- * @param {Boolean} [options.rawResult] if true, returns the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/ModifyResult.html)
- * @param {Function} [callback]
+ * @param {Boolean} [options.includeResultMetadata] if true, returns the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/ModifyResult.html)
+ * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object.
+ * @param {Boolean} [options.overwriteDiscriminatorKey=false] Mongoose removes discriminator key updates from `update` by default, set `overwriteDiscriminatorKey` to `true` to allow updating the discriminator key
* @return {Query}
- * @see Tutorial /docs/tutorials/findoneandupdate.html
+ * @see Tutorial https://mongoosejs.com/docs/tutorials/findoneandupdate.html
* @see mongodb https://www.mongodb.com/docs/manual/reference/command/findAndModify/
* @api public
*/
-Model.findOneAndUpdate = function(conditions, update, options, callback) {
+Model.findOneAndUpdate = function(conditions, update, options) {
_checkContext(this, 'findOneAndUpdate');
+ if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function' || typeof arguments[2] === 'function' || typeof arguments[3] === 'function') {
+ throw new MongooseError('Model.findOneAndUpdate() no longer accepts a callback');
+ }
- if (typeof options === 'function') {
- callback = options;
- options = null;
- } else if (arguments.length === 1) {
- if (typeof conditions === 'function') {
- const msg = 'Model.findOneAndUpdate(): First argument must not be a function.\n\n'
- + ' ' + this.modelName + '.findOneAndUpdate(conditions, update, options, callback)\n'
- + ' ' + this.modelName + '.findOneAndUpdate(conditions, update, options)\n'
- + ' ' + this.modelName + '.findOneAndUpdate(conditions, update)\n'
- + ' ' + this.modelName + '.findOneAndUpdate(update)\n'
- + ' ' + this.modelName + '.findOneAndUpdate()\n';
- throw new TypeError(msg);
- }
+ if (arguments.length === 1) {
update = conditions;
- conditions = undefined;
+ conditions = null;
+ options = null;
}
- callback = this.$handleCallbackError(callback);
let fields;
if (options) {
fields = options.fields || options.projection;
}
- update = utils.clone(update, {
+ update = clone(update, {
depopulate: true,
_isNested: true
});
- _decorateUpdateWithVersionKey(update, options, this.schema.options.versionKey);
+ decorateUpdateWithVersionKey(update, options, this.schema.options.versionKey);
const mq = new this.Query({}, {}, this, this.$__collection);
mq.select(fields);
- return mq.findOneAndUpdate(conditions, update, options, callback);
+ return mq.findOneAndUpdate(conditions, update, options);
};
/**
- * Decorate the update with a version key, if necessary
- * @api private
- */
-
-function _decorateUpdateWithVersionKey(update, options, versionKey) {
- if (!versionKey || !(options && options.upsert || false)) {
- return;
- }
-
- const updatedPaths = modifiedPaths(update);
- if (!updatedPaths[versionKey]) {
- if (options.overwrite) {
- update[versionKey] = 0;
- } else {
- if (!update.$setOnInsert) {
- update.$setOnInsert = {};
- }
- update.$setOnInsert[versionKey] = 0;
- }
- }
-}
-
-/**
- * Issues a mongodb findAndModify update command by a document's _id field.
+ * Issues a mongodb findOneAndUpdate command by a document's _id field.
* `findByIdAndUpdate(id, ...)` is equivalent to `findOneAndUpdate({ _id: id }, ...)`.
*
* Finds a matching document, updates it according to the `update` arg,
- * passing any `options`, and returns the found document (if any) to the
- * callback. The query executes if `callback` is passed.
+ * passing any `options`, and returns the found document (if any).
*
* This function triggers the following middleware.
*
@@ -2723,9 +2339,7 @@ function _decorateUpdateWithVersionKey(update, options, versionKey) {
*
* #### Example:
*
- * A.findByIdAndUpdate(id, update, options, callback) // executes
* A.findByIdAndUpdate(id, update, options) // returns Query
- * A.findByIdAndUpdate(id, update, callback) // executes
* A.findByIdAndUpdate(id, update) // returns Query
* A.findByIdAndUpdate() // returns Query
*
@@ -2735,13 +2349,10 @@ function _decorateUpdateWithVersionKey(update, options, versionKey) {
*
* #### Example:
*
- * Model.findByIdAndUpdate(id, { name: 'jason bourne' }, options, callback)
+ * Model.findByIdAndUpdate(id, { name: 'jason bourne' }, options)
*
* // is sent as
- * Model.findByIdAndUpdate(id, { $set: { name: 'jason bourne' }}, options, callback)
- *
- * This helps prevent accidentally overwriting your document with `{ name: 'jason bourne' }`.
- * To prevent this behaviour, see the `overwrite` option
+ * Model.findByIdAndUpdate(id, { $set: { name: 'jason bourne' }}, options)
*
* #### Note:
*
@@ -2757,73 +2368,53 @@ function _decorateUpdateWithVersionKey(update, options, versionKey) {
*
* @param {Object|Number|String} id value of `_id` to query by
* @param {Object} [update]
- * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#query_Query-setOptions)
+ * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions())
* @param {String} [options.returnDocument='before'] Has two possible values, `'before'` and `'after'`. By default, it will return the document before the update was applied.
- * @param {Object} [options.lean] if truthy, mongoose will return the document as a plain JavaScript object rather than a mongoose document. See [`Query.lean()`](/docs/api/query.html#query_Query-lean) and [the Mongoose lean tutorial](/docs/tutorials/lean.html).
- * @param {ClientSession} [options.session=null] The session associated with this query. See [transactions docs](/docs/transactions.html).
+ * @param {Object} [options.lean] if truthy, mongoose will return the document as a plain JavaScript object rather than a mongoose document. See [`Query.lean()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.lean()) and [the Mongoose lean tutorial](https://mongoosejs.com/docs/tutorials/lean.html).
+ * @param {ClientSession} [options.session=null] The session associated with this query. See [transactions docs](https://mongoosejs.com/docs/transactions.html).
* @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict)
- * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Note that this allows you to overwrite timestamps. Does nothing if schema-level timestamps are not set.
- * @param {Boolean} [options.overwrite=false] By default, if you don't include any [update operators](https://www.mongodb.com/docs/manual/reference/operator/update/) in `update`, Mongoose will wrap `update` in `$set` for you. This prevents you from accidentally overwriting the document. This option tells Mongoose to skip adding `$set`. An alternative to this would be using [Model.findOneAndReplace({ _id: id }, update, options, callback)](#model_Model-findOneAndReplace).
+ * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Note that this allows you to overwrite timestamps. Does nothing if schema-level timestamps are not set.
* @param {Object|String} [options.sort] if multiple docs are found by the conditions, sets the sort order to choose which doc to update.
- * @param {Boolean} [options.runValidators] if true, runs [update validators](/docs/validation.html#update-validators) on this command. Update validators validate the update operation against the model's schema
+ * @param {Boolean} [options.runValidators] if true, runs [update validators](https://mongoosejs.com/docs/validation.html#update-validators) on this command. Update validators validate the update operation against the model's schema
* @param {Boolean} [options.setDefaultsOnInsert=true] If `setDefaultsOnInsert` and `upsert` are true, mongoose will apply the [defaults](https://mongoosejs.com/docs/defaults.html) specified in the model's schema if a new document is created
- * @param {Boolean} [options.rawResult] if true, returns the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/ModifyResult.html)
+ * @param {Boolean} [options.includeResultMetadata] if true, returns the full [ModifyResult from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/ModifyResult.html) rather than just the document
* @param {Boolean} [options.upsert=false] if true, and no documents found, insert a new document
* @param {Boolean} [options.new=false] if true, return the modified document rather than the original
* @param {Object|String} [options.select] sets the document fields to return.
- * @param {Function} [callback]
+ * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object.
+ * @param {Boolean} [options.overwriteDiscriminatorKey=false] Mongoose removes discriminator key updates from `update` by default, set `overwriteDiscriminatorKey` to `true` to allow updating the discriminator key
* @return {Query}
- * @see Model.findOneAndUpdate #model_Model-findOneAndUpdate
+ * @see Model.findOneAndUpdate https://mongoosejs.com/docs/api/model.html#Model.findOneAndUpdate()
* @see mongodb https://www.mongodb.com/docs/manual/reference/command/findAndModify/
* @api public
*/
-Model.findByIdAndUpdate = function(id, update, options, callback) {
+Model.findByIdAndUpdate = function(id, update, options) {
_checkContext(this, 'findByIdAndUpdate');
-
- callback = this.$handleCallbackError(callback);
- if (arguments.length === 1) {
- if (typeof id === 'function') {
- const msg = 'Model.findByIdAndUpdate(): First argument must not be a function.\n\n'
- + ' ' + this.modelName + '.findByIdAndUpdate(id, callback)\n'
- + ' ' + this.modelName + '.findByIdAndUpdate(id)\n'
- + ' ' + this.modelName + '.findByIdAndUpdate()\n';
- throw new TypeError(msg);
- }
- return this.findOneAndUpdate({ _id: id }, undefined);
+ if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function' || typeof arguments[2] === 'function' || typeof arguments[3] === 'function') {
+ throw new MongooseError('Model.findByIdAndUpdate() no longer accepts a callback');
}
// if a model is passed in instead of an id
if (id instanceof Document) {
- id = id._id;
+ id = id._doc._id;
}
- return this.findOneAndUpdate.call(this, { _id: id }, update, options, callback);
+ return this.findOneAndUpdate.call(this, { _id: id }, update, options);
};
/**
* Issue a MongoDB `findOneAndDelete()` command.
*
- * Finds a matching document, removes it, and passes the found document
- * (if any) to the callback.
- *
- * Executes the query if `callback` is passed.
+ * Finds a matching document, removes it, and returns the found document (if any).
*
* This function triggers the following middleware.
*
* - `findOneAndDelete()`
*
- * This function differs slightly from `Model.findOneAndRemove()` in that
- * `findOneAndRemove()` becomes a [MongoDB `findAndModify()` command](https://www.mongodb.com/docs/manual/reference/method/db.collection.findAndModify/),
- * as opposed to a `findOneAndDelete()` command. For most mongoose use cases,
- * this distinction is purely pedantic. You should use `findOneAndDelete()`
- * unless you have a good reason not to.
- *
* #### Example:
*
- * A.findOneAndDelete(conditions, options, callback) // executes
* A.findOneAndDelete(conditions, options) // return Query
- * A.findOneAndDelete(conditions, callback) // executes
* A.findOneAndDelete(conditions) // returns Query
* A.findOneAndDelete() // returns Query
*
@@ -2838,36 +2429,26 @@ Model.findByIdAndUpdate = function(id, update, options, callback) {
* await doc.save();
*
* @param {Object} conditions
- * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#query_Query-setOptions)
+ * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions())
* @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict)
- * @param {Object|String|String[]} [options.projection=null] optional fields to return, see [`Query.prototype.select()`](#query_Query-select)
- * @param {ClientSession} [options.session=null] The session associated with this query. See [transactions docs](/docs/transactions.html).
- * @param {Boolean} [options.rawResult] if true, returns the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/ModifyResult.html)
+ * @param {Object|String|String[]} [options.projection=null] optional fields to return, see [`Query.prototype.select()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.select())
+ * @param {ClientSession} [options.session=null] The session associated with this query. See [transactions docs](https://mongoosejs.com/docs/transactions.html).
+ * @param {Boolean} [options.includeResultMetadata] if true, returns the full [ModifyResult from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/ModifyResult.html) rather than just the document
* @param {Object|String} [options.sort] if multiple docs are found by the conditions, sets the sort order to choose which doc to update.
* @param {Object|String} [options.select] sets the document fields to return.
* @param {Number} [options.maxTimeMS] puts a time limit on the query - requires mongodb >= 2.6.0
- * @param {Function} [callback]
+ * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object.
* @return {Query}
* @api public
*/
-Model.findOneAndDelete = function(conditions, options, callback) {
+Model.findOneAndDelete = function(conditions, options) {
_checkContext(this, 'findOneAndDelete');
- if (arguments.length === 1 && typeof conditions === 'function') {
- const msg = 'Model.findOneAndDelete(): First argument must not be a function.\n\n'
- + ' ' + this.modelName + '.findOneAndDelete(conditions, callback)\n'
- + ' ' + this.modelName + '.findOneAndDelete(conditions)\n'
- + ' ' + this.modelName + '.findOneAndDelete()\n';
- throw new TypeError(msg);
+ if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function' || typeof arguments[2] === 'function') {
+ throw new MongooseError('Model.findOneAndDelete() no longer accepts a callback');
}
- if (typeof options === 'function') {
- callback = options;
- options = undefined;
- }
- callback = this.$handleCallbackError(callback);
-
let fields;
if (options) {
fields = options.select;
@@ -2877,7 +2458,7 @@ Model.findOneAndDelete = function(conditions, options, callback) {
const mq = new this.Query({}, {}, this, this.$__collection);
mq.select(fields);
- return mq.findOneAndDelete(conditions, options, callback);
+ return mq.findOneAndDelete(conditions, options);
};
/**
@@ -2890,36 +2471,28 @@ Model.findOneAndDelete = function(conditions, options, callback) {
* - `findOneAndDelete()`
*
* @param {Object|Number|String} id value of `_id` to query by
- * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#query_Query-setOptions)
+ * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions())
* @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict)
- * @param {Function} [callback]
+ * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object.
* @return {Query}
- * @see Model.findOneAndRemove #model_Model-findOneAndRemove
+ * @see Model.findOneAndDelete https://mongoosejs.com/docs/api/model.html#Model.findOneAndDelete()
* @see mongodb https://www.mongodb.com/docs/manual/reference/command/findAndModify/
*/
-Model.findByIdAndDelete = function(id, options, callback) {
+Model.findByIdAndDelete = function(id, options) {
_checkContext(this, 'findByIdAndDelete');
- if (arguments.length === 1 && typeof id === 'function') {
- const msg = 'Model.findByIdAndDelete(): First argument must not be a function.\n\n'
- + ' ' + this.modelName + '.findByIdAndDelete(id, callback)\n'
- + ' ' + this.modelName + '.findByIdAndDelete(id)\n'
- + ' ' + this.modelName + '.findByIdAndDelete()\n';
- throw new TypeError(msg);
+ if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function' || typeof arguments[2] === 'function') {
+ throw new MongooseError('Model.findByIdAndDelete() no longer accepts a callback');
}
- callback = this.$handleCallbackError(callback);
- return this.findOneAndDelete({ _id: id }, options, callback);
+ return this.findOneAndDelete({ _id: id }, options);
};
/**
* Issue a MongoDB `findOneAndReplace()` command.
*
- * Finds a matching document, replaces it with the provided doc, and passes the
- * returned doc to the callback.
- *
- * Executes the query if `callback` is passed.
+ * Finds a matching document, replaces it with the provided doc, and returns the document.
*
* This function triggers the following query middleware.
*
@@ -2927,128 +2500,35 @@ Model.findByIdAndDelete = function(id, options, callback) {
*
* #### Example:
*
- * A.findOneAndReplace(filter, replacement, options, callback) // executes
* A.findOneAndReplace(filter, replacement, options) // return Query
- * A.findOneAndReplace(filter, replacement, callback) // executes
* A.findOneAndReplace(filter, replacement) // returns Query
* A.findOneAndReplace() // returns Query
*
* @param {Object} filter Replace the first document that matches this filter
* @param {Object} [replacement] Replace with this document
- * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#query_Query-setOptions)
+ * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions())
* @param {String} [options.returnDocument='before'] Has two possible values, `'before'` and `'after'`. By default, it will return the document before the update was applied.
- * @param {Object} [options.lean] if truthy, mongoose will return the document as a plain JavaScript object rather than a mongoose document. See [`Query.lean()`](/docs/api/query.html#query_Query-lean) and [the Mongoose lean tutorial](/docs/tutorials/lean.html).
- * @param {ClientSession} [options.session=null] The session associated with this query. See [transactions docs](/docs/transactions.html).
+ * @param {Object} [options.lean] if truthy, mongoose will return the document as a plain JavaScript object rather than a mongoose document. See [`Query.lean()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.lean()) and [the Mongoose lean tutorial](https://mongoosejs.com/docs/tutorials/lean.html).
+ * @param {ClientSession} [options.session=null] The session associated with this query. See [transactions docs](https://mongoosejs.com/docs/transactions.html).
* @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict)
- * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Note that this allows you to overwrite timestamps. Does nothing if schema-level timestamps are not set.
- * @param {Object|String|String[]} [options.projection=null] optional fields to return, see [`Query.prototype.select()`](#query_Query-select)
+ * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Note that this allows you to overwrite timestamps. Does nothing if schema-level timestamps are not set.
+ * @param {Object|String|String[]} [options.projection=null] optional fields to return, see [`Query.prototype.select()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.select())
* @param {Object|String} [options.sort] if multiple docs are found by the conditions, sets the sort order to choose which doc to update.
- * @param {Boolean} [options.rawResult] if true, returns the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/ModifyResult.html)
+ * @param {Boolean} [options.includeResultMetadata] if true, returns the full [ModifyResult from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/ModifyResult.html) rather than just the document
* @param {Object|String} [options.select] sets the document fields to return.
* @param {Number} [options.maxTimeMS] puts a time limit on the query - requires mongodb >= 2.6.0
- * @param {Function} [callback]
+ * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object.
* @return {Query}
* @api public
*/
-Model.findOneAndReplace = function(filter, replacement, options, callback) {
+Model.findOneAndReplace = function(filter, replacement, options) {
_checkContext(this, 'findOneAndReplace');
- if (arguments.length === 1 && typeof filter === 'function') {
- const msg = 'Model.findOneAndReplace(): First argument must not be a function.\n\n'
- + ' ' + this.modelName + '.findOneAndReplace(filter, replacement, options, callback)\n'
- + ' ' + this.modelName + '.findOneAndReplace(filter, replacement, callback)\n'
- + ' ' + this.modelName + '.findOneAndReplace(filter, replacement)\n'
- + ' ' + this.modelName + '.findOneAndReplace(filter, callback)\n'
- + ' ' + this.modelName + '.findOneAndReplace()\n';
- throw new TypeError(msg);
- }
-
- if (arguments.length === 3 && typeof options === 'function') {
- callback = options;
- options = replacement;
- replacement = void 0;
- }
- if (arguments.length === 2 && typeof replacement === 'function') {
- callback = replacement;
- replacement = void 0;
- options = void 0;
- }
- callback = this.$handleCallbackError(callback);
-
- let fields;
- if (options) {
- fields = options.select;
- options.select = undefined;
- }
-
- const mq = new this.Query({}, {}, this, this.$__collection);
- mq.select(fields);
-
- return mq.findOneAndReplace(filter, replacement, options, callback);
-};
-
-/**
- * Issue a mongodb findAndModify remove command.
- *
- * Finds a matching document, removes it, passing the found document (if any) to the callback.
- *
- * Executes the query if `callback` is passed.
- *
- * This function triggers the following middleware.
- *
- * - `findOneAndRemove()`
- *
- * #### Example:
- *
- * A.findOneAndRemove(conditions, options, callback) // executes
- * A.findOneAndRemove(conditions, options) // return Query
- * A.findOneAndRemove(conditions, callback) // executes
- * A.findOneAndRemove(conditions) // returns Query
- * A.findOneAndRemove() // returns Query
- *
- * `findOneAndX` and `findByIdAndX` functions support limited validation. You can
- * enable validation by setting the `runValidators` option.
- *
- * If you need full-fledged validation, use the traditional approach of first
- * retrieving the document.
- *
- * const doc = await Model.findById(id);
- * doc.name = 'jason bourne';
- * await doc.save();
- *
- * @param {Object} conditions
- * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#query_Query-setOptions)
- * @param {ClientSession} [options.session=null] The session associated with this query. See [transactions docs](/docs/transactions.html).
- * @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict)
- * @param {Object|String|String[]} [options.projection=null] optional fields to return, see [`Query.prototype.select()`](#query_Query-select)
- * @param {Object|String} [options.sort] if multiple docs are found by the conditions, sets the sort order to choose which doc to update.
- * @param {Boolean} [options.rawResult] if true, returns the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/ModifyResult.html)
- * @param {Object|String} [options.select] sets the document fields to return.
- * @param {Number} [options.maxTimeMS] puts a time limit on the query - requires mongodb >= 2.6.0
- * @param {Function} [callback]
- * @return {Query}
- * @see mongodb https://www.mongodb.com/docs/manual/reference/command/findAndModify/
- * @api public
- */
-
-Model.findOneAndRemove = function(conditions, options, callback) {
- _checkContext(this, 'findOneAndRemove');
-
- if (arguments.length === 1 && typeof conditions === 'function') {
- const msg = 'Model.findOneAndRemove(): First argument must not be a function.\n\n'
- + ' ' + this.modelName + '.findOneAndRemove(conditions, callback)\n'
- + ' ' + this.modelName + '.findOneAndRemove(conditions)\n'
- + ' ' + this.modelName + '.findOneAndRemove()\n';
- throw new TypeError(msg);
+ if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function' || typeof arguments[2] === 'function' || typeof arguments[3] === 'function') {
+ throw new MongooseError('Model.findOneAndReplace() no longer accepts a callback');
}
- if (typeof options === 'function') {
- callback = options;
- options = undefined;
- }
- callback = this.$handleCallbackError(callback);
-
let fields;
if (options) {
fields = options.select;
@@ -3058,55 +2538,7 @@ Model.findOneAndRemove = function(conditions, options, callback) {
const mq = new this.Query({}, {}, this, this.$__collection);
mq.select(fields);
- return mq.findOneAndRemove(conditions, options, callback);
-};
-
-/**
- * Issue a mongodb findAndModify remove command by a document's _id field. `findByIdAndRemove(id, ...)` is equivalent to `findOneAndRemove({ _id: id }, ...)`.
- *
- * Finds a matching document, removes it, passing the found document (if any) to the callback.
- *
- * Executes the query if `callback` is passed.
- *
- * This function triggers the following middleware.
- *
- * - `findOneAndRemove()`
- *
- * #### Example:
- *
- * A.findByIdAndRemove(id, options, callback) // executes
- * A.findByIdAndRemove(id, options) // return Query
- * A.findByIdAndRemove(id, callback) // executes
- * A.findByIdAndRemove(id) // returns Query
- * A.findByIdAndRemove() // returns Query
- *
- * @param {Object|Number|String} id value of `_id` to query by
- * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#query_Query-setOptions)
- * @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict)
- * @param {ClientSession} [options.session=null] The session associated with this query. See [transactions docs](/docs/transactions.html).
- * @param {Object|String|String[]} [options.projection=null] optional fields to return, see [`Query.prototype.select()`](#query_Query-select)
- * @param {Object|String} [options.sort] if multiple docs are found by the conditions, sets the sort order to choose which doc to update.
- * @param {Boolean} [options.rawResult] if true, returns the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/ModifyResult.html)
- * @param {Object|String} [options.select] sets the document fields to return.
- * @param {Function} [callback]
- * @return {Query}
- * @see Model.findOneAndRemove #model_Model-findOneAndRemove
- * @see mongodb https://www.mongodb.com/docs/manual/reference/command/findAndModify/
- */
-
-Model.findByIdAndRemove = function(id, options, callback) {
- _checkContext(this, 'findByIdAndRemove');
-
- if (arguments.length === 1 && typeof id === 'function') {
- const msg = 'Model.findByIdAndRemove(): First argument must not be a function.\n\n'
- + ' ' + this.modelName + '.findByIdAndRemove(id, callback)\n'
- + ' ' + this.modelName + '.findByIdAndRemove(id)\n'
- + ' ' + this.modelName + '.findByIdAndRemove()\n';
- throw new TypeError(msg);
- }
- callback = this.$handleCallbackError(callback);
-
- return this.findOneAndRemove({ _id: id }, options, callback);
+ return mq.findOneAndReplace(filter, replacement, options);
};
/**
@@ -3132,126 +2564,153 @@ Model.findByIdAndRemove = function(id, options, callback) {
* await Character.create([{ name: 'Jean-Luc Picard' }], { session });
*
* @param {Array|Object} docs Documents to insert, as a spread or array
- * @param {Object} [options] Options passed down to `save()`. To specify `options`, `docs` **must** be an array, not a spread. See [Model.save](#model_Model-save) for available options.
- * @param {Function} [callback] callback
+ * @param {Object} [options] Options passed down to `save()`. To specify `options`, `docs` **must** be an array, not a spread. See [Model.save](https://mongoosejs.com/docs/api/model.html#Model.prototype.save()) for available options.
+ * @param {Boolean} [options.ordered] saves the docs in series rather than parallel.
+ * @param {Boolean} [options.aggregateErrors] Aggregate Errors instead of throwing the first one that occurs. Default: false
* @return {Promise}
* @api public
*/
-Model.create = function create(doc, options, callback) {
+Model.create = async function create(doc, options) {
+ if (typeof options === 'function' ||
+ typeof arguments[2] === 'function') {
+ throw new MongooseError('Model.create() no longer accepts a callback');
+ }
+
_checkContext(this, 'create');
let args;
- let cb;
const discriminatorKey = this.schema.options.discriminatorKey;
if (Array.isArray(doc)) {
args = doc;
- cb = typeof options === 'function' ? options : callback;
options = options != null && typeof options === 'object' ? options : {};
} else {
const last = arguments[arguments.length - 1];
options = {};
- // Handle falsy callbacks re: #5061
- if (typeof last === 'function' || (arguments.length > 1 && !last)) {
- args = [...arguments];
- cb = args.pop();
+ const hasCallback = typeof last === 'function' ||
+ typeof options === 'function' ||
+ typeof arguments[2] === 'function';
+ if (hasCallback) {
+ throw new MongooseError('Model.create() no longer accepts a callback');
} else {
args = [...arguments];
+ // For backwards compatibility with 6.x, because of gh-5061 Mongoose 6.x and
+ // older would treat a falsy last arg as a callback. We don't want to throw
+ // an error here, because it would look strange if `Test.create({}, void 0)`
+ // threw a callback error. But we also don't want to create an unnecessary document.
+ if (args.length > 1 && !last) {
+ args.pop();
+ }
}
if (args.length === 2 &&
args[0] != null &&
args[1] != null &&
args[0].session == null &&
+ last &&
getConstructorName(last.session) === 'ClientSession' &&
!this.schema.path('session')) {
// Probably means the user is running into the common mistake of trying
// to use a spread to specify options, see gh-7535
utils.warn('WARNING: to pass a `session` to `Model.create()` in ' +
'Mongoose, you **must** pass an array as the first argument. See: ' +
- 'https://mongoosejs.com/docs/api/model.html#model_Model-create');
+ 'https://mongoosejs.com/docs/api/model.html#Model.create()');
}
}
- return this.db.base._promiseOrCallback(cb, cb => {
- cb = this.$wrapCallback(cb);
+ if (args.length === 0) {
+ return Array.isArray(doc) ? [] : null;
+ }
+ let res = [];
+ const immediateError = typeof options.aggregateErrors === 'boolean' ? !options.aggregateErrors : true;
- if (args.length === 0) {
- if (Array.isArray(doc)) {
- return cb(null, []);
- } else {
- return cb(null);
- }
- }
+ delete options.aggregateErrors; // dont pass on the option to "$save"
- const toExecute = [];
- let firstError;
- args.forEach(doc => {
- toExecute.push(callback => {
+ if (options.ordered) {
+ for (let i = 0; i < args.length; i++) {
+ try {
+ const doc = args[i];
const Model = this.discriminators && doc[discriminatorKey] != null ?
this.discriminators[doc[discriminatorKey]] || getDiscriminatorByValue(this.discriminators, doc[discriminatorKey]) :
this;
if (Model == null) {
throw new MongooseError(`Discriminator "${doc[discriminatorKey]}" not ` +
- `found for model "${this.modelName}"`);
+ `found for model "${this.modelName}"`);
}
let toSave = doc;
- const callbackWrapper = (error, doc) => {
- if (error) {
- if (!firstError) {
- firstError = error;
- }
- return callback(null, { error: error });
- }
- callback(null, { doc: doc });
- };
-
if (!(toSave instanceof Model)) {
- try {
- toSave = new Model(toSave);
- } catch (error) {
- return callbackWrapper(error);
- }
+ toSave = new Model(toSave);
}
- toSave.$save(options, callbackWrapper);
- });
- });
-
- let numFns = toExecute.length;
- if (numFns === 0) {
- return cb(null, []);
- }
- const _done = (error, res) => {
- const savedDocs = [];
- for (const val of res) {
- if (val.doc) {
- savedDocs.push(val.doc);
+ await toSave.$save(options);
+ res.push(toSave);
+ } catch (err) {
+ if (!immediateError) {
+ res.push(err);
+ } else {
+ throw err;
}
}
+ }
+ return res;
+ } else if (!immediateError) {
+ res = await Promise.allSettled(args.map(async doc => {
+ const Model = this.discriminators && doc[discriminatorKey] != null ?
+ this.discriminators[doc[discriminatorKey]] || getDiscriminatorByValue(this.discriminators, doc[discriminatorKey]) :
+ this;
+ if (Model == null) {
+ throw new MongooseError(`Discriminator "${doc[discriminatorKey]}" not ` +
+ `found for model "${this.modelName}"`);
+ }
+ let toSave = doc;
- if (firstError) {
- return cb(firstError, savedDocs);
+ if (!(toSave instanceof Model)) {
+ toSave = new Model(toSave);
}
- if (Array.isArray(doc)) {
- cb(null, savedDocs);
- } else {
- cb.apply(this, [null].concat(savedDocs));
+ await toSave.$save(options);
+
+ return toSave;
+ }));
+ res = res.map(result => result.status === 'fulfilled' ? result.value : result.reason);
+ } else {
+ let firstError = null;
+ res = await Promise.all(args.map(async doc => {
+ const Model = this.discriminators && doc[discriminatorKey] != null ?
+ this.discriminators[doc[discriminatorKey]] || getDiscriminatorByValue(this.discriminators, doc[discriminatorKey]) :
+ this;
+ if (Model == null) {
+ throw new MongooseError(`Discriminator "${doc[discriminatorKey]}" not ` +
+ `found for model "${this.modelName}"`);
}
- };
+ try {
+ let toSave = doc;
- const _res = [];
- toExecute.forEach((fn, i) => {
- fn((err, res) => {
- _res[i] = res;
- if (--numFns <= 0) {
- return _done(null, _res);
+ if (!(toSave instanceof Model)) {
+ toSave = new Model(toSave);
}
- });
- });
- }, this.events);
+
+ await toSave.$save(options);
+
+ return toSave;
+ } catch (err) {
+ if (!firstError) {
+ firstError = err;
+ }
+ }
+ }));
+ if (firstError) {
+ throw firstError;
+ }
+ }
+
+
+ if (!Array.isArray(doc) && args.length === 1) {
+ return res[0];
+ }
+
+ return res;
};
/**
@@ -3278,7 +2737,7 @@ Model.create = function create(doc, options, callback) {
* // operationType: 'delete',
* // ns: { db: 'mydb', coll: 'Person' },
* // documentKey: { _id: 5a51b125c5500f5aa094c7bd } }
- * await doc.remove();
+ * await doc.deleteOne();
*
* @param {Array} [pipeline]
* @param {Object} [options] see the [mongodb driver options](https://mongodb.github.io/node-mongodb-native/4.9/classes/Collection.html#watch)
@@ -3326,7 +2785,7 @@ Model.watch = function(pipeline, options) {
*
* const session = await Person.startSession();
* let doc = await Person.findOne({ name: 'Ned Stark' }, null, { session });
- * await doc.remove();
+ * await doc.deleteOne();
* // `doc` will always be null, even if reading from a replica set
* // secondary. Without causal consistency, it is possible to
* // get a doc back from the below query if the query reads from a
@@ -3335,7 +2794,6 @@ Model.watch = function(pipeline, options) {
*
* @param {Object} [options] see the [mongodb driver options](https://mongodb.github.io/node-mongodb-native/4.9/classes/MongoClient.html#startSession)
* @param {Boolean} [options.causalConsistency=true] set to false to disable causal consistency
- * @param {Function} [callback]
* @return {Promise} promise that resolves to a MongoDB driver `ClientSession`
* @api public
*/
@@ -3365,31 +2823,45 @@ Model.startSession = function() {
*
* #### Example:
*
- * const arr = [{ name: 'Star Wars' }, { name: 'The Empire Strikes Back' }];
- * Movies.insertMany(arr, function(error, docs) {});
+ * const docs = await Movies.insertMany([
+ * { name: 'Star Wars' },
+ * { name: 'The Empire Strikes Back' }
+ * ]);
+ * docs[0].name; // 'Star Wars'
+ *
+ * // Return raw result from MongoDB
+ * const result = await Movies.insertMany([
+ * { name: 'Star Wars' },
+ * { name: 'The Empire Strikes Back' }
+ * ], { rawResult: true });
*
* @param {Array|Object|*} doc(s)
* @param {Object} [options] see the [mongodb driver options](https://mongodb.github.io/node-mongodb-native/4.9/classes/Collection.html#insertMany)
* @param {Boolean} [options.ordered=true] if true, will fail fast on the first error encountered. If false, will insert all the documents it can and report errors later. An `insertMany()` with `ordered = false` is called an "unordered" `insertMany()`.
* @param {Boolean} [options.rawResult=false] if false, the returned promise resolves to the documents that passed mongoose document validation. If `true`, will return the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/InsertManyResult.html) with a `mongoose` property that contains `validationErrors` and `results` if this is an unordered `insertMany`.
- * @param {Boolean} [options.lean=false] if `true`, skips hydrating and validating the documents. This option is useful if you need the extra performance, but Mongoose won't validate the documents before inserting.
+ * @param {Boolean} [options.lean=false] if `true`, skips hydrating the documents. This means Mongoose will **not** cast, validate, or apply defaults to any of the documents passed to `insertMany()`. This option is useful if you need the extra performance, but comes with data integrity risk. Consider using with [`castObject()`](https://mongoosejs.com/docs/api/model.html#Model.castObject()) and [`applyDefaults()`](https://mongoosejs.com/docs/api/model.html#Model.applyDefaults()).
* @param {Number} [options.limit=null] this limits the number of documents being processed (validation/casting) by mongoose in parallel, this does **NOT** send the documents in batches to MongoDB. Use this option if you're processing a large number of documents and your app is running out of memory.
* @param {String|Object|Array} [options.populate=null] populates the result documents. This option is a no-op if `rawResult` is set.
- * @param {Function} [callback] callback
+ * @param {Boolean} [options.throwOnValidationError=false] If true and `ordered: false`, throw an error if one of the operations failed validation, but all valid operations completed successfully.
* @return {Promise} resolving to the raw result from the MongoDB driver if `options.rawResult` was `true`, or the documents that passed validation, otherwise
* @api public
*/
-Model.insertMany = function(arr, options, callback) {
+Model.insertMany = async function insertMany(arr, options) {
_checkContext(this, 'insertMany');
-
- if (typeof options === 'function') {
- callback = options;
- options = null;
+ if (typeof options === 'function' ||
+ typeof arguments[2] === 'function') {
+ throw new MongooseError('Model.insertMany() no longer accepts a callback');
}
- return this.db.base._promiseOrCallback(callback, cb => {
- this.$__insertMany(arr, options, cb);
- }, this.events);
+
+ return new Promise((resolve, reject) => {
+ this.$__insertMany(arr, options, (err, res) => {
+ if (err != null) {
+ return reject(err);
+ }
+ resolve(res);
+ });
+ });
};
/**
@@ -3410,17 +2882,20 @@ Model.$__insertMany = function(arr, options, callback) {
callback = options;
options = null;
}
- if (callback) {
- callback = this.$handleCallbackError(callback);
- callback = this.$wrapCallback(callback);
- }
+
callback = callback || utils.noop;
options = options || {};
const limit = options.limit || 1000;
const rawResult = !!options.rawResult;
const ordered = typeof options.ordered === 'boolean' ? options.ordered : true;
+ const throwOnValidationError = typeof options.throwOnValidationError === 'boolean' ? options.throwOnValidationError : false;
const lean = !!options.lean;
+ const asyncLocalStorage = this.db.base.transactionAsyncLocalStorage?.getStore();
+ if ((!options || !options.hasOwnProperty('session')) && asyncLocalStorage?.session != null) {
+ options = { ...options, session: asyncLocalStorage.session };
+ }
+
if (!Array.isArray(arr)) {
arr = [arr];
}
@@ -3430,13 +2905,26 @@ Model.$__insertMany = function(arr, options, callback) {
const results = ordered ? null : new Array(arr.length);
const toExecute = arr.map((doc, index) =>
callback => {
+ // If option `lean` is set to true bypass validation and hydration
+ if (lean) {
+ // we have to execute callback at the nextTick to be compatible
+ // with parallelLimit, as `results` variable has TDZ issue if we
+ // execute the callback synchronously
+ return immediate(() => callback(null, doc));
+ }
+ let createdNewDoc = false;
if (!(doc instanceof _this)) {
+ if (doc != null && typeof doc !== 'object') {
+ return callback(new ObjectParameterError(doc, 'arr.' + index, 'insertMany'));
+ }
try {
doc = new _this(doc);
+ createdNewDoc = true;
} catch (err) {
return callback(err);
}
}
+
if (options.session != null) {
doc.$session(options.session);
}
@@ -3447,21 +2935,18 @@ Model.$__insertMany = function(arr, options, callback) {
// execute the callback synchronously
return immediate(() => callback(null, doc));
}
- doc.$validate({ __noPromise: true }, function(error) {
- if (error) {
- // Option `ordered` signals that insert should be continued after reaching
- // a failing insert. Therefore we delegate "null", meaning the validation
- // failed. It's up to the next function to filter out all failed models
+ doc.$validate(createdNewDoc ? { _skipParallelValidateCheck: true } : null).then(
+ () => { callback(null, doc); },
+ error => {
if (ordered === false) {
validationErrors.push(error);
validationErrorsToOriginalOrder.set(error, index);
results[index] = error;
return callback(null, null);
}
- return callback(error);
+ callback(error);
}
- callback(null, doc);
- });
+ );
});
parallelLimit(toExecute, limit, function(error, docs) {
@@ -3496,6 +2981,14 @@ Model.$__insertMany = function(arr, options, callback) {
// Quickly escape while there aren't any valid docAttributes
if (docAttributes.length === 0) {
+ if (throwOnValidationError) {
+ return callback(new MongooseBulkWriteError(
+ validationErrors,
+ results,
+ null,
+ 'insertMany'
+ ));
+ }
if (rawResult) {
const res = {
acknowledged: true,
@@ -3510,37 +3003,97 @@ Model.$__insertMany = function(arr, options, callback) {
callback(null, []);
return;
}
- const docObjects = docAttributes.map(function(doc) {
+ const docObjects = lean ? docAttributes : docAttributes.map(function(doc) {
if (doc.$__schema.options.versionKey) {
doc[doc.$__schema.options.versionKey] = 0;
}
const shouldSetTimestamps = (!options || options.timestamps !== false) && doc.initializeTimestamps && (!doc.$__ || doc.$__.timestamps !== false);
if (shouldSetTimestamps) {
- return doc.initializeTimestamps().toObject(internalToObjectOptions);
+ doc.initializeTimestamps();
+ }
+ if (doc.$__hasOnlyPrimitiveValues()) {
+ return doc.$__toObjectShallow();
}
return doc.toObject(internalToObjectOptions);
});
- _this.$__collection.insertMany(docObjects, options, function(error, res) {
- if (error) {
+ _this.$__collection.insertMany(docObjects, options).then(
+ res => {
+ if (!lean) {
+ for (const attribute of docAttributes) {
+ attribute.$__reset();
+ _setIsNew(attribute, false);
+ }
+ }
+
+ if (ordered === false && throwOnValidationError && validationErrors.length > 0) {
+ for (let i = 0; i < results.length; ++i) {
+ if (results[i] === void 0) {
+ results[i] = docs[i];
+ }
+ }
+ return callback(new MongooseBulkWriteError(
+ validationErrors,
+ results,
+ res,
+ 'insertMany'
+ ));
+ }
+
+ if (rawResult) {
+ if (ordered === false) {
+ for (let i = 0; i < results.length; ++i) {
+ if (results[i] === void 0) {
+ results[i] = docs[i];
+ }
+ }
+
+ // Decorate with mongoose validation errors in case of unordered,
+ // because then still do `insertMany()`
+ res.mongoose = {
+ validationErrors: validationErrors,
+ results: results
+ };
+ }
+ return callback(null, res);
+ }
+
+ if (options.populate != null) {
+ return _this.populate(docAttributes, options.populate).then(
+ docs => { callback(null, docs); },
+ err => {
+ if (err != null) {
+ err.insertedDocs = docAttributes;
+ }
+ throw err;
+ }
+ );
+ }
+
+ callback(null, docAttributes);
+ },
+ error => {
// `writeErrors` is a property reported by the MongoDB driver,
// just not if there's only 1 error.
if (error.writeErrors == null &&
- (error.result && error.result.result && error.result.result.writeErrors) != null) {
+ (error.result && error.result.result && error.result.result.writeErrors) != null) {
error.writeErrors = error.result.result.writeErrors;
}
// `insertedDocs` is a Mongoose-specific property
+ const hasWriteErrors = error && error.writeErrors;
const erroredIndexes = new Set((error && error.writeErrors || []).map(err => err.index));
- for (let i = 0; i < error.writeErrors.length; ++i) {
- const originalIndex = validDocIndexToOriginalIndex.get(error.writeErrors[i].index);
- error.writeErrors[i] = {
- ...error.writeErrors[i],
- index: originalIndex
- };
- if (!ordered) {
- results[originalIndex] = error.writeErrors[i];
+ if (error.writeErrors != null) {
+ for (let i = 0; i < error.writeErrors.length; ++i) {
+ const originalIndex = validDocIndexToOriginalIndex.get(error.writeErrors[i].index);
+ error.writeErrors[i] = {
+ ...error.writeErrors[i],
+ index: originalIndex
+ };
+ if (!ordered) {
+ results[originalIndex] = error.writeErrors[i];
+ }
}
}
@@ -3557,7 +3110,7 @@ Model.$__insertMany = function(arr, options, callback) {
let firstErroredIndex = -1;
error.insertedDocs = docAttributes.
filter((doc, i) => {
- const isErrored = erroredIndexes.has(i);
+ const isErrored = !hasWriteErrors || erroredIndexes.has(i);
if (ordered) {
if (firstErroredIndex > -1) {
@@ -3572,6 +3125,9 @@ Model.$__insertMany = function(arr, options, callback) {
return !isErrored;
}).
map(function setIsNewForInsertedDoc(doc) {
+ if (lean) {
+ return doc;
+ }
doc.$__reset();
_setIsNew(doc, false);
return doc;
@@ -3585,45 +3141,8 @@ Model.$__insertMany = function(arr, options, callback) {
}
callback(error, null);
- return;
- }
-
- for (const attribute of docAttributes) {
- attribute.$__reset();
- _setIsNew(attribute, false);
- }
-
- if (rawResult) {
- if (ordered === false) {
- for (let i = 0; i < results.length; ++i) {
- if (results[i] === void 0) {
- results[i] = docs[i];
- }
- }
-
- // Decorate with mongoose validation errors in case of unordered,
- // because then still do `insertMany()`
- res.mongoose = {
- validationErrors: validationErrors,
- results: results
- };
- }
- return callback(null, res);
}
-
- if (options.populate != null) {
- return _this.populate(docAttributes, options.populate, err => {
- if (err != null) {
- error.insertedDocs = docAttributes;
- return callback(err);
- }
-
- callback(null, docs);
- });
- }
-
- callback(null, docAttributes);
- });
+ );
});
};
@@ -3636,7 +3155,7 @@ function _setIsNew(doc, val) {
doc.$emit('isNew', val);
doc.constructor.emit('isNew', val);
- const subdocs = doc.$getAllSubdocs();
+ const subdocs = doc.$getAllSubdocs({ useCache: true });
for (const subdoc of subdocs) {
subdoc.$isNew = val;
subdoc.$emit('isNew', val);
@@ -3651,10 +3170,11 @@ function _setIsNew(doc, val) {
* trip to MongoDB.
*
* Mongoose will perform casting on all operations you provide.
+ * The only exception is [setting the `update` operator for `updateOne` or `updateMany` to a pipeline](https://www.mongodb.com/docs/manual/reference/method/db.collection.bulkWrite/#updateone-and-updatemany): Mongoose does **not** cast update pipelines.
*
* This function does **not** trigger any middleware, neither `save()`, nor `update()`.
* If you need to trigger
- * `save()` middleware for every document use [`create()`](#model_Model-create) instead.
+ * `save()` middleware for every document use [`create()`](https://mongoosejs.com/docs/api/model.html#Model.create()) instead.
*
* #### Example:
*
@@ -3686,6 +3206,15 @@ function _setIsNew(doc, val) {
* console.log(res.insertedCount, res.modifiedCount, res.deletedCount);
* });
*
+ * // Mongoose does **not** cast update pipelines, so no casting for the `update` option below.
+ * // Mongoose does still cast `filter`
+ * await Character.bulkWrite([{
+ * updateOne: {
+ * filter: { name: 'Annika Hansen' },
+ * update: [{ $set: { name: 7 } }] // Array means update pipeline, so Mongoose skips casting
+ * }
+ * }]);
+ *
* The [supported operations](https://www.mongodb.com/docs/manual/reference/method/db.collection.bulkWrite/#db.collection.bulkWrite) are:
*
* - `insertOne`
@@ -3697,6 +3226,7 @@ function _setIsNew(doc, val) {
*
* @param {Array} ops
* @param {Object} [ops.insertOne.document] The document to insert
+ * @param {Object} [ops.insertOne.timestamps=true] If false, do not apply [timestamps](https://mongoosejs.com/docs/guide.html#timestamps) to the operation
* @param {Object} [ops.updateOne.filter] Update the first document that matches this filter
* @param {Object} [ops.updateOne.update] An object containing [update operators](https://www.mongodb.com/docs/manual/reference/operator/update/)
* @param {Boolean} [ops.updateOne.upsert=false] If true, insert a doc if none match
@@ -3714,125 +3244,204 @@ function _setIsNew(doc, val) {
* @param {Object} [ops.replaceOne.filter] Replace the first document that matches this filter
* @param {Object} [ops.replaceOne.replacement] The replacement document
* @param {Boolean} [ops.replaceOne.upsert=false] If true, insert a doc if no documents match `filter`
+ * @param {Object} [ops.replaceOne.timestamps=true] If false, do not apply [timestamps](https://mongoosejs.com/docs/guide.html#timestamps) to the operation
* @param {Object} [options]
* @param {Boolean} [options.ordered=true] If true, execute writes in order and stop at the first error. If false, execute writes in parallel and continue until all writes have either succeeded or errored.
- * @param {ClientSession} [options.session=null] The session associated with this bulk write. See [transactions docs](/docs/transactions.html).
- * @param {String|number} [options.w=1] The [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/). See [`Query#w()`](/docs/api/query.html#query_Query-w) for more information.
+ * @param {Boolean} [options.timestamps=true] If false, do not apply [timestamps](https://mongoosejs.com/docs/guide.html#timestamps) to any operations. Can be overridden at the operation-level.
+ * @param {ClientSession} [options.session=null] The session associated with this bulk write. See [transactions docs](https://mongoosejs.com/docs/transactions.html).
+ * @param {String|number} [options.w=1] The [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/). See [`Query#w()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.w()) for more information.
* @param {number} [options.wtimeout=null] The [write concern timeout](https://www.mongodb.com/docs/manual/reference/write-concern/#wtimeout).
* @param {Boolean} [options.j=true] If false, disable [journal acknowledgement](https://www.mongodb.com/docs/manual/reference/write-concern/#j-option)
* @param {Boolean} [options.skipValidation=false] Set to true to skip Mongoose schema validation on bulk write operations. Mongoose currently runs validation on `insertOne` and `replaceOne` operations by default.
* @param {Boolean} [options.bypassDocumentValidation=false] If true, disable [MongoDB server-side schema validation](https://www.mongodb.com/docs/manual/core/schema-validation/) for all writes in this bulk.
- * @param {Boolean} [options.strict=null] Overwrites the [`strict` option](/docs/guide.html#strict) on schema. If false, allows filtering and writing fields not defined in the schema for all writes in this bulk.
- * @param {Function} [callback] callback `function(error, bulkWriteOpResult) {}`
+ * @param {Boolean} [options.throwOnValidationError=false] If true and `ordered: false`, throw an error if one of the operations failed validation, but all valid operations completed successfully. Note that Mongoose will still send all valid operations to the MongoDB server.
+ * @param {Boolean|"throw"} [options.strict=null] Overwrites the [`strict` option](https://mongoosejs.com/docs/guide.html#strict) on schema. If false, allows filtering and writing fields not defined in the schema for all writes in this bulk.
* @return {Promise} resolves to a [`BulkWriteOpResult`](https://mongodb.github.io/node-mongodb-native/4.9/classes/BulkWriteResult.html) if the operation succeeds
* @api public
*/
-Model.bulkWrite = function(ops, options, callback) {
+Model.bulkWrite = async function bulkWrite(ops, options) {
_checkContext(this, 'bulkWrite');
- if (typeof options === 'function') {
- callback = options;
- options = null;
+ if (typeof options === 'function' ||
+ typeof arguments[2] === 'function') {
+ throw new MongooseError('Model.bulkWrite() no longer accepts a callback');
}
options = options || {};
+
+ const shouldSkip = await new Promise((resolve, reject) => {
+ this.hooks.execPre('bulkWrite', this, [ops, options], (err) => {
+ if (err != null) {
+ if (err instanceof Kareem.skipWrappedFunction) {
+ return resolve(err);
+ }
+ return reject(err);
+ }
+ resolve();
+ });
+ });
+
+ if (shouldSkip) {
+ return shouldSkip.args[0];
+ }
+
const ordered = options.ordered == null ? true : options.ordered;
+ if (ops.length === 0) {
+ return getDefaultBulkwriteResult();
+ }
+
const validations = ops.map(op => castBulkWrite(this, op, options));
+ const asyncLocalStorage = this.db.base.transactionAsyncLocalStorage?.getStore();
+ if ((!options || !options.hasOwnProperty('session')) && asyncLocalStorage?.session != null) {
+ options = { ...options, session: asyncLocalStorage.session };
+ }
- callback = this.$handleCallbackError(callback);
- return this.db.base._promiseOrCallback(callback, cb => {
- cb = this.$wrapCallback(cb);
- if (ordered) {
+ let res = null;
+ if (ordered) {
+ await new Promise((resolve, reject) => {
each(validations, (fn, cb) => fn(cb), error => {
if (error) {
- return cb(error);
- }
-
- if (ops.length === 0) {
- return cb(null, getDefaultBulkwriteResult());
+ return reject(error);
}
- try {
- this.$__collection.bulkWrite(ops, options, (error, res) => {
- if (error) {
- return cb(error);
- }
-
- cb(null, res);
- });
- } catch (err) {
- return cb(err);
- }
+ resolve();
});
+ });
- return;
+ try {
+ res = await this.$__collection.bulkWrite(ops, options);
+ } catch (error) {
+ await new Promise((resolve, reject) => {
+ const _opts = { error: error };
+ this.hooks.execPost('bulkWrite', this, [null], _opts, (err) => {
+ if (err != null) {
+ return reject(err);
+ }
+ resolve();
+ });
+ });
}
-
+ } else {
let remaining = validations.length;
let validOps = [];
let validationErrors = [];
- for (let i = 0; i < validations.length; ++i) {
- validations[i]((err) => {
- if (err == null) {
- validOps.push(i);
- } else {
- validationErrors.push({ index: i, error: err });
- }
- if (--remaining <= 0) {
- completeUnorderedValidation.call(this);
- }
- });
- }
+ const results = [];
+ await new Promise((resolve) => {
+ for (let i = 0; i < validations.length; ++i) {
+ validations[i]((err) => {
+ if (err == null) {
+ validOps.push(i);
+ } else {
+ validationErrors.push({ index: i, error: err });
+ results[i] = err;
+ }
+ if (--remaining <= 0) {
+ resolve();
+ }
+ });
+ }
+ });
validationErrors = validationErrors.
sort((v1, v2) => v1.index - v2.index).
map(v => v.error);
- function completeUnorderedValidation() {
- validOps = validOps.sort().map(index => ops[index]);
+ const validOpIndexes = validOps;
+ validOps = validOps.sort().map(index => ops[index]);
+
+ if (validOps.length === 0) {
+ if (options.throwOnValidationError && validationErrors.length) {
+ throw new MongooseBulkWriteError(
+ validationErrors,
+ results,
+ res,
+ 'bulkWrite'
+ );
+ }
+ return getDefaultBulkwriteResult();
+ }
- this.$__collection.bulkWrite(validOps, options, (error, res) => {
- if (error) {
- if (validationErrors.length > 0) {
- error.mongoose = error.mongoose || {};
- error.mongoose.validationErrors = validationErrors;
- }
+ let error;
+ [res, error] = await this.$__collection.bulkWrite(validOps, options).
+ then(res => ([res, null])).
+ catch(error => ([null, error]));
- return cb(error);
- }
-
- if (validationErrors.length > 0) {
- res.mongoose = res.mongoose || {};
- res.mongoose.validationErrors = validationErrors;
- }
+ if (error) {
+ if (validationErrors.length > 0) {
+ error.mongoose = error.mongoose || {};
+ error.mongoose.validationErrors = validationErrors;
+ }
- cb(null, res);
+ await new Promise((resolve, reject) => {
+ const _opts = { error: error };
+ this.hooks.execPost('bulkWrite', this, [null], _opts, (err) => {
+ if (err != null) {
+ return reject(err);
+ }
+ resolve();
+ });
});
}
- }, this.events);
+
+ for (let i = 0; i < validOpIndexes.length; ++i) {
+ results[validOpIndexes[i]] = null;
+ }
+ if (validationErrors.length > 0) {
+ if (options.throwOnValidationError) {
+ throw new MongooseBulkWriteError(
+ validationErrors,
+ results,
+ res,
+ 'bulkWrite'
+ );
+ } else {
+ res.mongoose = res.mongoose || {};
+ res.mongoose.validationErrors = validationErrors;
+ res.mongoose.results = results;
+ }
+ }
+ }
+
+ await new Promise((resolve, reject) => {
+ this.hooks.execPost('bulkWrite', this, [res], (err) => {
+ if (err != null) {
+ return reject(err);
+ }
+ resolve();
+ });
+ });
+
+ return res;
};
/**
- * takes an array of documents, gets the changes and inserts/updates documents in the database
- * according to whether or not the document is new, or whether it has changes or not.
+ * Takes an array of documents, gets the changes and inserts/updates documents in the database
+ * according to whether or not the document is new, or whether it has changes or not.
*
* `bulkSave` uses `bulkWrite` under the hood, so it's mostly useful when dealing with many documents (10K+)
*
+ * `bulkSave()` throws errors under the following conditions:
+ *
+ * - one of the provided documents fails validation. In this case, `bulkSave()` does not send a `bulkWrite()`, and throws the first validation error.
+ * - `bulkWrite()` fails (for example, due to being unable to connect to MongoDB or due to duplicate key error)
+ * - `bulkWrite()` did not insert or update **any** documents. In this case, `bulkSave()` will throw a DocumentNotFound error.
+ *
+ * Note that `bulkSave()` will **not** throw an error if only some of the `save()` calls succeeded.
+ *
* @param {Array} documents
* @param {Object} [options] options passed to the underlying `bulkWrite()`
* @param {Boolean} [options.timestamps] defaults to `null`, when set to false, mongoose will not add/update timestamps to the documents.
- * @param {ClientSession} [options.session=null] The session associated with this bulk write. See [transactions docs](/docs/transactions.html).
- * @param {String|number} [options.w=1] The [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/). See [`Query#w()`](/docs/api/query.html#query_Query-w) for more information.
+ * @param {ClientSession} [options.session=null] The session associated with this bulk write. See [transactions docs](https://mongoosejs.com/docs/transactions.html).
+ * @param {String|number} [options.w=1] The [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/). See [`Query#w()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.w()) for more information.
* @param {number} [options.wtimeout=null] The [write concern timeout](https://www.mongodb.com/docs/manual/reference/write-concern/#wtimeout).
* @param {Boolean} [options.j=true] If false, disable [journal acknowledgement](https://www.mongodb.com/docs/manual/reference/write-concern/#j-option)
- *
+ * @param {Boolean} [options.validateBeforeSave=true] set to `false` to skip Mongoose validation on all documents
+ * @return {BulkWriteResult} the return value from `bulkWrite()`
*/
-Model.bulkSave = async function(documents, options) {
+Model.bulkSave = async function bulkSave(documents, options) {
options = options || {};
- const writeOperations = this.buildBulkWriteOperations(documents, { skipValidation: true, timestamps: options.timestamps });
-
if (options.timestamps != null) {
for (const document of documents) {
document.$__.saveOptions = document.$__.saveOptions || {};
@@ -3847,25 +3456,41 @@ Model.bulkSave = async function(documents, options) {
}
}
- await Promise.all(documents.map(buildPreSavePromise));
+ await Promise.all(documents.map(doc => buildPreSavePromise(doc, options)));
- const { bulkWriteResult, bulkWriteError } = await this.bulkWrite(writeOperations, options).then(
+ const writeOperations = this.buildBulkWriteOperations(documents, { skipValidation: true, timestamps: options.timestamps });
+ const { bulkWriteResult, bulkWriteError } = await this.bulkWrite(writeOperations, { skipValidation: true, ...options }).then(
(res) => ({ bulkWriteResult: res, bulkWriteError: null }),
(err) => ({ bulkWriteResult: null, bulkWriteError: err })
);
+ // If not a MongoBulkWriteError, treat this as all documents failed to save.
+ if (bulkWriteError != null && !(bulkWriteError instanceof MongoBulkWriteError)) {
+ throw bulkWriteError;
+ }
- await Promise.all(
- documents.map(async(document) => {
- const documentError = bulkWriteError && bulkWriteError.writeErrors.find(writeError => {
- const writeErrorDocumentId = writeError.err.op._id || writeError.err.op.q._id;
- return writeErrorDocumentId.toString() === document._id.toString();
- });
+ const matchedCount = bulkWriteResult?.matchedCount ?? 0;
+ const insertedCount = bulkWriteResult?.insertedCount ?? 0;
+ if (writeOperations.length > 0 && matchedCount + insertedCount < writeOperations.length && !bulkWriteError) {
+ throw new MongooseBulkSaveIncompleteError(
+ this.modelName,
+ documents,
+ bulkWriteResult
+ );
+ }
- if (documentError == null) {
- await handleSuccessfulWrite(document);
- }
- })
- );
+ const successfulDocuments = [];
+ for (let i = 0; i < documents.length; i++) {
+ const document = documents[i];
+ const documentError = bulkWriteError && bulkWriteError.writeErrors.find(writeError => {
+ const writeErrorDocumentId = writeError.err.op._id || writeError.err.op.q._id;
+ return writeErrorDocumentId.toString() === document._doc._id.toString();
+ });
+
+ if (documentError == null) {
+ successfulDocuments.push(document);
+ }
+ }
+ await Promise.all(successfulDocuments.map(document => handleSuccessfulWrite(document)));
if (bulkWriteError && bulkWriteError.writeErrors && bulkWriteError.writeErrors.length) {
throw bulkWriteError;
@@ -3874,9 +3499,9 @@ Model.bulkSave = async function(documents, options) {
return bulkWriteResult;
};
-function buildPreSavePromise(document) {
+function buildPreSavePromise(document, options) {
return new Promise((resolve, reject) => {
- document.schema.s.hooks.execPre('save', document, (err) => {
+ document.schema.s.hooks.execPre('save', document, [options], (err) => {
if (err) {
reject(err);
return;
@@ -3913,6 +3538,9 @@ function handleSuccessfulWrite(document) {
*/
Model.applyDefaults = function applyDefaults(doc) {
+ if (doc == null) {
+ return doc;
+ }
if (doc.$__ != null) {
applyDefaultsHelper(doc, doc.$__.fields, doc.$__.exclude);
@@ -3920,12 +3548,81 @@ Model.applyDefaults = function applyDefaults(doc) {
applyDefaults(subdoc, subdoc.$__.fields, subdoc.$__.exclude);
}
- return doc;
+ return doc;
+ }
+
+ applyDefaultsToPOJO(doc, this.schema);
+
+ return doc;
+};
+
+/**
+ * Apply this model's virtuals to a given POJO. Virtuals execute with the POJO as the context `this`.
+ *
+ * #### Example:
+ *
+ * const userSchema = new Schema({ name: String });
+ * userSchema.virtual('upper').get(function() { return this.name.toUpperCase(); });
+ * const User = mongoose.model('User', userSchema);
+ *
+ * const obj = { name: 'John' };
+ * User.applyVirtuals(obj);
+ * obj.name; // 'John'
+ * obj.upper; // 'JOHN', Mongoose applied the return value of the virtual to the given object
+ *
+ * @param {Object} obj object or document to apply virtuals on
+ * @param {Array} [virtualsToApply] optional whitelist of virtuals to apply
+ * @returns {Object} obj
+ * @api public
+ */
+
+Model.applyVirtuals = function applyVirtuals(obj, virtualsToApply) {
+ if (obj == null) {
+ return obj;
+ }
+ // Nothing to do if this is already a hydrated document - it should already have virtuals
+ if (obj.$__ != null) {
+ return obj;
+ }
+
+ applyVirtualsHelper(this.schema, obj, virtualsToApply);
+
+ return obj;
+};
+
+/**
+ * Apply this model's timestamps to a given POJO, including subdocument timestamps
+ *
+ * #### Example:
+ *
+ * const userSchema = new Schema({ name: String }, { timestamps: true });
+ * const User = mongoose.model('User', userSchema);
+ *
+ * const obj = { name: 'John' };
+ * User.applyTimestamps(obj);
+ * obj.createdAt; // 2024-06-01T18:00:00.000Z
+ * obj.updatedAt; // 2024-06-01T18:00:00.000Z
+ *
+ * @param {Object} obj object or document to apply virtuals on
+ * @param {Object} [options]
+ * @param {Boolean} [options.isUpdate=false] if true, treat this as an update: just set updatedAt, skip setting createdAt. If false, set both createdAt and updatedAt
+ * @param {Function} [options.currentTime] if set, Mongoose will call this function to get the current time.
+ * @returns {Object} obj
+ * @api public
+ */
+
+Model.applyTimestamps = function applyTimestamps(obj, options) {
+ if (obj == null) {
+ return obj;
+ }
+ // Nothing to do if this is already a hydrated document - it should already have timestamps
+ if (obj.$__ != null) {
+ return obj;
}
- applyDefaultsToPOJO(doc, this.schema);
+ applyTimestampsHelper(this.schema, obj, options);
- return doc;
+ return obj;
};
/**
@@ -3952,7 +3649,11 @@ Model.castObject = function castObject(obj, options) {
options = options || {};
const ret = {};
- const schema = this.schema;
+ let schema = this.schema;
+ const discriminatorKey = schema.options.discriminatorKey;
+ if (schema.discriminators != null && obj != null && obj[discriminatorKey] != null) {
+ schema = getSchemaDiscriminatorByValue(schema, obj[discriminatorKey]) || schema;
+ }
const paths = Object.keys(schema.paths);
for (const path of paths) {
@@ -3989,7 +3690,21 @@ Model.castObject = function castObject(obj, options) {
}
if (schemaType.$isMongooseDocumentArray) {
- continue;
+ const castNonArraysOption = schemaType.options?.castNonArrays ?? schemaType.constructor.options.castNonArrays;
+ if (!Array.isArray(val)) {
+ if (!castNonArraysOption) {
+ if (!options.ignoreCastErrors) {
+ error = error || new ValidationError();
+ error.addError(path, new ObjectExpectedError(path, val));
+ }
+ } else {
+ cur[pieces[pieces.length - 1]] = [
+ Model.castObject.call(schemaType.caster, val)
+ ];
+ }
+
+ continue;
+ }
}
if (schemaType.$isSingleNested || schemaType.$isMongooseDocumentArrayElement) {
try {
@@ -4043,6 +3758,7 @@ Model.buildBulkWriteOperations = function buildBulkWriteOperations(documents, op
}
setDefaultOptions();
+ const discriminatorKey = this.schema.options.discriminatorKey;
const writeOperations = documents.reduce((accumulator, document, i) => {
if (!options.skipValidation) {
@@ -4073,6 +3789,23 @@ Model.buildBulkWriteOperations = function buildBulkWriteOperations(documents, op
_applyCustomWhere(document, where);
+ // If shard key is set, add shard keys to _filter_ condition to right shard is targeted
+ const shardKey = this.schema.options.shardKey;
+ if (shardKey) {
+ const paths = Object.keys(shardKey);
+ const len = paths.length;
+
+ for (let i = 0; i < len; ++i) {
+ where[paths[i]] = document[paths[i]];
+ }
+ }
+
+ // Set the discriminator key, so bulk write casting knows which
+ // schema to use re: gh-13907
+ if (document[discriminatorKey] != null && !(discriminatorKey in where)) {
+ where[discriminatorKey] = document[discriminatorKey];
+ }
+
document.$__version(where, delta);
const writeOperation = { updateOne: { filter: where, update: changes } };
utils.injectTimestampsOption(writeOperation.updateOne, options.timestamps);
@@ -4109,6 +3842,7 @@ Model.buildBulkWriteOperations = function buildBulkWriteOperations(documents, op
* @param {Object|String|String[]} [projection] optional projection containing which fields should be selected for this document
* @param {Object} [options] optional options
* @param {Boolean} [options.setters=false] if true, apply schema setters when hydrating
+ * @param {Boolean} [options.hydratedPopulatedDocs=false] if true, populates the docs if passing pre-populated data
* @return {Document} document instance
* @api public
*/
@@ -4122,98 +3856,13 @@ Model.hydrate = function(obj, projection, options) {
}
obj = applyProjection(obj, projection);
}
-
- const document = require('./queryhelpers').createModel(this, obj, projection);
+ const document = require('./queryHelpers').createModel(this, obj, projection);
document.$init(obj, options);
return document;
};
/**
- * Updates one document in the database without returning it.
- *
- * This function triggers the following middleware.
- *
- * - `update()`
- *
- * This method is deprecated. See [Deprecation Warnings](../deprecations.html#update) for details.
- *
- * #### Example:
- *
- * MyModel.update({ age: { $gt: 18 } }, { oldEnough: true }, fn);
- *
- * const res = await MyModel.update({ name: 'Tobi' }, { ferret: true });
- * res.n; // Number of documents that matched `{ name: 'Tobi' }`
- * // Number of documents that were changed. If every doc matched already
- * // had `ferret` set to `true`, `nModified` will be 0.
- * res.nModified;
- *
- * #### Valid options:
- *
- * - `strict` (boolean): overrides the [schema-level `strict` option](/docs/guide.html#strict) for this update
- * - `upsert` (boolean): whether to create the doc if it doesn't match (false)
- * - `writeConcern` (object): sets the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/) for replica sets. Overrides the [schema-level write concern](/docs/guide.html#writeConcern)
- * - `multi` (boolean): whether multiple documents should be updated (false)
- * - `runValidators`: if true, runs [update validators](/docs/validation.html#update-validators) on this command. Update validators validate the update operation against the model's schema.
- * - `setDefaultsOnInsert` (boolean): if this and `upsert` are true, mongoose will apply the [defaults](https://mongoosejs.com/docs/defaults.html) specified in the model's schema if a new document is created. This option only works on MongoDB >= 2.4 because it relies on [MongoDB's `$setOnInsert` operator](https://www.mongodb.com/docs/manual/reference/operator/update/setOnInsert/).
- * - `timestamps` (boolean): If set to `false` and [schema-level timestamps](/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Does nothing if schema-level timestamps are not set.
- * - `overwrite` (boolean): disables update-only mode, allowing you to overwrite the doc (false)
- *
- * All `update` values are cast to their appropriate SchemaTypes before being sent.
- *
- * The `callback` function receives `(err, rawResponse)`.
- *
- * - `err` is the error if any occurred
- * - `rawResponse` is the full response from Mongo
- *
- * #### Note:
- *
- * All top level keys which are not `atomic` operation names are treated as set operations:
- *
- * #### Example:
- *
- * const query = { name: 'borne' };
- * Model.update(query, { name: 'jason bourne' }, options, callback);
- *
- * // is sent as
- * Model.update(query, { $set: { name: 'jason bourne' }}, options, function(err, res));
- * // if overwrite option is false. If overwrite is true, sent without the $set wrapper.
- *
- * This helps prevent accidentally overwriting all documents in your collection with `{ name: 'jason bourne' }`.
- *
- * #### Note:
- *
- * Be careful to not use an existing model instance for the update clause (this won't work and can cause weird behavior like infinite loops). Also, ensure that the update clause does not have an _id property, which causes Mongo to return a "Mod on _id not allowed" error.
- *
- * @deprecated
- * @see strict https://mongoosejs.com/docs/guide.html#strict
- * @see response https://mongodb.github.io/node-mongodb-native/4.9/classes/Collection.html#update
- * @param {Object} filter
- * @param {Object} doc
- * @param {Object} [options] optional see [`Query.prototype.setOptions()`](/docs/api/query.html#query_Query-setOptions)
- * @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](/docs/guide.html#strict)
- * @param {Boolean} [options.upsert=false] if true, and no documents found, insert a new document
- * @param {Object} [options.writeConcern=null] sets the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/) for replica sets. Overrides the [schema-level write concern](/docs/guide.html#writeConcern)
- * @param {Boolean} [options.multi=false] whether multiple documents should be updated or just the first one that matches `filter`.
- * @param {Boolean} [options.runValidators=false] if true, runs [update validators](/docs/validation.html#update-validators) on this command. Update validators validate the update operation against the model's schema.
- * @param {Boolean} [options.setDefaultsOnInsert=false] `true` by default. If `setDefaultsOnInsert` and `upsert` are true, mongoose will apply the [defaults](https://mongoosejs.com/docs/defaults.html) specified in the model's schema if a new document is created.
- * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Does nothing if schema-level timestamps are not set.
- * @param {Boolean} [options.overwrite=false] By default, if you don't include any [update operators](https://www.mongodb.com/docs/manual/reference/operator/update/) in `doc`, Mongoose will wrap `doc` in `$set` for you. This prevents you from accidentally overwriting the document. This option tells Mongoose to skip adding `$set`.
- * @param {Function} [callback] params are (error, [updateWriteOpResult](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/UpdateResult.html))
- * @return {Query}
- * @see MongoDB docs https://www.mongodb.com/docs/manual/reference/command/update/#update-command-output
- * @see UpdateResult https://mongodb.github.io/node-mongodb-native/4.9/interfaces/UpdateResult.html
- * @see Query docs https://mongoosejs.com/docs/queries.html
- * @api public
- */
-
-Model.update = function update(conditions, doc, options, callback) {
- _checkContext(this, 'update');
-
- return _update(this, 'update', conditions, doc, options, callback);
-};
-
-/**
- * Same as `update()`, except MongoDB will update _all_ documents that match
+ * Same as `updateOne()`, except MongoDB will update _all_ documents that match
* `filter` (as opposed to just the first one) regardless of the value of
* the `multi` option.
*
@@ -4225,7 +3874,7 @@ Model.update = function update(conditions, doc, options, callback) {
* const res = await Person.updateMany({ name: /Stark$/ }, { isDeleted: true });
* res.matchedCount; // Number of documents matched
* res.modifiedCount; // Number of documents modified
- * res.acknowledged; // Boolean indicating everything went smoothly.
+ * res.acknowledged; // Boolean indicating the MongoDB server received the operation. This may be false if Mongoose did not send an update to the server because the update was empty.
* res.upsertedId; // null or an id containing a document that had to be upserted.
* res.upsertedCount; // Number indicating how many documents had to be upserted. Will either be 0 or 1.
*
@@ -4234,13 +3883,14 @@ Model.update = function update(conditions, doc, options, callback) {
* - `updateMany()`
*
* @param {Object} filter
- * @param {Object|Array} update
- * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#query_Query-setOptions)
+ * @param {Object|Array} update. If array, this update will be treated as an update pipeline and not casted.
+ * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions())
* @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict)
* @param {Boolean} [options.upsert=false] if true, and no documents found, insert a new document
- * @param {Object} [options.writeConcern=null] sets the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/) for replica sets. Overrides the [schema-level write concern](/docs/guide.html#writeConcern)
- * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Does nothing if schema-level timestamps are not set.
- * @param {Function} [callback] `function(error, res) {}` where `res` has 5 properties: `modifiedCount`, `matchedCount`, `acknowledged`, `upsertedId`, and `upsertedCount`.
+ * @param {Object} [options.writeConcern=null] sets the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/) for replica sets. Overrides the [schema-level write concern](https://mongoosejs.com/docs/guide.html#writeConcern)
+ * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Does nothing if schema-level timestamps are not set.
+ * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object.
+ * @param {Boolean} [options.overwriteDiscriminatorKey=false] Mongoose removes discriminator key updates from `update` by default, set `overwriteDiscriminatorKey` to `true` to allow updating the discriminator key
* @return {Query}
* @see Query docs https://mongoosejs.com/docs/queries.html
* @see MongoDB docs https://www.mongodb.com/docs/manual/reference/command/update/#update-command-output
@@ -4248,17 +3898,15 @@ Model.update = function update(conditions, doc, options, callback) {
* @api public
*/
-Model.updateMany = function updateMany(conditions, doc, options, callback) {
+Model.updateMany = function updateMany(conditions, doc, options) {
_checkContext(this, 'updateMany');
- return _update(this, 'updateMany', conditions, doc, options, callback);
+ return _update(this, 'updateMany', conditions, doc, options);
};
/**
- * Same as `update()`, except it does not support the `multi` or `overwrite`
- * options.
+ * Update _only_ the first document that matches `filter`.
*
- * - MongoDB will update _only_ the first document that matches `filter` regardless of the value of the `multi` option.
* - Use `replaceOne()` if you want to overwrite an entire document rather than using atomic operators like `$set`.
*
* #### Example:
@@ -4266,7 +3914,7 @@ Model.updateMany = function updateMany(conditions, doc, options, callback) {
* const res = await Person.updateOne({ name: 'Jean-Luc Picard' }, { ship: 'USS Enterprise' });
* res.matchedCount; // Number of documents matched
* res.modifiedCount; // Number of documents modified
- * res.acknowledged; // Boolean indicating everything went smoothly.
+ * res.acknowledged; // Boolean indicating the MongoDB server received the operation. This may be false if Mongoose did not send an update to the server because the update was empty.
* res.upsertedId; // null or an id containing a document that had to be upserted.
* res.upsertedCount; // Number indicating how many documents had to be upserted. Will either be 0 or 1.
*
@@ -4275,13 +3923,14 @@ Model.updateMany = function updateMany(conditions, doc, options, callback) {
* - `updateOne()`
*
* @param {Object} filter
- * @param {Object|Array} update
- * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#query_Query-setOptions)
+ * @param {Object|Array} update. If array, this update will be treated as an update pipeline and not casted.
+ * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions())
* @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict)
* @param {Boolean} [options.upsert=false] if true, and no documents found, insert a new document
- * @param {Object} [options.writeConcern=null] sets the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/) for replica sets. Overrides the [schema-level write concern](/docs/guide.html#writeConcern)
- * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Note that this allows you to overwrite timestamps. Does nothing if schema-level timestamps are not set.
- * @param {Function} [callback] params are (error, writeOpResult)
+ * @param {Object} [options.writeConcern=null] sets the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/) for replica sets. Overrides the [schema-level write concern](https://mongoosejs.com/docs/guide.html#writeConcern)
+ * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Note that this allows you to overwrite timestamps. Does nothing if schema-level timestamps are not set.
+ * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object.
+ * @param {Boolean} [options.overwriteDiscriminatorKey=false] Mongoose removes discriminator key updates from `update` by default, set `overwriteDiscriminatorKey` to `true` to allow updating the discriminator key
* @return {Query}
* @see Query docs https://mongoosejs.com/docs/queries.html
* @see MongoDB docs https://www.mongodb.com/docs/manual/reference/command/update/#update-command-output
@@ -4289,22 +3938,21 @@ Model.updateMany = function updateMany(conditions, doc, options, callback) {
* @api public
*/
-Model.updateOne = function updateOne(conditions, doc, options, callback) {
+Model.updateOne = function updateOne(conditions, doc, options) {
_checkContext(this, 'updateOne');
- return _update(this, 'updateOne', conditions, doc, options, callback);
+ return _update(this, 'updateOne', conditions, doc, options);
};
/**
- * Same as `update()`, except MongoDB replace the existing document with the
- * given document (no atomic operators like `$set`).
+ * Replace the existing document with the given document (no atomic operators like `$set`).
*
* #### Example:
*
* const res = await Person.replaceOne({ _id: 24601 }, { name: 'Jean Valjean' });
* res.matchedCount; // Number of documents matched
* res.modifiedCount; // Number of documents modified
- * res.acknowledged; // Boolean indicating everything went smoothly.
+ * res.acknowledged; // Boolean indicating the MongoDB server received the operation.
* res.upsertedId; // null or an id containing a document that had to be upserted.
* res.upsertedCount; // Number indicating how many documents had to be upserted. Will either be 0 or 1.
*
@@ -4314,12 +3962,12 @@ Model.updateOne = function updateOne(conditions, doc, options, callback) {
*
* @param {Object} filter
* @param {Object} doc
- * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#query_Query-setOptions)
+ * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions())
* @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict)
* @param {Boolean} [options.upsert=false] if true, and no documents found, insert a new document
- * @param {Object} [options.writeConcern=null] sets the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/) for replica sets. Overrides the [schema-level write concern](/docs/guide.html#writeConcern)
- * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Does nothing if schema-level timestamps are not set.
- * @param {Function} [callback] `function(error, res) {}` where `res` has 3 properties: `n`, `nModified`, `ok`.
+ * @param {Object} [options.writeConcern=null] sets the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/) for replica sets. Overrides the [schema-level write concern](https://mongoosejs.com/docs/guide.html#writeConcern)
+ * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Does nothing if schema-level timestamps are not set.
+ * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object.
* @return {Query}
* @see Query docs https://mongoosejs.com/docs/queries.html
* @see UpdateResult https://mongodb.github.io/node-mongodb-native/4.9/interfaces/UpdateResult.html
@@ -4327,7 +3975,7 @@ Model.updateOne = function updateOne(conditions, doc, options, callback) {
* @api public
*/
-Model.replaceOne = function replaceOne(conditions, doc, options, callback) {
+Model.replaceOne = function replaceOne(conditions, doc, options) {
_checkContext(this, 'replaceOne');
const versionKey = this && this.schema && this.schema.options && this.schema.options.versionKey || null;
@@ -4335,7 +3983,7 @@ Model.replaceOne = function replaceOne(conditions, doc, options, callback) {
doc[versionKey] = 0;
}
- return _update(this, 'replaceOne', conditions, doc, options, callback);
+ return _update(this, 'replaceOne', conditions, doc, options);
};
/**
@@ -4344,149 +3992,31 @@ Model.replaceOne = function replaceOne(conditions, doc, options, callback) {
* @api private
*/
-function _update(model, op, conditions, doc, options, callback) {
+function _update(model, op, conditions, doc, options) {
const mq = new model.Query({}, {}, model, model.collection);
- callback = model.$handleCallbackError(callback);
+
// gh-2406
// make local deep copy of conditions
if (conditions instanceof Document) {
conditions = conditions.toObject();
} else {
- conditions = utils.clone(conditions);
+ conditions = clone(conditions);
}
- options = typeof options === 'function' ? options : utils.clone(options);
+ options = typeof options === 'function' ? options : clone(options);
const versionKey = model &&
model.schema &&
model.schema.options &&
model.schema.options.versionKey || null;
- _decorateUpdateWithVersionKey(doc, options, versionKey);
+ decorateUpdateWithVersionKey(doc, options, versionKey);
- return mq[op](conditions, doc, options, callback);
+ return mq[op](conditions, doc, options);
}
/**
- * Executes a mapReduce command.
- *
- * `opts` is an object specifying all mapReduce options as well as the map and reduce functions. All options are delegated to the driver implementation. See [node-mongodb-native mapReduce() documentation](https://mongodb.github.io/node-mongodb-native/4.9/classes/Collection.html#mapReduce) for more detail about options.
- *
- * This function does not trigger any middleware.
- *
- * #### Example:
- *
- * const opts = {};
- * // `map()` and `reduce()` are run on the MongoDB server, not Node.js,
- * // these functions are converted to strings
- * opts.map = function () { emit(this.name, 1) };
- * opts.reduce = function (k, vals) { return vals.length };
- * User.mapReduce(opts, function (err, results) {
- * console.log(results)
- * })
- *
- * If `opts.out` is set to `replace`, `merge`, or `reduce`, a Model instance is returned that can be used for further querying. Queries run against this model are all executed with the [`lean` option](/docs/tutorials/lean.html); meaning only the js object is returned and no Mongoose magic is applied (getters, setters, etc).
- *
- * #### Example:
- *
- * const opts = {};
- * // You can also define `map()` and `reduce()` as strings if your
- * // linter complains about `emit()` not being defined
- * opts.map = 'function () { emit(this.name, 1) }';
- * opts.reduce = 'function (k, vals) { return vals.length }';
- * opts.out = { replace: 'createdCollectionNameForResults' }
- * opts.verbose = true;
- *
- * User.mapReduce(opts, function (err, model, stats) {
- * console.log('map reduce took %d ms', stats.processtime)
- * model.find().where('value').gt(10).exec(function (err, docs) {
- * console.log(docs);
- * });
- * })
- *
- * // `mapReduce()` returns a promise. However, ES6 promises can only
- * // resolve to exactly one value,
- * opts.resolveToObject = true;
- * const promise = User.mapReduce(opts);
- * promise.then(function (res) {
- * const model = res.model;
- * const stats = res.stats;
- * console.log('map reduce took %d ms', stats.processtime)
- * return model.find().where('value').gt(10).exec();
- * }).then(function (docs) {
- * console.log(docs);
- * }).then(null, handleError).end()
- *
- * @param {Object} opts an object specifying map-reduce options
- * @param {Boolean} [opts.verbose=false] provide statistics on job execution time
- * @param {ReadPreference|String} [opts.readPreference] a read-preference string or a read-preference instance
- * @param {Boolean} [opts.jsMode=false] it is possible to make the execution stay in JS. Provided in MongoDB > 2.0.X
- * @param {Object} [opts.scope] scope variables exposed to map/reduce/finalize during execution
- * @param {Function} [opts.finalize] finalize function
- * @param {Boolean} [opts.keeptemp=false] keep temporary data
- * @param {Number} [opts.limit] max number of documents
- * @param {Object} [opts.sort] sort input objects using this key
- * @param {Object} [opts.query] query filter object
- * @param {Object} [opts.out] sets the output target for the map reduce job
- * @param {Number} [opts.out.inline=1] the results are returned in an array
- * @param {String} [opts.out.replace] add the results to collectionName: the results replace the collection
- * @param {String} [opts.out.reduce] add the results to collectionName: if dups are detected, uses the reducer / finalize functions
- * @param {String} [opts.out.merge] add the results to collectionName: if dups exist the new docs overwrite the old
- * @param {Function} [callback] optional callback
- * @see MongoDB MapReduce https://www.mongodb.com/docs/manual/core/map-reduce/
- * @return {Promise}
- * @api public
- */
-
-Model.mapReduce = function mapReduce(opts, callback) {
- _checkContext(this, 'mapReduce');
-
- callback = this.$handleCallbackError(callback);
-
- return this.db.base._promiseOrCallback(callback, cb => {
- cb = this.$wrapCallback(cb);
-
- if (!Model.mapReduce.schema) {
- const opts = { _id: false, id: false, strict: false };
- Model.mapReduce.schema = new Schema({}, opts);
- }
-
- if (!opts.out) opts.out = { inline: 1 };
- if (opts.verbose !== false) opts.verbose = true;
-
- opts.map = String(opts.map);
- opts.reduce = String(opts.reduce);
-
- if (opts.query) {
- let q = new this.Query(opts.query);
- q.cast(this);
- opts.query = q._conditions;
- q = undefined;
- }
-
- this.$__collection.mapReduce(null, null, opts, (err, res) => {
- if (err) {
- return cb(err);
- }
- if (res.collection) {
- // returned a collection, convert to Model
- const model = Model.compile('_mapreduce_' + res.collection.collectionName,
- Model.mapReduce.schema, res.collection.collectionName, this.db,
- this.base);
-
- model._mapreduce = true;
- res.model = model;
-
- return cb(null, res);
- }
-
- cb(null, res);
- });
- }, this.events);
-};
-
-/**
- * Performs [aggregations](https://www.mongodb.com/docs/manual/applications/aggregation/) on the models collection.
+ * Performs [aggregations](https://www.mongodb.com/docs/manual/aggregation/) on the models collection.
*
- * If a `callback` is passed, the `aggregate` is executed and a `Promise` is returned. If a callback is not passed, the `aggregate` itself is returned.
+ * The `aggregate` itself is returned.
*
* This function triggers the following middleware.
*
@@ -4516,37 +4046,23 @@ Model.mapReduce = function mapReduce(opts, callback) {
*
* #### More About Aggregations:
*
- * - [Mongoose `Aggregate`](/docs/api/aggregate.html)
+ * - [Mongoose `Aggregate`](https://mongoosejs.com/docs/api/aggregate.html)
* - [An Introduction to Mongoose Aggregate](https://masteringjs.io/tutorials/mongoose/aggregate)
* - [MongoDB Aggregation docs](https://www.mongodb.com/docs/manual/applications/aggregation/)
*
- * @see Aggregate #aggregate_Aggregate
+ * @see Aggregate https://mongoosejs.com/docs/api/aggregate.html#Aggregate()
* @see MongoDB https://www.mongodb.com/docs/manual/applications/aggregation/
* @param {Array} [pipeline] aggregation pipeline as an array of objects
* @param {Object} [options] aggregation options
- * @param {Function} [callback]
* @return {Aggregate}
* @api public
*/
-Model.aggregate = function aggregate(pipeline, options, callback) {
+Model.aggregate = function aggregate(pipeline, options) {
_checkContext(this, 'aggregate');
- if (arguments.length > 3 || (pipeline && pipeline.constructor && pipeline.constructor.name) === 'Object') {
- throw new MongooseError('Mongoose 5.x disallows passing a spread of operators ' +
- 'to `Model.aggregate()`. Instead of ' +
- '`Model.aggregate({ $match }, { $skip })`, do ' +
- '`Model.aggregate([{ $match }, { $skip }])`');
- }
-
- if (typeof pipeline === 'function') {
- callback = pipeline;
- pipeline = [];
- }
-
- if (typeof options === 'function') {
- callback = options;
- options = null;
+ if (typeof options === 'function' || typeof arguments[2] === 'function') {
+ throw new MongooseError('Model.aggregate() no longer accepts a callback');
}
const aggregate = new Aggregate(pipeline || []);
@@ -4555,14 +4071,6 @@ Model.aggregate = function aggregate(pipeline, options, callback) {
aggregate.option(options);
}
- if (typeof callback === 'undefined') {
- return aggregate;
- }
-
- callback = this.$handleCallbackError(callback);
- callback = this.$wrapCallback(callback);
-
- aggregate.exec(callback);
return aggregate;
};
@@ -4585,58 +4093,82 @@ Model.aggregate = function aggregate(pipeline, options, callback) {
* }
*
* @param {Object} obj
- * @param {Array|String} pathsToValidate
+ * @param {Object|Array|String} pathsOrOptions
* @param {Object} [context]
- * @param {Function} [callback]
- * @return {Promise|undefined}
+ * @return {Promise} casted and validated copy of `obj` if validation succeeded
* @api public
*/
-Model.validate = function validate(obj, pathsToValidate, context, callback) {
+Model.validate = async function validate(obj, pathsOrOptions, context) {
if ((arguments.length < 3) || (arguments.length === 3 && typeof arguments[2] === 'function')) {
// For convenience, if we're validating a document or an object, make `context` default to
// the model so users don't have to always pass `context`, re: gh-10132, gh-10346
context = obj;
}
+ if (typeof context === 'function' || typeof arguments[3] === 'function') {
+ throw new MongooseError('Model.validate() no longer accepts a callback');
+ }
- return this.db.base._promiseOrCallback(callback, cb => {
- let schema = this.schema;
- const discriminatorKey = schema.options.discriminatorKey;
- if (schema.discriminators != null && obj != null && obj[discriminatorKey] != null) {
- schema = getSchemaDiscriminatorByValue(schema, obj[discriminatorKey]) || schema;
- }
- let paths = Object.keys(schema.paths);
-
- if (pathsToValidate != null) {
- const _pathsToValidate = typeof pathsToValidate === 'string' ? new Set(pathsToValidate.split(' ')) : new Set(pathsToValidate);
- paths = paths.filter(p => {
- const pieces = p.split('.');
- let cur = pieces[0];
+ let schema = this.schema;
+ const discriminatorKey = schema.options.discriminatorKey;
+ if (schema.discriminators != null && obj != null && obj[discriminatorKey] != null) {
+ schema = getSchemaDiscriminatorByValue(schema, obj[discriminatorKey]) || schema;
+ }
+ let paths = Object.keys(schema.paths);
- for (const piece of pieces) {
- if (_pathsToValidate.has(cur)) {
- return true;
+ if (pathsOrOptions != null) {
+ const _pathsToValidate = typeof pathsOrOptions === 'string' ? new Set(pathsOrOptions.split(' ')) : Array.isArray(pathsOrOptions) ? new Set(pathsOrOptions) : new Set(paths);
+ paths = paths.filter(p => {
+ if (pathsOrOptions.pathsToSkip) {
+ if (Array.isArray(pathsOrOptions.pathsToSkip)) {
+ if (pathsOrOptions.pathsToSkip.find(x => x == p)) {
+ return false;
}
- cur += '.' + piece;
+ } else if (typeof pathsOrOptions.pathsToSkip == 'string') {
+ if (pathsOrOptions.pathsToSkip.includes(p)) {
+ return false;
+ }
+ }
+ }
+ const pieces = p.split('.');
+ let cur = pieces[0];
+
+ for (const piece of pieces) {
+ if (_pathsToValidate.has(cur)) {
+ return true;
}
+ cur += '.' + piece;
+ }
- return _pathsToValidate.has(p);
- });
+ return _pathsToValidate.has(p);
+ });
+ }
+
+ for (const path of paths) {
+ const schemaType = schema.path(path);
+ if (!schemaType || !schemaType.$isMongooseArray || schemaType.$isMongooseDocumentArray) {
+ continue;
}
- for (const path of paths) {
- const schemaType = schema.path(path);
- if (!schemaType || !schemaType.$isMongooseArray || schemaType.$isMongooseDocumentArray) {
- continue;
- }
+ const val = get(obj, path);
+ pushNestedArrayPaths(paths, val, path);
+ }
- const val = get(obj, path);
- pushNestedArrayPaths(paths, val, path);
+ let error = null;
+ paths = new Set(paths);
+
+ try {
+ obj = this.castObject(obj);
+ } catch (err) {
+ error = err;
+ for (const key of Object.keys(error.errors || {})) {
+ paths.delete(key);
}
+ }
- let remaining = paths.length;
- let error = null;
+ let remaining = paths.size;
+ return new Promise((resolve, reject) => {
for (const path of paths) {
const schemaType = schema.path(path);
if (schemaType == null) {
@@ -4650,20 +4182,7 @@ Model.validate = function validate(obj, pathsToValidate, context, callback) {
cur = cur[pieces[i]];
}
- let val = get(obj, path, void 0);
-
- if (val != null) {
- try {
- val = schemaType.cast(val);
- cur[pieces[pieces.length - 1]] = val;
- } catch (err) {
- error = error || new ValidationError();
- error.addError(path, err);
-
- _checkDone();
- continue;
- }
- }
+ const val = get(obj, path, void 0);
schemaType.doValidate(val, err => {
if (err) {
@@ -4676,7 +4195,11 @@ Model.validate = function validate(obj, pathsToValidate, context, callback) {
function _checkDone() {
if (--remaining <= 0) {
- return cb(error);
+ if (error) {
+ reject(error);
+ } else {
+ resolve(obj);
+ }
}
}
});
@@ -4697,6 +4220,7 @@ Model.validate = function validate(obj, pathsToValidate, context, callback) {
* - options: optional query options like sort, limit, etc
* - justOne: optional boolean, if true Mongoose will always set `path` to a document, or `null` if no document was found. If false, Mongoose will always set `path` to an array, which will be empty if no documents are found. Inferred from schema by default.
* - strictPopulate: optional boolean, set to `false` to allow populating paths that aren't in the schema.
+ * - forceRepopulate: optional boolean, defaults to `true`. Set to `false` to prevent Mongoose from repopulating paths that are already populated
*
* #### Example:
*
@@ -4723,9 +4247,9 @@ Model.validate = function validate(obj, pathsToValidate, context, callback) {
* @param {Document|Array} docs Either a single document or array of documents to populate.
* @param {Object|String} options Either the paths to populate or an object specifying all parameters
* @param {string} [options.path=null] The path to populate.
- * @param {string|PopulateOptions} [options.populate=null] Recursively populate paths in the populated documents. See [deep populate docs](/docs/populate.html#deep-populate).
+ * @param {string|PopulateOptions} [options.populate=null] Recursively populate paths in the populated documents. See [deep populate docs](https://mongoosejs.com/docs/populate.html#deep-populate).
* @param {boolean} [options.retainNullValues=false] By default, Mongoose removes null and undefined values from populated arrays. Use this option to make `populate()` retain `null` and `undefined` array entries.
- * @param {boolean} [options.getters=false] If true, Mongoose will call any getters defined on the `localField`. By default, Mongoose gets the raw value of `localField`. For example, you would need to set this option to `true` if you wanted to [add a `lowercase` getter to your `localField`](/docs/schematypes.html#schematype-options).
+ * @param {boolean} [options.getters=false] If true, Mongoose will call any getters defined on the `localField`. By default, Mongoose gets the raw value of `localField`. For example, you would need to set this option to `true` if you wanted to [add a `lowercase` getter to your `localField`](https://mongoosejs.com/docs/schematypes.html#schematype-options).
* @param {boolean} [options.clone=false] When you do `BlogPost.find().populate('author')`, blog posts with the same author will share 1 copy of an `author` doc. Enable this option to make Mongoose clone populated docs before assigning them.
* @param {Object|Function} [options.match=null] Add an additional filter to the populate query. Can be a filter object containing [MongoDB query syntax](https://www.mongodb.com/docs/manual/tutorial/query-documents/), or a function that returns a filter object.
* @param {Boolean} [options.skipInvalidIds=false] By default, Mongoose throws a cast error if `localField` and `foreignField` schemas don't line up. If you enable this option, Mongoose will instead filter out any `localField` properties that cannot be casted to `foreignField`'s schema type.
@@ -4733,73 +4257,44 @@ Model.validate = function validate(obj, pathsToValidate, context, callback) {
* @param {Boolean} [options.strictPopulate=true] Set to false to allow populating paths that aren't defined in the given model's schema.
* @param {Object} [options.options=null] Additional options like `limit` and `lean`.
* @param {Function} [options.transform=null] Function that Mongoose will call on every populated document that allows you to transform the populated document.
- * @param {Function} [callback(err,doc)] Optional callback, executed upon completion. Receives `err` and the `doc(s)`.
+ * @param {Boolean} [options.forceRepopulate=true] Set to `false` to prevent Mongoose from repopulating paths that are already populated
* @return {Promise}
* @api public
*/
-Model.populate = function(docs, paths, callback) {
+Model.populate = async function populate(docs, paths) {
_checkContext(this, 'populate');
-
- const _this = this;
-
+ if (typeof paths === 'function' || typeof arguments[2] === 'function') {
+ throw new MongooseError('Model.populate() no longer accepts a callback');
+ }
// normalized paths
paths = utils.populate(paths);
- // data that should persist across subPopulate calls
- const cache = {};
-
- callback = this.$handleCallbackError(callback);
- return this.db.base._promiseOrCallback(callback, cb => {
- cb = this.$wrapCallback(cb);
- _populate(_this, docs, paths, cache, cb);
- }, this.events);
-};
-
-/**
- * Populate helper
- *
- * @param {Model} model the model to use
- * @param {Document|Array} docs Either a single document or array of documents to populate.
- * @param {Object} paths
- * @param {never} cache Unused
- * @param {Function} [callback] Optional callback, executed upon completion. Receives `err` and the `doc(s)`.
- * @return {Function}
- * @api private
- */
-function _populate(model, docs, paths, cache, callback) {
- let pending = paths.length;
if (paths.length === 0) {
- return callback(null, docs);
+ return docs;
}
+
// each path has its own query options and must be executed separately
+ const promises = [];
for (const path of paths) {
- populate(model, docs, path, next);
+ promises.push(_populatePath(this, docs, path));
}
+ await Promise.all(promises);
- function next(err) {
- if (err) {
- return callback(err, null);
- }
- if (--pending) {
- return;
- }
- callback(null, docs);
- }
-}
+ return docs;
+};
/*!
- * Populates `docs`
+ * Populates `docs` for a single `populateOptions` instance.
*/
const excludeIdReg = /\s?-_id\s?/;
const excludeIdRegGlobal = /\s?-_id\s?/g;
-function populate(model, docs, options, callback) {
- const populateOptions = { ...options };
- if (options.strictPopulate == null) {
- if (options._localModel != null && options._localModel.schema._userProvidedOptions.strictPopulate != null) {
- populateOptions.strictPopulate = options._localModel.schema._userProvidedOptions.strictPopulate;
- } else if (options._localModel != null && model.base.options.strictPopulate != null) {
+async function _populatePath(model, docs, populateOptions) {
+ if (populateOptions.strictPopulate == null) {
+ if (populateOptions._localModel != null && populateOptions._localModel.schema._userProvidedOptions.strictPopulate != null) {
+ populateOptions.strictPopulate = populateOptions._localModel.schema._userProvidedOptions.strictPopulate;
+ } else if (populateOptions._localModel != null && model.base.options.strictPopulate != null) {
populateOptions.strictPopulate = model.base.options.strictPopulate;
} else if (model.base.options.strictPopulate != null) {
populateOptions.strictPopulate = model.base.options.strictPopulate;
@@ -4811,15 +4306,12 @@ function populate(model, docs, options, callback) {
docs = [docs];
}
if (docs.length === 0 || docs.every(utils.isNullOrUndefined)) {
- return callback();
+ return;
}
const modelsMap = getModelsMapForPopulate(model, docs, populateOptions);
-
if (modelsMap instanceof MongooseError) {
- return immediate(function() {
- callback(modelsMap);
- });
+ throw modelsMap;
}
const len = modelsMap.length;
let vals = [];
@@ -4829,7 +4321,6 @@ function populate(model, docs, options, callback) {
return undefined !== item;
}
- let _remaining = len;
let hasOne = false;
const params = [];
for (let i = 0; i < len; ++i) {
@@ -4860,7 +4351,6 @@ function populate(model, docs, options, callback) {
// Ensure that we set to 0 or empty array even
// if we don't actually execute a query to make sure there's a value
// and we know this path was populated for future sets. See gh-7731, gh-8230
- --_remaining;
_assign(model, [], mod, assignmentOpts);
continue;
}
@@ -4877,9 +4367,11 @@ function populate(model, docs, options, callback) {
// _id back off before returning the result.
if (typeof select === 'string') {
select = select.replace(excludeIdRegGlobal, ' ');
+ } else if (Array.isArray(select)) {
+ select = select.filter(field => field !== '-_id');
} else {
// preserve original select conditions by copying
- select = utils.object.shallowCopy(select);
+ select = { ...select };
delete select._id;
}
}
@@ -4889,63 +4381,50 @@ function populate(model, docs, options, callback) {
} else if (mod.options.limit != null) {
assignmentOpts.originalLimit = mod.options.limit;
}
- params.push([mod, match, select, assignmentOpts, _next]);
+ params.push([mod, match, select, assignmentOpts]);
}
if (!hasOne) {
// If models but no docs, skip further deep populate.
if (modelsMap.length !== 0) {
- return callback();
+ return;
}
- // If no models to populate but we have a nested populate,
- // keep trying, re: gh-8946
+ // If no models and no docs to populate but we have a nested populate,
+ // probably a case of unnecessarily populating a non-ref path re: gh-8946
if (populateOptions.populate != null) {
const opts = utils.populate(populateOptions.populate).map(pop => Object.assign({}, pop, {
path: populateOptions.path + '.' + pop.path
}));
- return model.populate(docs, opts, callback);
+ return model.populate(docs, opts);
}
- return callback();
+ return;
}
+ const promises = [];
for (const arr of params) {
- _execPopulateQuery.apply(null, arr);
- }
- function _next(err, valsFromDb) {
- if (err != null) {
- return callback(err, null);
- }
- vals = vals.concat(valsFromDb);
- if (--_remaining === 0) {
- _done();
- }
+ promises.push(_execPopulateQuery.apply(null, arr).then(valsFromDb => { vals = vals.concat(valsFromDb); }));
}
- function _done() {
- for (const arr of params) {
- const mod = arr[0];
- const assignmentOpts = arr[3];
- for (const val of vals) {
- mod.options._childDocs.push(val);
- }
- try {
- _assign(model, vals, mod, assignmentOpts);
- } catch (err) {
- return callback(err);
- }
- }
+ await Promise.all(promises);
- for (const arr of params) {
- removeDeselectedForeignField(arr[0].foreignField, arr[0].options, vals);
+ for (const arr of params) {
+ const mod = arr[0];
+ const assignmentOpts = arr[3];
+ for (const val of vals) {
+ mod.options._childDocs.push(val);
}
- for (const arr of params) {
- const mod = arr[0];
- if (mod.options && mod.options.options && mod.options.options._leanTransform) {
- for (const doc of vals) {
- mod.options.options._leanTransform(doc);
- }
+ _assign(model, vals, mod, assignmentOpts);
+ }
+
+ for (const arr of params) {
+ removeDeselectedForeignField(arr[0].foreignField, arr[0].options, vals);
+ }
+ for (const arr of params) {
+ const mod = arr[0];
+ if (mod.options && mod.options.options && mod.options.options._leanTransform) {
+ for (const doc of vals) {
+ mod.options.options._leanTransform(doc);
}
}
- callback();
}
}
@@ -4953,8 +4432,8 @@ function populate(model, docs, options, callback) {
* ignore
*/
-function _execPopulateQuery(mod, match, select, assignmentOpts, callback) {
- let subPopulate = utils.clone(mod.options.populate);
+function _execPopulateQuery(mod, match, select) {
+ let subPopulate = clone(mod.options.populate);
const queryOptions = Object.assign({
skip: mod.options.skip,
limit: mod.options.limit,
@@ -4971,13 +4450,15 @@ function _execPopulateQuery(mod, match, select, assignmentOpts, callback) {
} else if (queryOptions.limit != null) {
queryOptions.limit = queryOptions.limit * mod.ids.length;
}
+
const query = mod.model.find(match, select, queryOptions);
// If we're doing virtual populate and projection is inclusive and foreign
// field is not selected, automatically select it because mongoose needs it.
// If projection is exclusive and client explicitly unselected the foreign
// field, that's the client's fault.
for (const foreignField of mod.foreignField) {
- if (foreignField !== '_id' && query.selectedInclusively() &&
+ if (foreignField !== '_id' &&
+ query.selectedInclusively() &&
!isPathSelectedInclusive(query._fields, foreignField)) {
query.select(foreignField);
}
@@ -5017,17 +4498,14 @@ function _execPopulateQuery(mod, match, select, assignmentOpts, callback) {
query.populate(subPopulate);
}
- query.exec((err, docs) => {
- if (err != null) {
- return callback(err);
- }
-
- for (const val of docs) {
- leanPopulateMap.set(val, mod.model);
+ return query.exec().then(
+ docs => {
+ for (const val of docs) {
+ leanPopulateMap.set(val, mod.model);
+ }
+ return docs;
}
- callback(null, docs);
- });
-
+ );
}
/*!
@@ -5049,7 +4527,7 @@ function _assign(model, vals, mod, assignmentOpts) {
let val;
// Clone because `assignRawDocsToIdStructure` will mutate the array
- const allIds = utils.clone(mod.allIds);
+ const allIds = clone(mod.allIds);
// optimization:
// record the document positions as returned by
// the query result.
@@ -5065,7 +4543,7 @@ function _assign(model, vals, mod, assignmentOpts) {
for (let __val of _val) {
if (__val instanceof Document) {
- __val = __val._id;
+ __val = __val._doc._id;
}
key = String(__val);
if (rawDocs[key]) {
@@ -5088,7 +4566,7 @@ function _assign(model, vals, mod, assignmentOpts) {
}
} else {
if (_val instanceof Document) {
- _val = _val._id;
+ _val = _val._doc._id;
}
key = String(_val);
if (rawDocs[key]) {
@@ -5097,7 +4575,7 @@ function _assign(model, vals, mod, assignmentOpts) {
rawOrder[key].push(i);
} else if (isVirtual ||
rawDocs[key].constructor !== val.constructor ||
- String(rawDocs[key]._id) !== String(val._id)) {
+ (rawDocs[key] instanceof Document ? String(rawDocs[key]._doc._id) : String(rawDocs[key]._id)) !== (val instanceof Document ? String(val._doc._id) : String(val._id))) {
// May need to store multiple docs with the same id if there's multiple models
// if we have discriminators or a ref function. But avoid converting to an array
// if we have multiple queries on the same model because of `perDocumentLimit` re: gh-9906
@@ -5111,7 +4589,7 @@ function _assign(model, vals, mod, assignmentOpts) {
}
// flag each as result of population
if (!lean) {
- val.$__.wasPopulated = val.$__.wasPopulated || true;
+ val.$__.wasPopulated = val.$__.wasPopulated || { value: _val };
}
}
}
@@ -5212,8 +4690,6 @@ Model.compile = function compile(name, schema, collectionName, connection, base)
schema._preCompile();
- model.prototype.$__setSchema(schema);
-
const _userProvidedOptions = schema._userProvidedOptions || {};
const collectionOptions = {
@@ -5226,13 +4702,16 @@ Model.compile = function compile(name, schema, collectionName, connection, base)
collectionOptions.autoCreate = schema.options.autoCreate;
}
- model.prototype.collection = connection.collection(
+ const collection = connection.collection(
collectionName,
collectionOptions
);
- model.prototype.$collection = model.prototype.collection;
- model.prototype[modelCollectionSymbol] = model.prototype.collection;
+ model.prototype.collection = collection;
+ model.prototype.$collection = collection;
+ model.prototype[modelCollectionSymbol] = collection;
+
+ model.prototype.$__setSchema(schema);
// apply methods and statics
applyMethods(model, schema);
@@ -5241,8 +4720,8 @@ Model.compile = function compile(name, schema, collectionName, connection, base)
applyStaticHooks(model, schema.s.hooks, schema.statics);
model.schema = model.prototype.$__schema;
- model.collection = model.prototype.collection;
- model.$__collection = model.collection;
+ model.collection = collection;
+ model.$__collection = collection;
// Create custom query constructor
model.Query = function() {
@@ -5251,12 +4730,40 @@ Model.compile = function compile(name, schema, collectionName, connection, base)
Object.setPrototypeOf(model.Query.prototype, Query.prototype);
model.Query.base = Query.base;
model.Query.prototype.constructor = Query;
- applyQueryMiddleware(model.Query, model);
+ model._applyQueryMiddleware();
applyQueryMethods(model, schema.query);
return model;
};
+/**
+ * Update this model to use the new connection, including updating all internal
+ * references and creating a new `Collection` instance using the new connection.
+ * Not for external use, only used by `setDriver()` to ensure that you can still
+ * call `setDriver()` after creating a model using `mongoose.model()`.
+ *
+ * @param {Connection} newConnection the new connection to use
+ * @api private
+ */
+
+Model.$__updateConnection = function $__updateConnection(newConnection) {
+ this.db = newConnection;
+ this.prototype.db = newConnection;
+ this.prototype[modelDbSymbol] = newConnection;
+
+ const collection = newConnection.collection(
+ this.collection.collectionName,
+ this.collection.opts
+ );
+
+ this.prototype.collection = collection;
+ this.prototype.$collection = collection;
+ this.prototype[modelCollectionSymbol] = collection;
+
+ this.collection = collection;
+ this.$__collection = collection;
+};
+
/**
* Register custom query methods for this model
*
@@ -5334,54 +4841,47 @@ Model.__subclass = function subclass(conn, schema, collection) {
Model.collection = Model.prototype.collection;
Model.$__collection = Model.collection;
// Errors handled internally, so ignore
- Model.init(() => {});
+ Model.init().catch(() => {});
return Model;
};
-Model.$handleCallbackError = function(callback) {
- if (callback == null) {
- return callback;
- }
- if (typeof callback !== 'function') {
- throw new MongooseError('Callback must be a function, got ' + callback);
- }
-
- const _this = this;
- return function() {
- immediate(() => {
- try {
- callback.apply(null, arguments);
- } catch (error) {
- _this.emit('error', error);
- }
- });
- };
-};
-
/**
- * ignore
+ * Apply changes made to this model's schema after this model was compiled.
+ * By default, adding virtuals and other properties to a schema after the model is compiled does nothing.
+ * Call this function to apply virtuals and properties that were added later.
*
- * @param {Function} callback
- * @api private
- * @method $wrapCallback
+ * #### Example:
+ *
+ * const schema = new mongoose.Schema({ field: String });
+ * const TestModel = mongoose.model('Test', schema);
+ * TestModel.schema.virtual('myVirtual').get(function() {
+ * return this.field + ' from myVirtual';
+ * });
+ * const doc = new TestModel({ field: 'Hello' });
+ * doc.myVirtual; // undefined
+ *
+ * TestModel.recompileSchema();
+ * doc.myVirtual; // 'Hello from myVirtual'
+ *
+ * @return {undefined}
+ * @api public
* @memberOf Model
* @static
+ * @method recompileSchema
*/
-Model.$wrapCallback = function(callback) {
- const serverSelectionError = new ServerSelectionError();
- const _this = this;
+Model.recompileSchema = function recompileSchema() {
+ this.prototype.$__setSchema(this.schema);
- return function(err) {
- if (err != null && err.name === 'MongoServerSelectionError') {
- arguments[0] = serverSelectionError.assimilateError(err);
- }
- if (err != null && err.name === 'MongoNetworkTimeoutError' && err.message.endsWith('timed out')) {
- _this.db.emit('timeout');
+ if (this.schema._applyDiscriminators != null) {
+ for (const disc of this.schema._applyDiscriminators.keys()) {
+ this.discriminator(disc, this.schema._applyDiscriminators.get(disc));
}
+ }
- return callback.apply(null, arguments);
- };
+ delete this.schema._defaultToObjectOptionsMap;
+
+ applyEmbeddedDiscriminators(this.schema, new WeakSet(), true);
};
/**
@@ -5401,11 +4901,63 @@ Model.inspect = function() {
return `Model { ${this.modelName} }`;
};
+/**
+ * Return the MongoDB namespace for this model as a string. The namespace is the database name, followed by '.', followed by the collection name.
+ *
+ * #### Example:
+ *
+ * const conn = mongoose.createConnection('mongodb://127.0.0.1:27017/mydb');
+ * const TestModel = conn.model('Test', mongoose.Schema({ name: String }));
+ *
+ * TestModel.namespace(); // 'mydb.tests'
+ *
+ * @api public
+ */
+
+Model.namespace = function namespace() {
+ return this.db.name + '.' + this.collection.collectionName;
+};
+
if (util.inspect.custom) {
// Avoid Node deprecation warning DEP0079
Model[util.inspect.custom] = Model.inspect;
}
+/*!
+ * Applies query middleware from this model's schema to this model's
+ * Query constructor.
+ */
+
+Model._applyQueryMiddleware = function _applyQueryMiddleware() {
+ const Query = this.Query;
+ const queryMiddleware = this.schema.s.hooks.filter(hook => {
+ const contexts = _getContexts(hook);
+ if (hook.name === 'validate') {
+ return !!contexts.query;
+ }
+ if (hook.name === 'deleteOne' || hook.name === 'updateOne') {
+ return !!contexts.query || Object.keys(contexts).length === 0;
+ }
+ if (hook.query != null || hook.document != null) {
+ return !!hook.query;
+ }
+ return true;
+ });
+
+ Query.prototype._queryMiddleware = queryMiddleware;
+};
+
+function _getContexts(hook) {
+ const ret = {};
+ if (hook.hasOwnProperty('query')) {
+ ret.query = hook.query;
+ }
+ if (hook.hasOwnProperty('document')) {
+ ret.document = hook.document;
+ }
+ return ret;
+}
+
/*!
* Module exports.
*/
diff --git a/lib/modifiedPathsSnapshot.js b/lib/modifiedPathsSnapshot.js
new file mode 100644
index 00000000000..54d6b30d70b
--- /dev/null
+++ b/lib/modifiedPathsSnapshot.js
@@ -0,0 +1,9 @@
+'use strict';
+
+module.exports = class ModifiedPathsSnapshot {
+ constructor(subdocSnapshot, activePaths, version) {
+ this.subdocSnapshot = subdocSnapshot;
+ this.activePaths = activePaths;
+ this.version = version;
+ }
+};
diff --git a/lib/mongoose.js b/lib/mongoose.js
new file mode 100644
index 00000000000..f3b5f0948bd
--- /dev/null
+++ b/lib/mongoose.js
@@ -0,0 +1,1358 @@
+'use strict';
+
+/*!
+ * Module dependencies.
+ */
+
+const Document = require('./document');
+const EventEmitter = require('events').EventEmitter;
+const Kareem = require('kareem');
+const Schema = require('./schema');
+const SchemaType = require('./schemaType');
+const SchemaTypes = require('./schema/index');
+const VirtualType = require('./virtualType');
+const STATES = require('./connectionState');
+const VALID_OPTIONS = require('./validOptions');
+const Types = require('./types');
+const Query = require('./query');
+const Model = require('./model');
+const applyPlugins = require('./helpers/schema/applyPlugins');
+const builtinPlugins = require('./plugins');
+const driver = require('./driver');
+const legacyPluralize = require('./helpers/pluralize');
+const utils = require('./utils');
+const pkg = require('../package.json');
+const cast = require('./cast');
+
+const Aggregate = require('./aggregate');
+const trusted = require('./helpers/query/trusted').trusted;
+const sanitizeFilter = require('./helpers/query/sanitizeFilter');
+const isBsonType = require('./helpers/isBsonType');
+const MongooseError = require('./error/mongooseError');
+const SetOptionError = require('./error/setOptionError');
+const applyEmbeddedDiscriminators = require('./helpers/discriminator/applyEmbeddedDiscriminators');
+
+const defaultMongooseSymbol = Symbol.for('mongoose:default');
+const defaultConnectionSymbol = Symbol('mongoose:defaultConnection');
+
+require('./helpers/printJestWarning');
+
+const objectIdHexRegexp = /^[0-9A-Fa-f]{24}$/;
+
+const { AsyncLocalStorage } = require('node:async_hooks');
+
+/**
+ * Mongoose constructor.
+ *
+ * The exports object of the `mongoose` module is an instance of this class.
+ * Most apps will only use this one instance.
+ *
+ * #### Example:
+ *
+ * const mongoose = require('mongoose');
+ * mongoose instanceof mongoose.Mongoose; // true
+ *
+ * // Create a new Mongoose instance with its own `connect()`, `set()`, `model()`, etc.
+ * const m = new mongoose.Mongoose();
+ *
+ * @api public
+ * @param {Object} options see [`Mongoose#set()` docs](https://mongoosejs.com/docs/api/mongoose.html#Mongoose.prototype.set())
+ */
+function Mongoose(options) {
+ this.connections = [];
+ this.nextConnectionId = 0;
+ this.models = {};
+ this.events = new EventEmitter();
+ this.__driver = driver.get();
+ // default global options
+ this.options = Object.assign({
+ pluralization: true,
+ autoIndex: true,
+ autoCreate: true,
+ autoSearchIndex: false
+ }, options);
+ const createInitialConnection = utils.getOption('createInitialConnection', this.options) ?? true;
+ if (createInitialConnection && this.__driver != null) {
+ _createDefaultConnection(this);
+ }
+
+ if (this.options.pluralization) {
+ this._pluralize = legacyPluralize;
+ }
+
+ // If a user creates their own Mongoose instance, give them a separate copy
+ // of the `Schema` constructor so they get separate custom types. (gh-6933)
+ if (!options || !options[defaultMongooseSymbol]) {
+ const _this = this;
+ this.Schema = function() {
+ this.base = _this;
+ return Schema.apply(this, arguments);
+ };
+ this.Schema.prototype = Object.create(Schema.prototype);
+
+ Object.assign(this.Schema, Schema);
+ this.Schema.base = this;
+ this.Schema.Types = Object.assign({}, Schema.Types);
+ } else {
+ // Hack to work around babel's strange behavior with
+ // `import mongoose, { Schema } from 'mongoose'`. Because `Schema` is not
+ // an own property of a Mongoose global, Schema will be undefined. See gh-5648
+ for (const key of ['Schema', 'model']) {
+ this[key] = Mongoose.prototype[key];
+ }
+ }
+ this.Schema.prototype.base = this;
+
+ if (options?.transactionAsyncLocalStorage) {
+ this.transactionAsyncLocalStorage = new AsyncLocalStorage();
+ }
+
+ Object.defineProperty(this, 'plugins', {
+ configurable: false,
+ enumerable: true,
+ writable: false,
+ value: Object.values(builtinPlugins).map(plugin => ([plugin, { deduplicate: true }]))
+ });
+}
+
+Mongoose.prototype.cast = cast;
+/**
+ * Expose connection states for user-land
+ *
+ * @memberOf Mongoose
+ * @property STATES
+ * @api public
+ */
+Mongoose.prototype.STATES = STATES;
+
+/**
+ * Expose connection states for user-land
+ *
+ * @memberOf Mongoose
+ * @property ConnectionStates
+ * @api public
+ */
+Mongoose.prototype.ConnectionStates = STATES;
+
+/**
+ * Object with `get()` and `set()` containing the underlying driver this Mongoose instance
+ * uses to communicate with the database. A driver is a Mongoose-specific interface that defines functions
+ * like `find()`.
+ *
+ * @deprecated
+ * @memberOf Mongoose
+ * @property driver
+ * @api public
+ */
+
+Mongoose.prototype.driver = driver;
+
+/**
+ * Overwrites the current driver used by this Mongoose instance. A driver is a
+ * Mongoose-specific interface that defines functions like `find()`.
+ *
+ * @memberOf Mongoose
+ * @method setDriver
+ * @api public
+ */
+
+Mongoose.prototype.setDriver = function setDriver(driver) {
+ const _mongoose = this instanceof Mongoose ? this : mongoose;
+
+ if (_mongoose.__driver === driver) {
+ return _mongoose;
+ }
+
+ const openConnection = _mongoose.connections && _mongoose.connections.find(conn => conn.readyState !== STATES.disconnected);
+ if (openConnection) {
+ const msg = 'Cannot modify Mongoose driver if a connection is already open. ' +
+ 'Call `mongoose.disconnect()` before modifying the driver';
+ throw new MongooseError(msg);
+ }
+ _mongoose.__driver = driver;
+
+ if (Array.isArray(driver.plugins)) {
+ for (const plugin of driver.plugins) {
+ if (typeof plugin === 'function') {
+ _mongoose.plugin(plugin);
+ }
+ }
+ }
+
+ const Connection = driver.Connection;
+ const oldDefaultConnection = _mongoose.connections[0];
+ _mongoose.connections = [new Connection(_mongoose)];
+ _mongoose.connections[0].models = _mongoose.models;
+ if (oldDefaultConnection == null) {
+ return _mongoose;
+ }
+
+ // Update all models that pointed to the old default connection to
+ // the new default connection, including collections
+ for (const model of Object.values(_mongoose.models)) {
+ if (model.db !== oldDefaultConnection) {
+ continue;
+ }
+ model.$__updateConnection(_mongoose.connections[0]);
+ }
+
+ return _mongoose;
+};
+
+/**
+ * Sets mongoose options
+ *
+ * `key` can be used a object to set multiple options at once.
+ * If a error gets thrown for one option, other options will still be evaluated.
+ *
+ * #### Example:
+ *
+ * mongoose.set('test', value) // sets the 'test' option to `value`
+ *
+ * mongoose.set('debug', true) // enable logging collection methods + arguments to the console/file
+ *
+ * mongoose.set('debug', function(collectionName, methodName, ...methodArgs) {}); // use custom function to log collection methods + arguments
+ *
+ * mongoose.set({ debug: true, autoIndex: false }); // set multiple options at once
+ *
+ * Currently supported options are:
+ * - `allowDiskUse`: Set to `true` to set `allowDiskUse` to true to all aggregation operations by default.
+ * - `applyPluginsToChildSchemas`: `true` by default. Set to false to skip applying global plugins to child schemas
+ * - `applyPluginsToDiscriminators`: `false` by default. Set to true to apply global plugins to discriminator schemas. This typically isn't necessary because plugins are applied to the base schema and discriminators copy all middleware, methods, statics, and properties from the base schema.
+ * - `autoCreate`: Set to `true` to make Mongoose call [`Model.createCollection()`](https://mongoosejs.com/docs/api/model.html#Model.createCollection()) automatically when you create a model with `mongoose.model()` or `conn.model()`. This is useful for testing transactions, change streams, and other features that require the collection to exist.
+ * - `autoIndex`: `true` by default. Set to false to disable automatic index creation for all models associated with this Mongoose instance.
+ * - `bufferCommands`: enable/disable mongoose's buffering mechanism for all connections and models
+ * - `bufferTimeoutMS`: If bufferCommands is on, this option sets the maximum amount of time Mongoose buffering will wait before throwing an error. If not specified, Mongoose will use 10000 (10 seconds).
+ * - `cloneSchemas`: `false` by default. Set to `true` to `clone()` all schemas before compiling into a model.
+ * - `debug`: If `true`, prints the operations mongoose sends to MongoDB to the console. If a writable stream is passed, it will log to that stream, without colorization. If a callback function is passed, it will receive the collection name, the method name, then all arguments passed to the method. For example, if you wanted to replicate the default logging, you could output from the callback `Mongoose: ${collectionName}.${methodName}(${methodArgs.join(', ')})`.
+ * - `id`: If `true`, adds a `id` virtual to all schemas unless overwritten on a per-schema basis.
+ * - `timestamps.createdAt.immutable`: `true` by default. If `false`, it will change the `createdAt` field to be [`immutable: false`](https://mongoosejs.com/docs/api/schematype.html#SchemaType.prototype.immutable) which means you can update the `createdAt`
+ * - `maxTimeMS`: If set, attaches [maxTimeMS](https://www.mongodb.com/docs/manual/reference/operator/meta/maxTimeMS/) to every query
+ * - `objectIdGetter`: `true` by default. Mongoose adds a getter to MongoDB ObjectId's called `_id` that returns `this` for convenience with populate. Set this to false to remove the getter.
+ * - `overwriteModels`: Set to `true` to default to overwriting models with the same name when calling `mongoose.model()`, as opposed to throwing an `OverwriteModelError`.
+ * - `returnOriginal`: If `false`, changes the default `returnOriginal` option to `findOneAndUpdate()`, `findByIdAndUpdate`, and `findOneAndReplace()` to false. This is equivalent to setting the `new` option to `true` for `findOneAndX()` calls by default. Read our [`findOneAndUpdate()` tutorial](https://mongoosejs.com/docs/tutorials/findoneandupdate.html) for more information.
+ * - `runValidators`: `false` by default. Set to true to enable [update validators](https://mongoosejs.com/docs/validation.html#update-validators) for all validators by default.
+ * - `sanitizeFilter`: `false` by default. Set to true to enable the [sanitization of the query filters](https://mongoosejs.com/docs/api/mongoose.html#Mongoose.prototype.sanitizeFilter()) against query selector injection attacks by wrapping any nested objects that have a property whose name starts with `$` in a `$eq`.
+ * - `selectPopulatedPaths`: `true` by default. Set to false to opt out of Mongoose adding all fields that you `populate()` to your `select()`. The schema-level option `selectPopulatedPaths` overwrites this one.
+ * - `strict`: `true` by default, may be `false`, `true`, or `'throw'`. Sets the default strict mode for schemas.
+ * - `strictQuery`: `false` by default. May be `false`, `true`, or `'throw'`. Sets the default [strictQuery](https://mongoosejs.com/docs/guide.html#strictQuery) mode for schemas.
+ * - `toJSON`: `{ transform: true, flattenDecimals: true }` by default. Overwrites default objects to [`toJSON()`](https://mongoosejs.com/docs/api/document.html#Document.prototype.toJSON()), for determining how Mongoose documents get serialized by `JSON.stringify()`
+ * - `toObject`: `{ transform: true, flattenDecimals: true }` by default. Overwrites default objects to [`toObject()`](https://mongoosejs.com/docs/api/document.html#Document.prototype.toObject())
+ *
+ * @param {String|Object} key The name of the option or a object of multiple key-value pairs
+ * @param {String|Function|Boolean} value The value of the option, unused if "key" is a object
+ * @returns {Mongoose} The used Mongoose instnace
+ * @api public
+ */
+
+Mongoose.prototype.set = function getsetOptions(key, value) {
+ const _mongoose = this instanceof Mongoose ? this : mongoose;
+
+ if (arguments.length === 1 && typeof key !== 'object') {
+ if (VALID_OPTIONS.indexOf(key) === -1) {
+ const error = new SetOptionError();
+ error.addError(key, new SetOptionError.SetOptionInnerError(key));
+ throw error;
+ }
+
+ return _mongoose.options[key];
+ }
+
+ let options = {};
+
+ if (arguments.length === 2) {
+ options = { [key]: value };
+ }
+
+ if (arguments.length === 1 && typeof key === 'object') {
+ options = key;
+ }
+
+ // array for errors to collect all errors for all key-value pairs, like ".validate"
+ let error = undefined;
+
+ for (const [optionKey, optionValue] of Object.entries(options)) {
+ if (VALID_OPTIONS.indexOf(optionKey) === -1) {
+ if (!error) {
+ error = new SetOptionError();
+ }
+ error.addError(optionKey, new SetOptionError.SetOptionInnerError(optionKey));
+ continue;
+ }
+
+ _mongoose.options[optionKey] = optionValue;
+
+ if (optionKey === 'objectIdGetter') {
+ if (optionValue) {
+ Object.defineProperty(_mongoose.Types.ObjectId.prototype, '_id', {
+ enumerable: false,
+ configurable: true,
+ get: function() {
+ return this;
+ }
+ });
+ } else {
+ delete _mongoose.Types.ObjectId.prototype._id;
+ }
+ } else if (optionKey === 'transactionAsyncLocalStorage') {
+ if (optionValue && !_mongoose.transactionAsyncLocalStorage) {
+ _mongoose.transactionAsyncLocalStorage = new AsyncLocalStorage();
+ } else if (!optionValue && _mongoose.transactionAsyncLocalStorage) {
+ delete _mongoose.transactionAsyncLocalStorage;
+ }
+ } else if (optionKey === 'createInitialConnection') {
+ if (optionValue && !_mongoose.connection) {
+ _createDefaultConnection(_mongoose);
+ } else if (optionValue === false && _mongoose.connection && _mongoose.connection[defaultConnectionSymbol]) {
+ if (_mongoose.connection.readyState === STATES.disconnected && Object.keys(_mongoose.connection.models).length === 0) {
+ _mongoose.connections.shift();
+ }
+ }
+ }
+ }
+
+ if (error) {
+ throw error;
+ }
+
+ return _mongoose;
+};
+
+/**
+ * Gets mongoose options
+ *
+ * #### Example:
+ *
+ * mongoose.get('test') // returns the 'test' value
+ *
+ * @param {String} key
+ * @method get
+ * @api public
+ */
+
+Mongoose.prototype.get = Mongoose.prototype.set;
+
+/**
+ * Creates a Connection instance.
+ *
+ * Each `connection` instance maps to a single database. This method is helpful when managing multiple db connections.
+ *
+ *
+ * _Options passed take precedence over options included in connection strings._
+ *
+ * #### Example:
+ *
+ * // with mongodb:// URI
+ * db = mongoose.createConnection('mongodb://user:pass@127.0.0.1:port/database');
+ *
+ * // and options
+ * const opts = { db: { native_parser: true }}
+ * db = mongoose.createConnection('mongodb://user:pass@127.0.0.1:port/database', opts);
+ *
+ * // replica sets
+ * db = mongoose.createConnection('mongodb://user:pass@127.0.0.1:port,anotherhost:port,yetanother:port/database');
+ *
+ * // and options
+ * const opts = { replset: { strategy: 'ping', rs_name: 'testSet' }}
+ * db = mongoose.createConnection('mongodb://user:pass@127.0.0.1:port,anotherhost:port,yetanother:port/database', opts);
+ *
+ * // initialize now, connect later
+ * db = mongoose.createConnection();
+ * await db.openUri('mongodb://127.0.0.1:27017/database');
+ *
+ * @param {String} uri mongodb URI to connect to
+ * @param {Object} [options] passed down to the [MongoDB driver's `connect()` function](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/MongoClientOptions.html), except for 4 mongoose-specific options explained below.
+ * @param {Boolean} [options.bufferCommands=true] Mongoose specific option. Set to false to [disable buffering](https://mongoosejs.com/docs/faq.html#callback_never_executes) on all models associated with this connection.
+ * @param {String} [options.dbName] The name of the database you want to use. If not provided, Mongoose uses the database name from connection string.
+ * @param {String} [options.user] username for authentication, equivalent to `options.auth.user`. Maintained for backwards compatibility.
+ * @param {String} [options.pass] password for authentication, equivalent to `options.auth.password`. Maintained for backwards compatibility.
+ * @param {Boolean} [options.autoIndex=true] Mongoose-specific option. Set to false to disable automatic index creation for all models associated with this connection.
+ * @param {Class} [options.promiseLibrary] Sets the [underlying driver's promise library](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/MongoClientOptions.html#promiseLibrary).
+ * @param {Number} [options.maxPoolSize=5] The maximum number of sockets the MongoDB driver will keep open for this connection. Keep in mind that MongoDB only allows one operation per socket at a time, so you may want to increase this if you find you have a few slow queries that are blocking faster queries from proceeding. See [Slow Trains in MongoDB and Node.js](https://thecodebarbarian.com/slow-trains-in-mongodb-and-nodejs).
+ * @param {Number} [options.minPoolSize=1] The minimum number of sockets the MongoDB driver will keep open for this connection. Keep in mind that MongoDB only allows one operation per socket at a time, so you may want to increase this if you find you have a few slow queries that are blocking faster queries from proceeding. See [Slow Trains in MongoDB and Node.js](https://thecodebarbarian.com/slow-trains-in-mongodb-and-nodejs).
+ * @param {Number} [options.socketTimeoutMS=0] How long the MongoDB driver will wait before killing a socket due to inactivity _after initial connection_. Defaults to 0, which means Node.js will not time out the socket due to inactivity. A socket may be inactive because of either no activity or a long-running operation. This option is passed to [Node.js `socket#setTimeout()` function](https://nodejs.org/api/net.html#net_socket_settimeout_timeout_callback) after the MongoDB driver successfully completes.
+ * @param {Number} [options.family=0] Passed transparently to [Node.js' `dns.lookup()`](https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback) function. May be either `0`, `4`, or `6`. `4` means use IPv4 only, `6` means use IPv6 only, `0` means try both.
+ * @return {Connection} the created Connection object. Connections are not thenable, so you can't do `await mongoose.createConnection()`. To await use `mongoose.createConnection(uri).asPromise()` instead.
+ * @api public
+ */
+
+Mongoose.prototype.createConnection = function createConnection(uri, options) {
+ const _mongoose = this instanceof Mongoose ? this : mongoose;
+
+ const Connection = _mongoose.__driver.Connection;
+ const conn = new Connection(_mongoose);
+ _mongoose.connections.push(conn);
+ _mongoose.nextConnectionId++;
+ _mongoose.events.emit('createConnection', conn);
+
+ if (arguments.length > 0) {
+ conn.openUri(uri, { ...options, _fireAndForget: true });
+ }
+
+ return conn;
+};
+
+/**
+ * Opens the default mongoose connection.
+ *
+ * #### Example:
+ *
+ * mongoose.connect('mongodb://user:pass@127.0.0.1:port/database');
+ *
+ * // replica sets
+ * const uri = 'mongodb://user:pass@127.0.0.1:port,anotherhost:port,yetanother:port/mydatabase';
+ * mongoose.connect(uri);
+ *
+ * // with options
+ * mongoose.connect(uri, options);
+ *
+ * // Using `await` throws "MongooseServerSelectionError: Server selection timed out after 30000 ms"
+ * // if Mongoose can't connect.
+ * const uri = 'mongodb://nonexistent.domain:27000';
+ * await mongoose.connect(uri);
+ *
+ * @param {String} uri mongodb URI to connect to
+ * @param {Object} [options] passed down to the [MongoDB driver's `connect()` function](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/MongoClientOptions.html), except for 4 mongoose-specific options explained below.
+ * @param {Boolean} [options.bufferCommands=true] Mongoose specific option. Set to false to [disable buffering](https://mongoosejs.com/docs/faq.html#callback_never_executes) on all models associated with this connection.
+ * @param {Number} [options.bufferTimeoutMS=10000] Mongoose specific option. If `bufferCommands` is true, Mongoose will throw an error after `bufferTimeoutMS` if the operation is still buffered.
+ * @param {String} [options.dbName] The name of the database we want to use. If not provided, use database name from connection string.
+ * @param {String} [options.user] username for authentication, equivalent to `options.auth.user`. Maintained for backwards compatibility.
+ * @param {String} [options.pass] password for authentication, equivalent to `options.auth.password`. Maintained for backwards compatibility.
+ * @param {Number} [options.maxPoolSize=100] The maximum number of sockets the MongoDB driver will keep open for this connection. Keep in mind that MongoDB only allows one operation per socket at a time, so you may want to increase this if you find you have a few slow queries that are blocking faster queries from proceeding. See [Slow Trains in MongoDB and Node.js](https://thecodebarbarian.com/slow-trains-in-mongodb-and-nodejs).
+ * @param {Number} [options.minPoolSize=0] The minimum number of sockets the MongoDB driver will keep open for this connection.
+ * @param {Number} [options.serverSelectionTimeoutMS] If `useUnifiedTopology = true`, the MongoDB driver will try to find a server to send any given operation to, and keep retrying for `serverSelectionTimeoutMS` milliseconds before erroring out. If not set, the MongoDB driver defaults to using `30000` (30 seconds).
+ * @param {Number} [options.heartbeatFrequencyMS] If `useUnifiedTopology = true`, the MongoDB driver sends a heartbeat every `heartbeatFrequencyMS` to check on the status of the connection. A heartbeat is subject to `serverSelectionTimeoutMS`, so the MongoDB driver will retry failed heartbeats for up to 30 seconds by default. Mongoose only emits a `'disconnected'` event after a heartbeat has failed, so you may want to decrease this setting to reduce the time between when your server goes down and when Mongoose emits `'disconnected'`. We recommend you do **not** set this setting below 1000, too many heartbeats can lead to performance degradation.
+ * @param {Boolean} [options.autoIndex=true] Mongoose-specific option. Set to false to disable automatic index creation for all models associated with this connection.
+ * @param {Class} [options.promiseLibrary] Sets the [underlying driver's promise library](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/MongoClientOptions.html#promiseLibrary).
+ * @param {Number} [options.socketTimeoutMS=0] How long the MongoDB driver will wait before killing a socket due to inactivity _after initial connection_. A socket may be inactive because of either no activity or a long-running operation. `socketTimeoutMS` defaults to 0, which means Node.js will not time out the socket due to inactivity. This option is passed to [Node.js `socket#setTimeout()` function](https://nodejs.org/api/net.html#net_socket_settimeout_timeout_callback) after the MongoDB driver successfully completes.
+ * @param {Number} [options.family=0] Passed transparently to [Node.js' `dns.lookup()`](https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback) function. May be either `0`, `4`, or `6`. `4` means use IPv4 only, `6` means use IPv6 only, `0` means try both.
+ * @param {Boolean} [options.autoCreate=false] Set to `true` to make Mongoose automatically call `createCollection()` on every model created on this connection.
+ * @see Mongoose#createConnection https://mongoosejs.com/docs/api/mongoose.html#Mongoose.prototype.createConnection()
+ * @api public
+ * @return {Promise} resolves to `this` if connection succeeded
+ */
+
+Mongoose.prototype.connect = async function connect(uri, options) {
+ if (typeof options === 'function' || (arguments.length >= 3 && typeof arguments[2] === 'function')) {
+ throw new MongooseError('Mongoose.prototype.connect() no longer accepts a callback');
+ }
+
+ const _mongoose = this instanceof Mongoose ? this : mongoose;
+ if (_mongoose.connection == null) {
+ _createDefaultConnection(_mongoose);
+ }
+ const conn = _mongoose.connection;
+
+ return conn.openUri(uri, options).then(() => _mongoose);
+};
+
+/**
+ * Runs `.close()` on all connections in parallel.
+ *
+ * @return {Promise} resolves when all connections are closed, or rejects with the first error that occurred.
+ * @api public
+ */
+
+Mongoose.prototype.disconnect = async function disconnect() {
+ if (arguments.length >= 1 && typeof arguments[0] === 'function') {
+ throw new MongooseError('Mongoose.prototype.disconnect() no longer accepts a callback');
+ }
+
+ const _mongoose = this instanceof Mongoose ? this : mongoose;
+
+ const remaining = _mongoose.connections.length;
+ if (remaining <= 0) {
+ return;
+ }
+ await Promise.all(_mongoose.connections.map(conn => conn.close()));
+};
+
+/**
+ * _Requires MongoDB >= 3.6.0._ Starts a [MongoDB session](https://www.mongodb.com/docs/manual/release-notes/3.6/#client-sessions)
+ * for benefits like causal consistency, [retryable writes](https://www.mongodb.com/docs/manual/core/retryable-writes/),
+ * and [transactions](https://thecodebarbarian.com/a-node-js-perspective-on-mongodb-4-transactions.html).
+ *
+ * Calling `mongoose.startSession()` is equivalent to calling `mongoose.connection.startSession()`.
+ * Sessions are scoped to a connection, so calling `mongoose.startSession()`
+ * starts a session on the [default mongoose connection](https://mongoosejs.com/docs/api/mongoose.html#Mongoose.prototype.connection).
+ *
+ * @param {Object} [options] see the [mongodb driver options](https://mongodb.github.io/node-mongodb-native/4.9/classes/MongoClient.html#startSession)
+ * @param {Boolean} [options.causalConsistency=true] set to false to disable causal consistency
+ * @return {Promise} promise that resolves to a MongoDB driver `ClientSession`
+ * @api public
+ */
+
+Mongoose.prototype.startSession = function startSession() {
+ const _mongoose = this instanceof Mongoose ? this : mongoose;
+
+ return _mongoose.connection.startSession.apply(_mongoose.connection, arguments);
+};
+
+/**
+ * Getter/setter around function for pluralizing collection names.
+ *
+ * @param {Function|null} [fn] overwrites the function used to pluralize collection names
+ * @return {Function|null} the current function used to pluralize collection names, defaults to the legacy function from `mongoose-legacy-pluralize`.
+ * @api public
+ */
+
+Mongoose.prototype.pluralize = function pluralize(fn) {
+ const _mongoose = this instanceof Mongoose ? this : mongoose;
+
+ if (arguments.length > 0) {
+ _mongoose._pluralize = fn;
+ }
+ return _mongoose._pluralize;
+};
+
+/**
+ * Defines a model or retrieves it.
+ *
+ * Models defined on the `mongoose` instance are available to all connection
+ * created by the same `mongoose` instance.
+ *
+ * If you call `mongoose.model()` with twice the same name but a different schema,
+ * you will get an `OverwriteModelError`. If you call `mongoose.model()` with
+ * the same name and same schema, you'll get the same schema back.
+ *
+ * #### Example:
+ *
+ * const mongoose = require('mongoose');
+ *
+ * // define an Actor model with this mongoose instance
+ * const schema = new Schema({ name: String });
+ * mongoose.model('Actor', schema);
+ *
+ * // create a new connection
+ * const conn = mongoose.createConnection(..);
+ *
+ * // create Actor model
+ * const Actor = conn.model('Actor', schema);
+ * conn.model('Actor') === Actor; // true
+ * conn.model('Actor', schema) === Actor; // true, same schema
+ * conn.model('Actor', schema, 'actors') === Actor; // true, same schema and collection name
+ *
+ * // This throws an `OverwriteModelError` because the schema is different.
+ * conn.model('Actor', new Schema({ name: String }));
+ *
+ * _When no `collection` argument is passed, Mongoose uses the model name. If you don't like this behavior, either pass a collection name, use `mongoose.pluralize()`, or set your schemas collection name option._
+ *
+ * #### Example:
+ *
+ * const schema = new Schema({ name: String }, { collection: 'actor' });
+ *
+ * // or
+ *
+ * schema.set('collection', 'actor');
+ *
+ * // or
+ *
+ * const collectionName = 'actor';
+ * const M = mongoose.model('Actor', schema, collectionName);
+ *
+ * @param {String|Function} name model name or class extending Model
+ * @param {Schema} [schema] the schema to use.
+ * @param {String} [collection] name (optional, inferred from model name)
+ * @param {Object} [options]
+ * @param {Boolean} [options.overwriteModels=false] If true, overwrite existing models with the same name to avoid `OverwriteModelError`
+ * @return {Model} The model associated with `name`. Mongoose will create the model if it doesn't already exist.
+ * @api public
+ */
+
+Mongoose.prototype.model = function model(name, schema, collection, options) {
+ const _mongoose = this instanceof Mongoose ? this : mongoose;
+
+ if (typeof schema === 'string') {
+ collection = schema;
+ schema = false;
+ }
+
+ if (arguments.length === 1) {
+ const model = _mongoose.models[name];
+ if (!model) {
+ throw new _mongoose.Error.MissingSchemaError(name);
+ }
+ return model;
+ }
+
+ if (utils.isObject(schema) && !(schema instanceof Schema)) {
+ schema = new Schema(schema);
+ }
+ if (schema && !(schema instanceof Schema)) {
+ throw new _mongoose.Error('The 2nd parameter to `mongoose.model()` should be a ' +
+ 'schema or a POJO');
+ }
+
+ // handle internal options from connection.model()
+ options = options || {};
+
+ const originalSchema = schema;
+ if (schema) {
+ if (_mongoose.get('cloneSchemas')) {
+ schema = schema.clone();
+ }
+ _mongoose._applyPlugins(schema);
+ }
+
+ // connection.model() may be passing a different schema for
+ // an existing model name. in this case don't read from cache.
+ const overwriteModels = _mongoose.options.hasOwnProperty('overwriteModels') ?
+ _mongoose.options.overwriteModels :
+ options.overwriteModels;
+ if (_mongoose.models.hasOwnProperty(name) && options.cache !== false && overwriteModels !== true) {
+ if (originalSchema &&
+ originalSchema.instanceOfSchema &&
+ originalSchema !== _mongoose.models[name].schema) {
+ throw new _mongoose.Error.OverwriteModelError(name);
+ }
+ if (collection && collection !== _mongoose.models[name].collection.name) {
+ // subclass current model with alternate collection
+ const model = _mongoose.models[name];
+ schema = model.prototype.schema;
+ const sub = model.__subclass(_mongoose.connection, schema, collection);
+ // do not cache the sub model
+ return sub;
+ }
+ return _mongoose.models[name];
+ }
+ if (schema == null) {
+ throw new _mongoose.Error.MissingSchemaError(name);
+ }
+
+ const model = _mongoose._model(name, schema, collection, options);
+ _mongoose.connection.models[name] = model;
+ _mongoose.models[name] = model;
+
+ return model;
+};
+
+/*!
+ * ignore
+ */
+
+Mongoose.prototype._model = function _model(name, schema, collection, options) {
+ const _mongoose = this instanceof Mongoose ? this : mongoose;
+
+ let model;
+ if (typeof name === 'function') {
+ model = name;
+ name = model.name;
+ if (!(model.prototype instanceof Model)) {
+ throw new _mongoose.Error('The provided class ' + name + ' must extend Model');
+ }
+ }
+
+ if (schema) {
+ if (_mongoose.get('cloneSchemas')) {
+ schema = schema.clone();
+ }
+ _mongoose._applyPlugins(schema);
+ }
+
+ // Apply relevant "global" options to the schema
+ if (schema == null || !('pluralization' in schema.options)) {
+ schema.options.pluralization = _mongoose.options.pluralization;
+ }
+
+ if (!collection) {
+ collection = schema.get('collection') ||
+ utils.toCollectionName(name, _mongoose.pluralize());
+ }
+
+ applyEmbeddedDiscriminators(schema);
+
+ const connection = options.connection || _mongoose.connection;
+ model = _mongoose.Model.compile(model || name, schema, collection, connection, _mongoose);
+ // Errors handled internally, so safe to ignore error
+ model.init().catch(function $modelInitNoop() {});
+
+ connection.emit('model', model);
+
+ if (schema._applyDiscriminators != null) {
+ for (const disc of schema._applyDiscriminators.keys()) {
+ const {
+ schema: discriminatorSchema,
+ options
+ } = schema._applyDiscriminators.get(disc);
+ model.discriminator(disc, discriminatorSchema, options);
+ }
+ }
+
+ return model;
+};
+
+/**
+ * Removes the model named `name` from the default connection, if it exists.
+ * You can use this function to clean up any models you created in your tests to
+ * prevent OverwriteModelErrors.
+ *
+ * Equivalent to `mongoose.connection.deleteModel(name)`.
+ *
+ * #### Example:
+ *
+ * mongoose.model('User', new Schema({ name: String }));
+ * console.log(mongoose.model('User')); // Model object
+ * mongoose.deleteModel('User');
+ * console.log(mongoose.model('User')); // undefined
+ *
+ * // Usually useful in a Mocha `afterEach()` hook
+ * afterEach(function() {
+ * mongoose.deleteModel(/.+/); // Delete every model
+ * });
+ *
+ * @api public
+ * @param {String|RegExp} name if string, the name of the model to remove. If regexp, removes all models whose name matches the regexp.
+ * @return {Mongoose} this
+ */
+
+Mongoose.prototype.deleteModel = function deleteModel(name) {
+ const _mongoose = this instanceof Mongoose ? this : mongoose;
+
+ _mongoose.connection.deleteModel(name);
+ delete _mongoose.models[name];
+ return _mongoose;
+};
+
+/**
+ * Returns an array of model names created on this instance of Mongoose.
+ *
+ * #### Note:
+ *
+ * _Does not include names of models created using `connection.model()`._
+ *
+ * @api public
+ * @return {Array}
+ */
+
+Mongoose.prototype.modelNames = function modelNames() {
+ const _mongoose = this instanceof Mongoose ? this : mongoose;
+
+ const names = Object.keys(_mongoose.models);
+ return names;
+};
+
+/**
+ * Applies global plugins to `schema`.
+ *
+ * @param {Schema} schema
+ * @api private
+ */
+
+Mongoose.prototype._applyPlugins = function _applyPlugins(schema, options) {
+ const _mongoose = this instanceof Mongoose ? this : mongoose;
+
+ options = options || {};
+ options.applyPluginsToDiscriminators = _mongoose.options && _mongoose.options.applyPluginsToDiscriminators || false;
+ options.applyPluginsToChildSchemas = typeof (_mongoose.options && _mongoose.options.applyPluginsToChildSchemas) === 'boolean' ?
+ _mongoose.options.applyPluginsToChildSchemas :
+ true;
+ applyPlugins(schema, _mongoose.plugins, options, '$globalPluginsApplied');
+};
+
+/**
+ * Declares a global plugin executed on all Schemas.
+ *
+ * Equivalent to calling `.plugin(fn)` on each Schema you create.
+ *
+ * @param {Function} fn plugin callback
+ * @param {Object} [opts] optional options
+ * @return {Mongoose} this
+ * @see plugins https://mongoosejs.com/docs/plugins.html
+ * @api public
+ */
+
+Mongoose.prototype.plugin = function plugin(fn, opts) {
+ const _mongoose = this instanceof Mongoose ? this : mongoose;
+
+ _mongoose.plugins.push([fn, opts]);
+ return _mongoose;
+};
+
+/**
+ * The Mongoose module's default connection. Equivalent to `mongoose.connections[0]`, see [`connections`](https://mongoosejs.com/docs/api/mongoose.html#Mongoose.prototype.connections).
+ *
+ * #### Example:
+ *
+ * const mongoose = require('mongoose');
+ * mongoose.connect(...);
+ * mongoose.connection.on('error', cb);
+ *
+ * This is the connection used by default for every model created using [mongoose.model](https://mongoosejs.com/docs/api/mongoose.html#Mongoose.prototype.model()).
+ *
+ * To create a new connection, use [`createConnection()`](https://mongoosejs.com/docs/api/mongoose.html#Mongoose.prototype.createConnection()).
+ *
+ * @memberOf Mongoose
+ * @instance
+ * @property {Connection} connection
+ * @api public
+ */
+
+Mongoose.prototype.__defineGetter__('connection', function() {
+ return this.connections[0];
+});
+
+Mongoose.prototype.__defineSetter__('connection', function(v) {
+ if (v instanceof this.__driver.Connection) {
+ this.connections[0] = v;
+ this.models = v.models;
+ }
+});
+
+/**
+ * An array containing all [connections](connection.html) associated with this
+ * Mongoose instance. By default, there is 1 connection. Calling
+ * [`createConnection()`](https://mongoosejs.com/docs/api/mongoose.html#Mongoose.prototype.createConnection()) adds a connection
+ * to this array.
+ *
+ * #### Example:
+ *
+ * const mongoose = require('mongoose');
+ * mongoose.connections.length; // 1, just the default connection
+ * mongoose.connections[0] === mongoose.connection; // true
+ *
+ * mongoose.createConnection('mongodb://127.0.0.1:27017/test');
+ * mongoose.connections.length; // 2
+ *
+ * @memberOf Mongoose
+ * @instance
+ * @property {Array} connections
+ * @api public
+ */
+
+Mongoose.prototype.connections;
+
+/**
+ * An integer containing the value of the next connection id. Calling
+ * [`createConnection()`](https://mongoosejs.com/docs/api/mongoose.html#Mongoose.prototype.createConnection()) increments
+ * this value.
+ *
+ * #### Example:
+ *
+ * const mongoose = require('mongoose');
+ * mongoose.createConnection(); // id `0`, `nextConnectionId` becomes `1`
+ * mongoose.createConnection(); // id `1`, `nextConnectionId` becomes `2`
+ * mongoose.connections[0].destroy() // Removes connection with id `0`
+ * mongoose.createConnection(); // id `2`, `nextConnectionId` becomes `3`
+ *
+ * @memberOf Mongoose
+ * @instance
+ * @property {Number} nextConnectionId
+ * @api private
+ */
+
+Mongoose.prototype.nextConnectionId;
+
+/**
+ * The Mongoose Aggregate constructor
+ *
+ * @method Aggregate
+ * @api public
+ */
+
+Mongoose.prototype.Aggregate = Aggregate;
+
+/**
+ * The Mongoose Collection constructor
+ *
+ * @memberOf Mongoose
+ * @instance
+ * @method Collection
+ * @api public
+ */
+
+Object.defineProperty(Mongoose.prototype, 'Collection', {
+ get: function() {
+ return this.__driver.Collection;
+ },
+ set: function(Collection) {
+ this.__driver.Collection = Collection;
+ }
+});
+
+/**
+ * The Mongoose [Connection](https://mongoosejs.com/docs/api/connection.html#Connection()) constructor
+ *
+ * @memberOf Mongoose
+ * @instance
+ * @method Connection
+ * @api public
+ */
+
+Object.defineProperty(Mongoose.prototype, 'Connection', {
+ get: function() {
+ return this.__driver.Connection;
+ },
+ set: function(Connection) {
+ if (Connection === this.__driver.Connection) {
+ return;
+ }
+
+ this.__driver.Connection = Connection;
+ }
+});
+
+/**
+ * The Mongoose version
+ *
+ * #### Example:
+ *
+ * console.log(mongoose.version); // '5.x.x'
+ *
+ * @property version
+ * @api public
+ */
+
+Mongoose.prototype.version = pkg.version;
+
+/**
+ * The Mongoose constructor
+ *
+ * The exports of the mongoose module is an instance of this class.
+ *
+ * #### Example:
+ *
+ * const mongoose = require('mongoose');
+ * const mongoose2 = new mongoose.Mongoose();
+ *
+ * @method Mongoose
+ * @api public
+ */
+
+Mongoose.prototype.Mongoose = Mongoose;
+
+/**
+ * The Mongoose [Schema](https://mongoosejs.com/docs/api/schema.html#Schema()) constructor
+ *
+ * #### Example:
+ *
+ * const mongoose = require('mongoose');
+ * const Schema = mongoose.Schema;
+ * const CatSchema = new Schema(..);
+ *
+ * @method Schema
+ * @api public
+ */
+
+Mongoose.prototype.Schema = Schema;
+
+/**
+ * The Mongoose [SchemaType](https://mongoosejs.com/docs/api/schematype.html#SchemaType()) constructor
+ *
+ * @method SchemaType
+ * @api public
+ */
+
+Mongoose.prototype.SchemaType = SchemaType;
+
+/**
+ * The various Mongoose SchemaTypes.
+ *
+ * #### Note:
+ *
+ * _Alias of mongoose.Schema.Types for backwards compatibility._
+ *
+ * @property SchemaTypes
+ * @see Schema.SchemaTypes https://mongoosejs.com/docs/schematypes.html
+ * @api public
+ */
+
+Mongoose.prototype.SchemaTypes = Schema.Types;
+
+/**
+ * The Mongoose [VirtualType](https://mongoosejs.com/docs/api/virtualtype.html#VirtualType()) constructor
+ *
+ * @method VirtualType
+ * @api public
+ */
+
+Mongoose.prototype.VirtualType = VirtualType;
+
+/**
+ * The various Mongoose Types.
+ *
+ * #### Example:
+ *
+ * const mongoose = require('mongoose');
+ * const array = mongoose.Types.Array;
+ *
+ * #### Types:
+ *
+ * - [Array](https://mongoosejs.com/docs/schematypes.html#arrays)
+ * - [Buffer](https://mongoosejs.com/docs/schematypes.html#buffers)
+ * - [Embedded](https://mongoosejs.com/docs/schematypes.html#schemas)
+ * - [DocumentArray](https://mongoosejs.com/docs/api/documentarraypath.html)
+ * - [Decimal128](https://mongoosejs.com/docs/api/mongoose.html#Mongoose.prototype.Decimal128)
+ * - [ObjectId](https://mongoosejs.com/docs/schematypes.html#objectids)
+ * - [Map](https://mongoosejs.com/docs/schematypes.html#maps)
+ * - [Subdocument](https://mongoosejs.com/docs/schematypes.html#schemas)
+ * - [Int32](https://mongoosejs.com/docs/schematypes.html#int32)
+ *
+ * Using this exposed access to the `ObjectId` type, we can construct ids on demand.
+ *
+ * const ObjectId = mongoose.Types.ObjectId;
+ * const id1 = new ObjectId;
+ *
+ * @property Types
+ * @api public
+ */
+
+Mongoose.prototype.Types = Types;
+
+/**
+ * The Mongoose [Query](https://mongoosejs.com/docs/api/query.html#Query()) constructor.
+ *
+ * @method Query
+ * @api public
+ */
+
+Mongoose.prototype.Query = Query;
+
+/**
+ * The Mongoose [Model](https://mongoosejs.com/docs/api/model.html#Model()) constructor.
+ *
+ * @method Model
+ * @api public
+ */
+
+Mongoose.prototype.Model = Model;
+
+/**
+ * The Mongoose [Document](https://mongoosejs.com/docs/api/document.html#Document()) constructor.
+ *
+ * @method Document
+ * @api public
+ */
+
+Mongoose.prototype.Document = Document;
+
+/**
+ * The Mongoose DocumentProvider constructor. Mongoose users should not have to
+ * use this directly
+ *
+ * @method DocumentProvider
+ * @api public
+ */
+
+Mongoose.prototype.DocumentProvider = require('./documentProvider');
+
+/**
+ * The Mongoose ObjectId [SchemaType](https://mongoosejs.com/docs/schematypes.html). Used for
+ * declaring paths in your schema that should be
+ * [MongoDB ObjectIds](https://www.mongodb.com/docs/manual/reference/method/ObjectId/).
+ * Do not use this to create a new ObjectId instance, use `mongoose.Types.ObjectId`
+ * instead.
+ *
+ * #### Example:
+ *
+ * const childSchema = new Schema({ parentId: mongoose.ObjectId });
+ *
+ * @property ObjectId
+ * @api public
+ */
+
+Mongoose.prototype.ObjectId = SchemaTypes.ObjectId;
+
+/**
+ * Returns true if Mongoose can cast the given value to an ObjectId, or
+ * false otherwise.
+ *
+ * #### Example:
+ *
+ * mongoose.isValidObjectId(new mongoose.Types.ObjectId()); // true
+ * mongoose.isValidObjectId('0123456789ab'); // true
+ * mongoose.isValidObjectId(6); // true
+ * mongoose.isValidObjectId(new User({ name: 'test' })); // true
+ *
+ * mongoose.isValidObjectId({ test: 42 }); // false
+ *
+ * @method isValidObjectId
+ * @param {Any} v
+ * @returns {boolean} true if `v` is something Mongoose can coerce to an ObjectId
+ * @api public
+ */
+
+Mongoose.prototype.isValidObjectId = function isValidObjectId(v) {
+ const _mongoose = this instanceof Mongoose ? this : mongoose;
+ return _mongoose.Types.ObjectId.isValid(v);
+};
+
+/**
+ * Returns true if the given value is a Mongoose ObjectId (using `instanceof`) or if the
+ * given value is a 24 character hex string, which is the most commonly used string representation
+ * of an ObjectId.
+ *
+ * This function is similar to `isValidObjectId()`, but considerably more strict, because
+ * `isValidObjectId()` will return `true` for _any_ value that Mongoose can convert to an
+ * ObjectId. That includes Mongoose documents, any string of length 12, and any number.
+ * `isObjectIdOrHexString()` returns true only for `ObjectId` instances or 24 character hex
+ * strings, and will return false for numbers, documents, and strings of length 12.
+ *
+ * #### Example:
+ *
+ * mongoose.isObjectIdOrHexString(new mongoose.Types.ObjectId()); // true
+ * mongoose.isObjectIdOrHexString('62261a65d66c6be0a63c051f'); // true
+ *
+ * mongoose.isObjectIdOrHexString('0123456789ab'); // false
+ * mongoose.isObjectIdOrHexString(6); // false
+ * mongoose.isObjectIdOrHexString(new User({ name: 'test' })); // false
+ * mongoose.isObjectIdOrHexString({ test: 42 }); // false
+ *
+ * @method isObjectIdOrHexString
+ * @param {Any} v
+ * @returns {boolean} true if `v` is an ObjectId instance _or_ a 24 char hex string
+ * @api public
+ */
+
+Mongoose.prototype.isObjectIdOrHexString = function isObjectIdOrHexString(v) {
+ return isBsonType(v, 'ObjectId') || (typeof v === 'string' && objectIdHexRegexp.test(v));
+};
+
+/**
+ *
+ * Syncs all the indexes for the models registered with this connection.
+ *
+ * @param {Object} options
+ * @param {Boolean} options.continueOnError `false` by default. If set to `true`, mongoose will not throw an error if one model syncing failed, and will return an object where the keys are the names of the models, and the values are the results/errors for each model.
+ * @return {Promise} Returns a Promise, when the Promise resolves the value is a list of the dropped indexes.
+ */
+Mongoose.prototype.syncIndexes = function syncIndexes(options) {
+ const _mongoose = this instanceof Mongoose ? this : mongoose;
+ return _mongoose.connection.syncIndexes(options);
+};
+
+/**
+ * The Mongoose Decimal128 [SchemaType](https://mongoosejs.com/docs/schematypes.html). Used for
+ * declaring paths in your schema that should be
+ * [128-bit decimal floating points](https://thecodebarbarian.com/a-nodejs-perspective-on-mongodb-34-decimal.html).
+ * Do not use this to create a new Decimal128 instance, use `mongoose.Types.Decimal128`
+ * instead.
+ *
+ * #### Example:
+ *
+ * const vehicleSchema = new Schema({ fuelLevel: mongoose.Decimal128 });
+ *
+ * @property Decimal128
+ * @api public
+ */
+
+Mongoose.prototype.Decimal128 = SchemaTypes.Decimal128;
+
+
+/**
+ * The Mongoose Mixed [SchemaType](https://mongoosejs.com/docs/schematypes.html). Used for
+ * declaring paths in your schema that Mongoose's change tracking, casting,
+ * and validation should ignore.
+ *
+ * #### Example:
+ *
+ * const schema = new Schema({ arbitrary: mongoose.Mixed });
+ *
+ * @property Mixed
+ * @api public
+ */
+
+Mongoose.prototype.Mixed = SchemaTypes.Mixed;
+
+/**
+ * The Mongoose Date [SchemaType](https://mongoosejs.com/docs/schematypes.html).
+ *
+ * #### Example:
+ *
+ * const schema = new Schema({ test: Date });
+ * schema.path('test') instanceof mongoose.Date; // true
+ *
+ * @property Date
+ * @api public
+ */
+
+Mongoose.prototype.Date = SchemaTypes.Date;
+
+/**
+ * The Mongoose Number [SchemaType](https://mongoosejs.com/docs/schematypes.html). Used for
+ * declaring paths in your schema that Mongoose should cast to numbers.
+ *
+ * #### Example:
+ *
+ * const schema = new Schema({ num: mongoose.Number });
+ * // Equivalent to:
+ * const schema = new Schema({ num: 'number' });
+ *
+ * @property Number
+ * @api public
+ */
+
+Mongoose.prototype.Number = SchemaTypes.Number;
+
+/**
+ * The [MongooseError](https://mongoosejs.com/docs/api/error.html#Error()) constructor.
+ *
+ * @method Error
+ * @api public
+ */
+
+Mongoose.prototype.Error = MongooseError;
+Mongoose.prototype.MongooseError = MongooseError;
+
+/**
+ * Mongoose uses this function to get the current time when setting
+ * [timestamps](https://mongoosejs.com/docs/guide.html#timestamps). You may stub out this function
+ * using a tool like [Sinon](https://www.npmjs.com/package/sinon) for testing.
+ *
+ * @method now
+ * @returns Date the current time
+ * @api public
+ */
+
+Mongoose.prototype.now = function now() { return new Date(); };
+
+/**
+ * The Mongoose CastError constructor
+ *
+ * @method CastError
+ * @param {String} type The name of the type
+ * @param {Any} value The value that failed to cast
+ * @param {String} path The path `a.b.c` in the doc where this cast error occurred
+ * @param {Error} [reason] The original error that was thrown
+ * @api public
+ */
+
+Mongoose.prototype.CastError = MongooseError.CastError;
+
+/**
+ * The constructor used for schematype options
+ *
+ * @method SchemaTypeOptions
+ * @api public
+ */
+
+Mongoose.prototype.SchemaTypeOptions = require('./options/schemaTypeOptions');
+
+/**
+ * The [mquery](https://github.com/aheckmann/mquery) query builder Mongoose uses.
+ *
+ * @property mquery
+ * @api public
+ */
+
+Mongoose.prototype.mquery = require('mquery');
+
+/**
+ * Sanitizes query filters against [query selector injection attacks](https://thecodebarbarian.com/2014/09/04/defending-against-query-selector-injection-attacks.html)
+ * by wrapping any nested objects that have a property whose name starts with `$` in a `$eq`.
+ *
+ * ```javascript
+ * const obj = { username: 'val', pwd: { $ne: null } };
+ * sanitizeFilter(obj);
+ * obj; // { username: 'val', pwd: { $eq: { $ne: null } } });
+ * ```
+ *
+ * @method sanitizeFilter
+ * @param {Object} filter
+ * @returns Object the sanitized object
+ * @api public
+ */
+
+Mongoose.prototype.sanitizeFilter = sanitizeFilter;
+
+/**
+ * Tells `sanitizeFilter()` to skip the given object when filtering out potential [query selector injection attacks](https://thecodebarbarian.com/2014/09/04/defending-against-query-selector-injection-attacks.html).
+ * Use this method when you have a known query selector that you want to use.
+ *
+ * ```javascript
+ * const obj = { username: 'val', pwd: trusted({ $type: 'string', $eq: 'my secret' }) };
+ * sanitizeFilter(obj);
+ *
+ * // Note that `sanitizeFilter()` did not add `$eq` around `$type`.
+ * obj; // { username: 'val', pwd: { $type: 'string', $eq: 'my secret' } });
+ * ```
+ *
+ * @method trusted
+ * @param {Object} obj
+ * @returns Object the passed in object
+ * @api public
+ */
+
+Mongoose.prototype.trusted = trusted;
+
+/**
+ * Use this function in `pre()` middleware to skip calling the wrapped function.
+ *
+ * #### Example:
+ *
+ * schema.pre('save', function() {
+ * // Will skip executing `save()`, but will execute post hooks as if
+ * // `save()` had executed with the result `{ matchedCount: 0 }`
+ * return mongoose.skipMiddlewareFunction({ matchedCount: 0 });
+ * });
+ *
+ * @method skipMiddlewareFunction
+ * @param {any} result
+ * @api public
+ */
+
+Mongoose.prototype.skipMiddlewareFunction = Kareem.skipWrappedFunction;
+
+/**
+ * Use this function in `post()` middleware to replace the result
+ *
+ * #### Example:
+ *
+ * schema.post('find', function(res) {
+ * // Normally you have to modify `res` in place. But with
+ * // `overwriteMiddlewarResult()`, you can make `find()` return a
+ * // completely different value.
+ * return mongoose.overwriteMiddlewareResult(res.filter(doc => !doc.isDeleted));
+ * });
+ *
+ * @method overwriteMiddlewareResult
+ * @param {any} result
+ * @api public
+ */
+
+Mongoose.prototype.overwriteMiddlewareResult = Kareem.overwriteResult;
+
+/**
+ * Takes in an object and deletes any keys from the object whose values
+ * are strictly equal to `undefined`.
+ * This function is useful for query filters because Mongoose treats
+ * `TestModel.find({ name: undefined })` as `TestModel.find({ name: null })`.
+ *
+ * #### Example:
+ *
+ * const filter = { name: 'John', age: undefined, status: 'active' };
+ * mongoose.omitUndefined(filter); // { name: 'John', status: 'active' }
+ * filter; // { name: 'John', status: 'active' }
+ *
+ * await UserModel.findOne(mongoose.omitUndefined(filter));
+ *
+ * @method omitUndefined
+ * @param {Object} [val] the object to remove undefined keys from
+ * @returns {Object} the object passed in
+ * @api public
+ */
+
+Mongoose.prototype.omitUndefined = require('./helpers/omitUndefined');
+
+/*!
+ * Create a new default connection (`mongoose.connection`) for a Mongoose instance.
+ * No-op if there is already a default connection.
+ */
+
+function _createDefaultConnection(mongoose) {
+ if (mongoose.connection) {
+ return;
+ }
+ const conn = mongoose.createConnection(); // default connection
+ conn[defaultConnectionSymbol] = true;
+ conn.models = mongoose.models;
+}
+
+/**
+ * The exports object is an instance of Mongoose.
+ *
+ * @api private
+ */
+
+const mongoose = module.exports = exports = new Mongoose({
+ [defaultMongooseSymbol]: true
+});
diff --git a/lib/options.js b/lib/options.js
index 4826e59fdc5..bbdcda8b97e 100644
--- a/lib/options.js
+++ b/lib/options.js
@@ -11,5 +11,7 @@ exports.internalToObjectOptions = {
_skipDepopulateTopLevel: true,
depopulate: true,
flattenDecimals: false,
- useProjection: false
+ useProjection: false,
+ versionKey: true,
+ flattenObjectIds: false
};
diff --git a/lib/options/PopulateOptions.js b/lib/options/populateOptions.js
similarity index 100%
rename from lib/options/PopulateOptions.js
rename to lib/options/populateOptions.js
diff --git a/lib/options/removeOptions.js b/lib/options/removeOptions.js
deleted file mode 100644
index 3e09bbc0f89..00000000000
--- a/lib/options/removeOptions.js
+++ /dev/null
@@ -1,14 +0,0 @@
-'use strict';
-
-const clone = require('../helpers/clone');
-
-class RemoveOptions {
- constructor(obj) {
- if (obj == null) {
- return;
- }
- Object.assign(this, clone(obj));
- }
-}
-
-module.exports = RemoveOptions;
diff --git a/lib/options/saveOptions.js b/lib/options/saveOptions.js
index 66c1608b1d5..286987ee1e4 100644
--- a/lib/options/saveOptions.js
+++ b/lib/options/saveOptions.js
@@ -11,4 +11,6 @@ class SaveOptions {
}
}
+SaveOptions.prototype.__subdocs = null;
+
module.exports = SaveOptions;
diff --git a/lib/options/SchemaArrayOptions.js b/lib/options/schemaArrayOptions.js
similarity index 97%
rename from lib/options/SchemaArrayOptions.js
rename to lib/options/schemaArrayOptions.js
index 54ad4f09058..01f4fb78c64 100644
--- a/lib/options/SchemaArrayOptions.js
+++ b/lib/options/schemaArrayOptions.js
@@ -1,6 +1,6 @@
'use strict';
-const SchemaTypeOptions = require('./SchemaTypeOptions');
+const SchemaTypeOptions = require('./schemaTypeOptions');
/**
* The options defined on an Array schematype.
diff --git a/lib/options/SchemaBufferOptions.js b/lib/options/schemaBufferOptions.js
similarity index 92%
rename from lib/options/SchemaBufferOptions.js
rename to lib/options/schemaBufferOptions.js
index 377e3566a58..bfc12cb4590 100644
--- a/lib/options/SchemaBufferOptions.js
+++ b/lib/options/schemaBufferOptions.js
@@ -1,6 +1,6 @@
'use strict';
-const SchemaTypeOptions = require('./SchemaTypeOptions');
+const SchemaTypeOptions = require('./schemaTypeOptions');
/**
* The options defined on a Buffer schematype.
diff --git a/lib/options/SchemaDateOptions.js b/lib/options/schemaDateOptions.js
similarity index 96%
rename from lib/options/SchemaDateOptions.js
rename to lib/options/schemaDateOptions.js
index c7d3d4e1044..a4ef62bb5fe 100644
--- a/lib/options/SchemaDateOptions.js
+++ b/lib/options/schemaDateOptions.js
@@ -1,6 +1,6 @@
'use strict';
-const SchemaTypeOptions = require('./SchemaTypeOptions');
+const SchemaTypeOptions = require('./schemaTypeOptions');
/**
* The options defined on a Date schematype.
diff --git a/lib/options/SchemaDocumentArrayOptions.js b/lib/options/schemaDocumentArrayOptions.js
similarity index 96%
rename from lib/options/SchemaDocumentArrayOptions.js
rename to lib/options/schemaDocumentArrayOptions.js
index b826b87877b..ae5b07b3c4b 100644
--- a/lib/options/SchemaDocumentArrayOptions.js
+++ b/lib/options/schemaDocumentArrayOptions.js
@@ -1,6 +1,6 @@
'use strict';
-const SchemaTypeOptions = require('./SchemaTypeOptions');
+const SchemaTypeOptions = require('./schemaTypeOptions');
/**
* The options defined on an Document Array schematype.
diff --git a/lib/options/SchemaMapOptions.js b/lib/options/schemaMapOptions.js
similarity index 94%
rename from lib/options/SchemaMapOptions.js
rename to lib/options/schemaMapOptions.js
index bbabaa0700d..d6de6a4c74a 100644
--- a/lib/options/SchemaMapOptions.js
+++ b/lib/options/schemaMapOptions.js
@@ -1,6 +1,6 @@
'use strict';
-const SchemaTypeOptions = require('./SchemaTypeOptions');
+const SchemaTypeOptions = require('./schemaTypeOptions');
/**
* The options defined on a Map schematype.
diff --git a/lib/options/SchemaNumberOptions.js b/lib/options/schemaNumberOptions.js
similarity index 92%
rename from lib/options/SchemaNumberOptions.js
rename to lib/options/schemaNumberOptions.js
index bd91da01b19..62a1b51a64e 100644
--- a/lib/options/SchemaNumberOptions.js
+++ b/lib/options/schemaNumberOptions.js
@@ -1,6 +1,6 @@
'use strict';
-const SchemaTypeOptions = require('./SchemaTypeOptions');
+const SchemaTypeOptions = require('./schemaTypeOptions');
/**
* The options defined on a Number schematype.
@@ -69,7 +69,7 @@ Object.defineProperty(SchemaNumberOptions.prototype, 'max', opts);
Object.defineProperty(SchemaNumberOptions.prototype, 'enum', opts);
/**
- * Sets default [populate options](/docs/populate.html#query-conditions).
+ * Sets default [populate options](https://mongoosejs.com/docs/populate.html#query-conditions).
*
* #### Example:
*
diff --git a/lib/options/SchemaObjectIdOptions.js b/lib/options/schemaObjectIdOptions.js
similarity index 89%
rename from lib/options/SchemaObjectIdOptions.js
rename to lib/options/schemaObjectIdOptions.js
index 37048e92c4d..2029c9de578 100644
--- a/lib/options/SchemaObjectIdOptions.js
+++ b/lib/options/schemaObjectIdOptions.js
@@ -1,6 +1,6 @@
'use strict';
-const SchemaTypeOptions = require('./SchemaTypeOptions');
+const SchemaTypeOptions = require('./schemaTypeOptions');
/**
* The options defined on an ObjectId schematype.
@@ -32,7 +32,7 @@ const opts = require('./propertyOptions');
Object.defineProperty(SchemaObjectIdOptions.prototype, 'auto', opts);
/**
- * Sets default [populate options](/docs/populate.html#query-conditions).
+ * Sets default [populate options](https://mongoosejs.com/docs/populate.html#query-conditions).
*
* #### Example:
*
diff --git a/lib/options/SchemaStringOptions.js b/lib/options/schemaStringOptions.js
similarity index 95%
rename from lib/options/SchemaStringOptions.js
rename to lib/options/schemaStringOptions.js
index 49836ef13d0..c5bd8ca69ee 100644
--- a/lib/options/SchemaStringOptions.js
+++ b/lib/options/schemaStringOptions.js
@@ -1,6 +1,6 @@
'use strict';
-const SchemaTypeOptions = require('./SchemaTypeOptions');
+const SchemaTypeOptions = require('./schemaTypeOptions');
/**
* The options defined on a string schematype.
@@ -120,7 +120,7 @@ Object.defineProperty(SchemaStringOptions.prototype, 'maxLength', opts);
Object.defineProperty(SchemaStringOptions.prototype, 'maxlength', opts);
/**
- * Sets default [populate options](/docs/populate.html#query-conditions).
+ * Sets default [populate options](https://mongoosejs.com/docs/populate.html#query-conditions).
*
* @api public
* @property populate
diff --git a/lib/options/SchemaSubdocumentOptions.js b/lib/options/schemaSubdocumentOptions.js
similarity index 94%
rename from lib/options/SchemaSubdocumentOptions.js
rename to lib/options/schemaSubdocumentOptions.js
index 6782120350a..07c906726f4 100644
--- a/lib/options/SchemaSubdocumentOptions.js
+++ b/lib/options/schemaSubdocumentOptions.js
@@ -1,6 +1,6 @@
'use strict';
-const SchemaTypeOptions = require('./SchemaTypeOptions');
+const SchemaTypeOptions = require('./schemaTypeOptions');
/**
* The options defined on a single nested schematype.
diff --git a/lib/options/SchemaTypeOptions.js b/lib/options/schemaTypeOptions.js
similarity index 98%
rename from lib/options/SchemaTypeOptions.js
rename to lib/options/schemaTypeOptions.js
index f2376431034..cfe3de3cd24 100644
--- a/lib/options/SchemaTypeOptions.js
+++ b/lib/options/schemaTypeOptions.js
@@ -161,7 +161,7 @@ Object.defineProperty(SchemaTypeOptions.prototype, 'index', opts);
/**
* 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).
+ * model is compiled. [The `unique` option is **not** a validator](https://mongoosejs.com/docs/validation.html#the-unique-option-is-not-a-validator).
*
* @api public
* @property unique
diff --git a/lib/options/VirtualOptions.js b/lib/options/virtualOptions.js
similarity index 98%
rename from lib/options/VirtualOptions.js
rename to lib/options/virtualOptions.js
index 3db53b99d27..c17c5a405ff 100644
--- a/lib/options/VirtualOptions.js
+++ b/lib/options/virtualOptions.js
@@ -146,7 +146,7 @@ Object.defineProperty(VirtualOptions.prototype, 'skip', opts);
Object.defineProperty(VirtualOptions.prototype, 'limit', opts);
/**
- * The `limit` option for `populate()` has [some unfortunate edge cases](/docs/populate.html#query-conditions)
+ * The `limit` option for `populate()` has [some unfortunate edge cases](https://mongoosejs.com/docs/populate.html#query-conditions)
* when working with multiple documents, like `.find().populate()`. The
* `perDocumentLimit` option makes `populate()` execute a separate query
* for each document returned from `find()` to ensure each document
diff --git a/lib/plugins/index.js b/lib/plugins/index.js
index 69fa6ad284c..a8a6c044240 100644
--- a/lib/plugins/index.js
+++ b/lib/plugins/index.js
@@ -1,6 +1,5 @@
'use strict';
-exports.removeSubdocs = require('./removeSubdocs');
exports.saveSubdocs = require('./saveSubdocs');
exports.sharding = require('./sharding');
exports.trackTransaction = require('./trackTransaction');
diff --git a/lib/plugins/removeSubdocs.js b/lib/plugins/removeSubdocs.js
deleted file mode 100644
index e320f782a0e..00000000000
--- a/lib/plugins/removeSubdocs.js
+++ /dev/null
@@ -1,31 +0,0 @@
-'use strict';
-
-const each = require('../helpers/each');
-
-/*!
- * ignore
- */
-
-module.exports = function removeSubdocs(schema) {
- const unshift = true;
- schema.s.hooks.pre('remove', false, function removeSubDocsPreRemove(next) {
- if (this.$isSubdocument) {
- next();
- return;
- }
-
- const _this = this;
- const subdocs = this.$getAllSubdocs();
-
- each(subdocs, function(subdoc, cb) {
- subdoc.$__remove(cb);
- }, function(error) {
- if (error) {
- return _this.$__schema.s.hooks.execPost('remove:error', _this, [_this], { error: error }, function(error) {
- next(error);
- });
- }
- next();
- });
- }, null, unshift);
-};
diff --git a/lib/plugins/saveSubdocs.js b/lib/plugins/saveSubdocs.js
index 758acbbfe2e..bb88db59f85 100644
--- a/lib/plugins/saveSubdocs.js
+++ b/lib/plugins/saveSubdocs.js
@@ -15,7 +15,7 @@ module.exports = function saveSubdocs(schema) {
}
const _this = this;
- const subdocs = this.$getAllSubdocs();
+ const subdocs = this.$getAllSubdocs({ useCache: true });
if (!subdocs.length) {
next();
@@ -27,6 +27,10 @@ module.exports = function saveSubdocs(schema) {
cb(err);
});
}, function(error) {
+ // Invalidate subdocs cache because subdoc pre hooks can add new subdocuments
+ if (_this.$__.saveOptions) {
+ _this.$__.saveOptions.__subdocs = null;
+ }
if (error) {
return _this.$__schema.s.hooks.execPost('save:error', _this, [_this], { error: error }, function(error) {
next(error);
@@ -36,31 +40,63 @@ module.exports = function saveSubdocs(schema) {
});
}, null, unshift);
- schema.s.hooks.post('save', function saveSubdocsPostSave(doc, next) {
+ schema.s.hooks.post('save', async function saveSubdocsPostDeleteOne() {
+ const removedSubdocs = this.$__.removedSubdocs;
+ if (!removedSubdocs || !removedSubdocs.length) {
+ return;
+ }
+
+ const promises = [];
+ for (const subdoc of removedSubdocs) {
+ promises.push(new Promise((resolve, reject) => {
+ subdoc.$__schema.s.hooks.execPost('deleteOne', subdoc, [subdoc], function(err) {
+ if (err) {
+ return reject(err);
+ }
+ resolve();
+ });
+ }));
+ }
+
+ this.$__.removedSubdocs = null;
+ await Promise.all(promises);
+ });
+
+ schema.s.hooks.post('save', async function saveSubdocsPostSave() {
if (this.$isSubdocument) {
- next();
return;
}
const _this = this;
- const subdocs = this.$getAllSubdocs();
+ const subdocs = this.$getAllSubdocs({ useCache: true });
if (!subdocs.length) {
- next();
return;
}
- each(subdocs, function(subdoc, cb) {
- subdoc.$__schema.s.hooks.execPost('save', subdoc, [subdoc], function(err) {
- cb(err);
- });
- }, function(error) {
- if (error) {
- return _this.$__schema.s.hooks.execPost('save:error', _this, [_this], { error: error }, function(error) {
- next(error);
+ const promises = [];
+ for (const subdoc of subdocs) {
+ promises.push(new Promise((resolve, reject) => {
+ subdoc.$__schema.s.hooks.execPost('save', subdoc, [subdoc], function(err) {
+ if (err) {
+ return reject(err);
+ }
+ resolve();
});
- }
- next();
- });
+ }));
+ }
+
+ try {
+ await Promise.all(promises);
+ } catch (error) {
+ await new Promise((resolve, reject) => {
+ this.$__schema.s.hooks.execPost('save:error', _this, [_this], { error: error }, function(error) {
+ if (error) {
+ return reject(error);
+ }
+ resolve();
+ });
+ });
+ }
}, null, unshift);
};
diff --git a/lib/plugins/trackTransaction.js b/lib/plugins/trackTransaction.js
index 1a409a026eb..857caac6044 100644
--- a/lib/plugins/trackTransaction.js
+++ b/lib/plugins/trackTransaction.js
@@ -27,13 +27,6 @@ module.exports = function trackTransaction(schema) {
initialState.atomics = _getAtomics(this);
session[sessionNewDocuments].set(this, initialState);
- } else {
- const state = session[sessionNewDocuments].get(this);
-
- for (const path of Object.keys(this.$__.activePaths.getStatePaths('modify'))) {
- state.modifiedPaths.add(path);
- }
- state.atomics = _getAtomics(this, state.atomics);
}
});
};
@@ -85,7 +78,7 @@ function mergeAtomics(destination, source) {
destination.$addToSet = (destination.$addToSet || []).concat(source.$addToSet);
}
if (source.$set != null) {
- destination.$set = Object.assign(destination.$set || {}, source.$set);
+ destination.$set = Array.isArray(source.$set) ? [...source.$set] : Object.assign({}, source.$set);
}
return destination;
diff --git a/lib/plugins/validateBeforeSave.js b/lib/plugins/validateBeforeSave.js
index 7ebdf4b993f..c55824184ac 100644
--- a/lib/plugins/validateBeforeSave.js
+++ b/lib/plugins/validateBeforeSave.js
@@ -32,12 +32,18 @@ module.exports = function validateBeforeSave(schema) {
const validateOptions = hasValidateModifiedOnlyOption ?
{ validateModifiedOnly: options.validateModifiedOnly } :
null;
- this.$validate(validateOptions, function(error) {
- return _this.$__schema.s.hooks.execPost('save:error', _this, [_this], { error: error }, function(error) {
- _this.$op = 'save';
- next(error);
- });
- });
+ this.$validate(validateOptions).then(
+ () => {
+ this.$op = 'save';
+ next();
+ },
+ error => {
+ _this.$__schema.s.hooks.execPost('save:error', _this, [_this], { error: error }, function(error) {
+ _this.$op = 'save';
+ next(error);
+ });
+ }
+ );
} else {
next();
}
diff --git a/lib/promise_provider.js b/lib/promise_provider.js
deleted file mode 100644
index 3febf368784..00000000000
--- a/lib/promise_provider.js
+++ /dev/null
@@ -1,49 +0,0 @@
-/*!
- * ignore
- */
-
-'use strict';
-
-const assert = require('assert');
-const mquery = require('mquery');
-
-/**
- * Helper for multiplexing promise implementations
- *
- * @api private
- */
-
-const store = {
- _promise: null
-};
-
-/**
- * Get the current promise constructor
- *
- * @api private
- */
-
-store.get = function() {
- return store._promise;
-};
-
-/**
- * Set the current promise constructor
- *
- * @api private
- */
-
-store.set = function(lib) {
- assert.ok(typeof lib === 'function',
- `mongoose.Promise must be a function, got ${lib}`);
- store._promise = lib;
- mquery.Promise = lib;
-};
-
-/*!
- * Use native promises by default
- */
-
-store.set(global.Promise);
-
-module.exports = store;
diff --git a/lib/query.js b/lib/query.js
index 4dc900f4f78..067a6e020ed 100644
--- a/lib/query.js
+++ b/lib/query.js
@@ -9,23 +9,23 @@ const DocumentNotFoundError = require('./error/notFound');
const Kareem = require('kareem');
const MongooseError = require('./error/mongooseError');
const ObjectParameterError = require('./error/objectParameter');
-const QueryCursor = require('./cursor/QueryCursor');
-const ReadPreference = require('./driver').get().ReadPreference;
+const QueryCursor = require('./cursor/queryCursor');
const ValidationError = require('./error/validation');
const { applyGlobalMaxTimeMS, applyGlobalDiskUse } = require('./helpers/query/applyGlobalOption');
+const handleReadPreferenceAliases = require('./helpers/query/handleReadPreferenceAliases');
+const applyReadConcern = require('./helpers/schema/applyReadConcern');
const applyWriteConcern = require('./helpers/schema/applyWriteConcern');
const cast = require('./cast');
const castArrayFilters = require('./helpers/update/castArrayFilters');
const castNumber = require('./cast/number');
const castUpdate = require('./helpers/query/castUpdate');
-const completeMany = require('./helpers/query/completeMany');
-const promiseOrCallback = require('./helpers/promiseOrCallback');
+const clone = require('./helpers/clone');
const getDiscriminatorByValue = require('./helpers/discriminator/getDiscriminatorByValue');
-const hasDollarKeys = require('./helpers/query/hasDollarKeys');
-const helpers = require('./queryhelpers');
-const immediate = require('./helpers/immediate');
+const helpers = require('./queryHelpers');
+const internalToObjectOptions = require('./options').internalToObjectOptions;
const isExclusive = require('./helpers/projection/isExclusive');
const isInclusive = require('./helpers/projection/isInclusive');
+const isPathSelectedInclusive = require('./helpers/projection/isPathSelectedInclusive');
const isSubpath = require('./helpers/projection/isSubpath');
const mpath = require('mpath');
const mquery = require('mquery');
@@ -35,11 +35,11 @@ const sanitizeFilter = require('./helpers/query/sanitizeFilter');
const sanitizeProjection = require('./helpers/query/sanitizeProjection');
const selectPopulatedFields = require('./helpers/query/selectPopulatedFields');
const setDefaultsOnInsert = require('./helpers/setDefaultsOnInsert');
+const specialProperties = require('./helpers/specialProperties');
const updateValidators = require('./helpers/updateValidators');
const util = require('util');
const utils = require('./utils');
-const validOps = require('./helpers/query/validOps');
-const wrapThunk = require('./helpers/query/wrapThunk');
+const queryMiddlewareFunctions = require('./constants').queryMiddlewareFunctions;
const queryOptionMethods = new Set([
'allowDiskUse',
@@ -51,9 +51,7 @@ const queryOptionMethods = new Set([
'j',
'lean',
'limit',
- 'maxScan',
'maxTimeMS',
- 'maxscan',
'populate',
'projection',
'read',
@@ -70,7 +68,7 @@ const queryOptionMethods = new Set([
/**
* Query constructor used for building queries. You do not need
* to instantiate a `Query` directly. Instead use Model functions like
- * [`Model.find()`](/docs/api/model.html#model_Model-find).
+ * [`Model.find()`](https://mongoosejs.com/docs/api/model.html#Model.find()).
*
* #### Example:
*
@@ -117,7 +115,6 @@ function Query(conditions, options, model, collection) {
this.schema = model.schema;
}
-
// this is needed because map reduce returns a model that can be queried, but
// all of the queries on said model should be lean
if (this.model && this.model._mapreduce) {
@@ -155,8 +152,26 @@ function Query(conditions, options, model, collection) {
Query.prototype = new mquery();
Query.prototype.constructor = Query;
+
+// Remove some legacy methods that we removed in Mongoose 8, but
+// are still in mquery 5.
+Query.prototype.count = undefined;
+Query.prototype.findOneAndRemove = undefined;
+
Query.base = mquery.prototype;
+/*!
+ * Overwrite mquery's `_distinct`, because Mongoose uses that name
+ * to store the field to apply distinct on.
+ */
+
+Object.defineProperty(Query.prototype, '_distinct', {
+ configurable: true,
+ writable: true,
+ enumerable: true,
+ value: undefined
+});
+
/**
* Flag to opt out of using `$geoWithin`.
*
@@ -189,12 +204,8 @@ Query.use$geoWithin = mquery.use$geoWithin;
* // create a custom Query constructor based off these settings
* const Adventure = query.toConstructor();
*
- * // Adventure is now a subclass of mongoose.Query and works the same way but with the
- * // default query parameters and options set.
- * Adventure().exec(callback)
- *
* // further narrow down our query results while still using the previous settings
- * Adventure().where({ name: /^Life/ }).exec(callback);
+ * await Adventure().where({ name: /^Life/ }).exec();
*
* // since Adventure is a stand-alone constructor we can also add our own
* // helper methods and getters without impacting global queries
@@ -208,7 +219,7 @@ Query.use$geoWithin = mquery.use$geoWithin;
* return this;
* }
* })
- * Adventure().highlyRated.startsWith('Life').exec(callback)
+ * await Adventure().highlyRated.startsWith('Life').exec();
*
* @return {Query} subclass-of-Query
* @api public
@@ -222,7 +233,7 @@ Query.prototype.toConstructor = function toConstructor() {
if (!(this instanceof CustomQuery)) {
return new CustomQuery(criteria, options);
}
- this._mongooseOptions = utils.clone(p._mongooseOptions);
+ this._mongooseOptions = clone(p._mongooseOptions);
Query.call(this, criteria, options || null, model, coll);
};
@@ -245,9 +256,9 @@ Query.prototype.toConstructor = function toConstructor() {
p.op = this.op;
p._validateOp();
- p._conditions = utils.clone(this._conditions);
- p._fields = utils.clone(this._fields);
- p._update = utils.clone(this._update, {
+ p._conditions = clone(this._conditions);
+ p._fields = clone(this._fields);
+ p._update = clone(this._update, {
flattenDecimals: false
});
p._path = this._path;
@@ -276,7 +287,7 @@ Query.prototype.toConstructor = function toConstructor() {
* @api public
*/
-Query.prototype.clone = function clone() {
+Query.prototype.clone = function() {
const model = this.model;
const collection = this.mongooseCollection;
@@ -294,9 +305,9 @@ Query.prototype.clone = function clone() {
q.op = this.op;
q._validateOp();
- q._conditions = utils.clone(this._conditions);
- q._fields = utils.clone(this._fields);
- q._update = utils.clone(this._update, {
+ q._conditions = clone(this._conditions);
+ q._fields = clone(this._fields);
+ q._update = clone(this._update, {
flattenDecimals: false
});
q._path = this._path;
@@ -341,7 +352,7 @@ Query.prototype.clone = function clone() {
* #### Example:
*
* // instead of writing:
- * User.find({age: {$gte: 21, $lte: 65}}, callback);
+ * User.find({age: {$gte: 21, $lte: 65}});
*
* // we can instead write:
* User.where('age').gte(21).lte(65);
@@ -354,7 +365,7 @@ Query.prototype.clone = function clone() {
* .where('age').gte(21).lte(65)
* .where('name', /^vonderful/i)
* .where('friends').slice(10)
- * .exec(callback)
+ * .exec()
*
* @method where
* @memberOf Query
@@ -451,7 +462,7 @@ Query.prototype.slice = function() {
* ignore
*/
-const validOpsSet = new Set(validOps);
+const validOpsSet = new Set(queryMiddlewareFunctions);
Query.prototype._validateOp = function() {
if (this.op != null && !validOpsSet.has(this.op)) {
@@ -820,7 +831,7 @@ Query.prototype.mod = function() {
*
* #### Note:
*
- * As of Mongoose 3.7, `$geoWithin` is always used for queries. To change this behavior, see [Query.use$geoWithin](#query_Query-use%2524geoWithin).
+ * As of Mongoose 3.7, `$geoWithin` is always used for queries. To change this behavior, see [Query.use$geoWithin](https://mongoosejs.com/docs/api/query.html#Query.prototype.use$geoWithin).
*
* #### Note:
*
@@ -905,25 +916,6 @@ Query.prototype.skip = function skip(v) {
return this;
};
-/**
- * Specifies the maxScan option.
- *
- * #### Example:
- *
- * query.maxScan(100);
- *
- * #### Note:
- *
- * Cannot be used with `distinct()`
- *
- * @method maxScan
- * @memberOf Query
- * @instance
- * @param {Number} val
- * @see maxScan https://www.mongodb.com/docs/v4.0/reference/method/cursor.maxScan/
- * @api public
- */
-
/**
* Specifies the batchSize option.
*
@@ -962,26 +954,6 @@ Query.prototype.skip = function skip(v) {
* @api public
*/
-/**
- * Specifies this query as a `snapshot` query.
- *
- * #### Example:
- *
- * query.snapshot(); // true
- * query.snapshot(true);
- * query.snapshot(false);
- *
- * #### Note:
- *
- * Cannot be used with `distinct()`
- *
- * @method snapshot
- * @memberOf Query
- * @instance
- * @return {Query} this
- * @api public
- */
-
/**
* Sets query hints.
*
@@ -1046,7 +1018,7 @@ Query.prototype.projection = function(arg) {
/**
* Specifies which document fields to include or exclude (also known as the query "projection")
*
- * When using string syntax, prefixing a path with `-` will flag that path as excluded. When a path does not have the `-` prefix, it is included. Lastly, if a path is prefixed with `+`, it forces inclusion of the path, which is useful for paths excluded at the [schema level](/docs/api/schematype.html#schematype_SchemaType-select).
+ * When using string syntax, prefixing a path with `-` will flag that path as excluded. When a path does not have the `-` prefix, it is included. Lastly, if a path is prefixed with `+`, it forces inclusion of the path, which is useful for paths excluded at the [schema level](https://mongoosejs.com/docs/api/schematype.html#SchemaType.prototype.select()).
*
* A projection _must_ be either inclusive or exclusive. In other words, you must
* either list the fields to include (which excludes all others), or list the fields
@@ -1087,7 +1059,7 @@ Query.prototype.projection = function(arg) {
* @instance
* @param {Object|String|String[]} arg
* @return {Query} this
- * @see SchemaType /docs/api/schematype.html
+ * @see SchemaType https://mongoosejs.com/docs/api/schematype.html
* @api public
*/
@@ -1115,14 +1087,15 @@ Query.prototype.select = function select() {
function sanitizeValue(value) {
return typeof value === 'string' && sanitizeProjection ? value = 1 : value;
}
-
- arg = parseProjection(arg);
-
+ arg = parseProjection(arg, true); // we want to keep the minus and pluses, so add boolean arg.
if (utils.isObject(arg)) {
if (this.selectedInclusively()) {
Object.entries(arg).forEach(([key, value]) => {
if (value) {
// Add the field to the projection
+ if (fields['-' + key] != null) {
+ delete fields['-' + key];
+ }
fields[key] = userProvidedFields[key] = sanitizeValue(value);
} else {
// Remove the field from the projection
@@ -1138,6 +1111,9 @@ Query.prototype.select = function select() {
Object.entries(arg).forEach(([key, value]) => {
if (!value) {
// Add the field to the projection
+ if (fields['+' + key] != null) {
+ delete fields['+' + key];
+ }
fields[key] = userProvidedFields[key] = sanitizeValue(value);
} else {
// Remove the field from the projection
@@ -1153,16 +1129,103 @@ Query.prototype.select = function select() {
const keys = Object.keys(arg);
for (let i = 0; i < keys.length; ++i) {
const value = arg[keys[i]];
- fields[keys[i]] = sanitizeValue(value);
- userProvidedFields[keys[i]] = sanitizeValue(value);
+ const key = keys[i];
+ fields[key] = sanitizeValue(value);
+ userProvidedFields[key] = sanitizeValue(value);
}
}
+
return this;
}
throw new TypeError('Invalid select() argument. Must be string or object.');
};
+/**
+ * Enable or disable schema level projections for this query. Enabled by default.
+ * Set to `false` to include fields with `select: false` in the query result by default.
+ *
+ * #### Example:
+ *
+ * const userSchema = new Schema({
+ * email: { type: String, required: true },
+ * passwordHash: { type: String, select: false, required: true }
+ * });
+ * const UserModel = mongoose.model('User', userSchema);
+ *
+ * const doc = await UserModel.findOne().orFail().schemaLevelProjections(false);
+ *
+ * // Contains password hash, because `schemaLevelProjections()` overrides `select: false`
+ * doc.passwordHash;
+ *
+ * @method schemaLevelProjections
+ * @memberOf Query
+ * @instance
+ * @param {Boolean} value
+ * @return {Query} this
+ * @see SchemaTypeOptions https://mongoosejs.com/docs/schematypes.html#all-schema-types
+ * @api public
+ */
+
+Query.prototype.schemaLevelProjections = function schemaLevelProjections(value) {
+ this._mongooseOptions.schemaLevelProjections = value;
+
+ return this;
+};
+
+/**
+ * Sets this query's `sanitizeProjection` option. If set, `sanitizeProjection` does
+ * two things:
+ *
+ * 1. Enforces that projection values are numbers, not strings.
+ * 2. Prevents using `+` syntax to override properties that are deselected by default.
+ *
+ * With `sanitizeProjection()`, you can pass potentially untrusted user data to `.select()`.
+ *
+ * #### Example
+ *
+ * const userSchema = new Schema({
+ * name: String,
+ * password: { type: String, select: false }
+ * });
+ * const UserModel = mongoose.model('User', userSchema);
+ * const { _id } = await UserModel.create({ name: 'John', password: 'secret' })
+ *
+ * // The MongoDB server has special handling for string values that start with '$'
+ * // in projections, which can lead to unexpected leaking of sensitive data.
+ * let doc = await UserModel.findOne().select({ name: '$password' });
+ * doc.name; // 'secret'
+ * doc.password; // undefined
+ *
+ * // With `sanitizeProjection`, Mongoose forces all projection values to be numbers
+ * doc = await UserModel.findOne().sanitizeProjection(true).select({ name: '$password' });
+ * doc.name; // 'John'
+ * doc.password; // undefined
+ *
+ * // By default, Mongoose supports projecting in `password` using `+password`
+ * doc = await UserModel.findOne().select('+password');
+ * doc.password; // 'secret'
+ *
+ * // With `sanitizeProjection`, Mongoose prevents projecting in `password` and other
+ * // fields that have `select: false` in the schema.
+ * doc = await UserModel.findOne().sanitizeProjection(true).select('+password');
+ * doc.password; // undefined
+ *
+ * @method sanitizeProjection
+ * @memberOf Query
+ * @instance
+ * @param {Boolean} value
+ * @return {Query} this
+ * @see sanitizeProjection https://thecodebarbarian.com/whats-new-in-mongoose-5-13-sanitizeprojection.html
+ * @api public
+ */
+
+Query.prototype.sanitizeProjection = function sanitizeProjection(value) {
+ this._mongooseOptions.sanitizeProjection = value;
+
+ return this;
+};
+
/**
* Determines the MongoDB nodes from which to read.
*
@@ -1211,17 +1274,20 @@ Query.prototype.select = function select() {
* @method read
* @memberOf Query
* @instance
- * @param {String} pref one of the listed preference options or aliases
+ * @param {String} mode one of the listed preference options or aliases
* @param {Array} [tags] optional tags for this query
* @see mongodb https://www.mongodb.com/docs/manual/applications/replication/#read-preference
* @return {Query} this
* @api public
*/
-Query.prototype.read = function read(pref, tags) {
- // first cast into a ReadPreference object to support tags
- const read = new ReadPreference(pref, tags);
- this.options.readPreference = read;
+Query.prototype.read = function read(mode, tags) {
+ if (typeof mode === 'string') {
+ mode = handleReadPreferenceAliases(mode);
+ this.options.readPreference = { mode, tags };
+ } else {
+ this.options.readPreference = mode;
+ }
return this;
};
@@ -1242,7 +1308,6 @@ Query.prototype.toString = function toString() {
this.op === 'deleteMany' ||
this.op === 'deleteOne' ||
this.op === 'findOneAndDelete' ||
- this.op === 'findOneAndRemove' ||
this.op === 'remove') {
return `${this.model.modelName}.${this.op}(${util.inspect(this._conditions)})`;
}
@@ -1265,7 +1330,7 @@ Query.prototype.toString = function toString() {
/**
* Sets the [MongoDB session](https://www.mongodb.com/docs/manual/reference/server-sessions/)
* associated with this query. Sessions are how you mark a query as part of a
- * [transaction](/docs/transactions.html).
+ * [transaction](https://mongoosejs.com/docs/transactions.html).
*
* Calling `session(null)` removes the session from this query.
*
@@ -1278,8 +1343,8 @@ Query.prototype.toString = function toString() {
* @memberOf Query
* @instance
* @param {ClientSession} [session] from `await conn.startSession()`
- * @see Connection.prototype.startSession() /docs/api/connection.html#connection_Connection-startSession
- * @see mongoose.startSession() /docs/api/mongoose.html#mongoose_Mongoose-startSession
+ * @see Connection.prototype.startSession() https://mongoosejs.com/docs/api/connection.html#Connection.prototype.startSession()
+ * @see mongoose.startSession() https://mongoosejs.com/docs/api/mongoose.html#Mongoose.prototype.startSession()
* @return {Query} this
* @api public
*/
@@ -1297,7 +1362,7 @@ Query.prototype.session = function session(v) {
*
* - `w`: Sets the specified number of `mongod` servers, or tag set of `mongod` servers, that must acknowledge this write before this write is considered successful.
* - `j`: Boolean, set to `true` to request acknowledgement that this operation has been persisted to MongoDB's on-disk journal.
- * - `wtimeout`: If [`w > 1`](#query_Query-w), the maximum amount of time to wait for this write to propagate through the replica set before this operation fails. The default is `0`, which means no timeout.
+ * - `wtimeout`: If [`w > 1`](https://mongoosejs.com/docs/api/query.html#Query.prototype.w()), the maximum amount of time to wait for this write to propagate through the replica set before this operation fails. The default is `0`, which means no timeout.
*
* This option is only valid for operations that write to the database:
*
@@ -1306,12 +1371,10 @@ Query.prototype.session = function session(v) {
* - `findOneAndDelete()`
* - `findOneAndReplace()`
* - `findOneAndUpdate()`
- * - `remove()`
- * - `update()`
* - `updateOne()`
* - `updateMany()`
*
- * Defaults to the schema's [`writeConcern` option](/docs/guide.html#writeConcern)
+ * Defaults to the schema's [`writeConcern` option](https://mongoosejs.com/docs/guide.html#writeConcern)
*
* #### Example:
*
@@ -1349,12 +1412,10 @@ Query.prototype.writeConcern = function writeConcern(val) {
* - `findOneAndDelete()`
* - `findOneAndReplace()`
* - `findOneAndUpdate()`
- * - `remove()`
- * - `update()`
* - `updateOne()`
* - `updateMany()`
*
- * Defaults to the schema's [`writeConcern.w` option](/docs/guide.html#writeConcern)
+ * Defaults to the schema's [`writeConcern.w` option](https://mongoosejs.com/docs/guide.html#writeConcern)
*
* #### Example:
*
@@ -1395,12 +1456,10 @@ Query.prototype.w = function w(val) {
* - `findOneAndDelete()`
* - `findOneAndReplace()`
* - `findOneAndUpdate()`
- * - `remove()`
- * - `update()`
* - `updateOne()`
* - `updateMany()`
*
- * Defaults to the schema's [`writeConcern.j` option](/docs/guide.html#writeConcern)
+ * Defaults to the schema's [`writeConcern.j` option](https://mongoosejs.com/docs/guide.html#writeConcern)
*
* #### Example:
*
@@ -1428,7 +1487,7 @@ Query.prototype.j = function j(val) {
};
/**
- * If [`w > 1`](#query_Query-w), the maximum amount of time to
+ * If [`w > 1`](https://mongoosejs.com/docs/api/query.html#Query.prototype.w()), the maximum amount of time to
* wait for this write to propagate through the replica set before this
* operation fails. The default is `0`, which means no timeout.
*
@@ -1439,12 +1498,10 @@ Query.prototype.j = function j(val) {
* - `findOneAndDelete()`
* - `findOneAndReplace()`
* - `findOneAndUpdate()`
- * - `remove()`
- * - `update()`
* - `updateOne()`
* - `updateMany()`
*
- * Defaults to the schema's [`writeConcern.wtimeout` option](/docs/guide.html#writeConcern)
+ * Defaults to the schema's [`writeConcern.wtimeout` option](https://mongoosejs.com/docs/guide.html#writeConcern)
*
* #### Example:
*
@@ -1554,7 +1611,6 @@ Query.prototype.getOptions = function() {
* The following options are only for `find()`:
*
* - [tailable](https://www.mongodb.com/docs/manual/core/tailable-cursors/)
- * - [sort](https://www.mongodb.com/docs/manual/reference/method/cursor.sort/)
* - [limit](https://www.mongodb.com/docs/manual/reference/method/cursor.limit/)
* - [skip](https://www.mongodb.com/docs/manual/reference/method/cursor.skip/)
* - [allowDiskUse](https://www.mongodb.com/docs/manual/reference/method/cursor.allowDiskUse/)
@@ -1562,34 +1618,38 @@ Query.prototype.getOptions = function() {
* - [readPreference](https://www.mongodb.com/docs/manual/applications/replication/#read-preference)
* - [hint](https://www.mongodb.com/docs/manual/reference/method/cursor.hint/)
* - [comment](https://www.mongodb.com/docs/manual/reference/method/cursor.comment/)
- * - snapshot
- * - [maxscan](https://www.mongodb.com/docs/v4.0/reference/method/cursor.maxScan/)
*
- * The following options are only for write operations: `update()`, `updateOne()`, `updateMany()`, `replaceOne()`, `findOneAndUpdate()`, and `findByIdAndUpdate()`:
+ * The following options are only for write operations: `updateOne()`, `updateMany()`, `replaceOne()`, `findOneAndUpdate()`, and `findByIdAndUpdate()`:
*
* - [upsert](https://www.mongodb.com/docs/manual/reference/method/db.collection.update/)
* - [writeConcern](https://www.mongodb.com/docs/manual/reference/method/db.collection.update/)
* - [timestamps](https://mongoosejs.com/docs/guide.html#timestamps): If `timestamps` is set in the schema, set this option to `false` to skip timestamps for that particular update. Has no effect if `timestamps` is not enabled in the schema options.
* - overwriteDiscriminatorKey: allow setting the discriminator key in the update. Will use the correct discriminator schema if the update changes the discriminator key.
+ * - overwriteImmutable: allow overwriting properties that are set to `immutable` in the schema. Defaults to false.
*
- * The following options are only for `find()`, `findOne()`, `findById()`, `findOneAndUpdate()`, and `findByIdAndUpdate()`:
+ * The following options are only for `find()`, `findOne()`, `findById()`, `findOneAndUpdate()`, `findOneAndReplace()`, `findOneAndDelete()`, and `findByIdAndUpdate()`:
*
- * - [lean](#query_Query-lean)
- * - [populate](/docs/populate.html)
- * - [projection](#query_Query-projection)
+ * - [lean](https://mongoosejs.com/docs/api/query.html#Query.prototype.lean())
+ * - [populate](https://mongoosejs.com/docs/populate.html)
+ * - [projection](https://mongoosejs.com/docs/api/query.html#Query.prototype.projection())
* - sanitizeProjection
+ * - useBigInt64
*
- * The following options are only for all operations **except** `update()`, `updateOne()`, `updateMany()`, `remove()`, `deleteOne()`, and `deleteMany()`:
+ * The following options are only for all operations **except** `updateOne()`, `updateMany()`, `deleteOne()`, and `deleteMany()`:
*
* - [maxTimeMS](https://www.mongodb.com/docs/manual/reference/operator/meta/maxTimeMS/)
*
- * The following options are for `findOneAndUpdate()` and `findOneAndRemove()`
+ * The following options are for `find()`, `findOne()`, `findOneAndUpdate()`, `findOneAndDelete()`, `updateOne()`, and `deleteOne()`:
+ *
+ * - [sort](https://www.mongodb.com/docs/manual/reference/method/cursor.sort/)
+ *
+ * The following options are for `findOneAndUpdate()` and `findOneAndDelete()`
*
- * - rawResult
+ * - includeResultMetadata
*
* The following options are for all operations:
*
- * - [strict](/docs/guide.html#strict)
+ * - [strict](https://mongoosejs.com/docs/guide.html#strict)
* - [collation](https://www.mongodb.com/docs/manual/reference/collation/)
* - [session](https://www.mongodb.com/docs/manual/reference/server-sessions/)
* - [explain](https://www.mongodb.com/docs/manual/reference/method/cursor.explain/)
@@ -1603,7 +1663,7 @@ Query.prototype.setOptions = function(options, overwrite) {
// overwrite is only for internal use
if (overwrite) {
// ensure that _mongooseOptions & options are two different objects
- this._mongooseOptions = (options && utils.clone(options)) || {};
+ this._mongooseOptions = (options && clone(options)) || {};
this.options = options || {};
if ('populate' in options) {
@@ -1637,6 +1697,10 @@ Query.prototype.setOptions = function(options, overwrite) {
this._mongooseOptions.overwriteDiscriminatorKey = options.overwriteDiscriminatorKey;
delete options.overwriteDiscriminatorKey;
}
+ if ('overwriteImmutable' in options) {
+ this._mongooseOptions.overwriteImmutable = options.overwriteImmutable;
+ delete options.overwriteImmutable;
+ }
if ('sanitizeProjection' in options) {
if (options.sanitizeProjection && !this._mongooseOptions.sanitizeProjection) {
sanitizeProjection(this._fields);
@@ -1649,11 +1713,23 @@ Query.prototype.setOptions = function(options, overwrite) {
this._mongooseOptions.sanitizeFilter = options.sanitizeFilter;
delete options.sanitizeFilter;
}
-
+ if ('timestamps' in options) {
+ this._mongooseOptions.timestamps = options.timestamps;
+ delete options.timestamps;
+ }
if ('defaults' in options) {
this._mongooseOptions.defaults = options.defaults;
// deleting options.defaults will cause 7287 to fail
}
+ if ('translateAliases' in options) {
+ this._mongooseOptions.translateAliases = options.translateAliases;
+ delete options.translateAliases;
+ }
+ if ('schemaLevelProjections' in options) {
+ this._mongooseOptions.schemaLevelProjections = options.schemaLevelProjections;
+ delete options.schemaLevelProjections;
+ }
+
if (options.lean == null && this.schema && 'lean' in this.schema.options) {
this._mongooseOptions.lean = this.schema.options.lean;
}
@@ -1707,7 +1783,7 @@ Query.prototype.setOptions = function(options, overwrite) {
* @api public
*/
-Query.prototype.explain = function(verbose) {
+Query.prototype.explain = function explain(verbose) {
if (arguments.length === 0) {
this.options.explain = true;
} else if (verbose === false) {
@@ -1836,7 +1912,7 @@ Query.prototype.setQuery = function(val) {
* #### Example:
*
* const query = new Query();
- * query.update({}, { $set: { a: 5 } });
+ * query.updateOne({}, { $set: { a: 5 } });
* query.getUpdate(); // { $set: { a: 5 } }
*
* @return {Object} current update operations
@@ -1853,7 +1929,7 @@ Query.prototype.getUpdate = function() {
* #### Example:
*
* const query = new Query();
- * query.update({}, { $set: { a: 5 } });
+ * query.updateOne({}, { $set: { a: 5 } });
* query.setUpdate({ $set: { b: 6 } });
* query.getUpdate(); // { $set: { b: 6 } }
*
@@ -1882,7 +1958,7 @@ Query.prototype._fieldsForExec = function() {
if (Object.keys(this._fields).length === 0) {
return null;
}
- return utils.clone(this._fields);
+ return clone(this._fields);
};
@@ -1896,7 +1972,7 @@ Query.prototype._fieldsForExec = function() {
*/
Query.prototype._updateForExec = function() {
- const update = utils.clone(this._update, {
+ const update = clone(this._update, {
transform: false,
depopulate: true
});
@@ -1907,11 +1983,6 @@ Query.prototype._updateForExec = function() {
while (i--) {
const op = ops[i];
- if (this.options.overwrite) {
- ret[op] = update[op];
- continue;
- }
-
if ('$' !== op[0]) {
// fix up $set sugar
if (!ret.$set) {
@@ -1966,17 +2037,22 @@ Query.prototype._updateForExec = function() {
*/
Query.prototype._optionsForExec = function(model) {
- const options = utils.clone(this.options);
+ const options = clone(this.options);
delete options.populate;
model = model || this.model;
if (!model) {
return options;
}
-
+ applyReadConcern(model.schema, options);
// Apply schema-level `writeConcern` option
applyWriteConcern(model.schema, options);
+ const asyncLocalStorage = this.model?.db?.base.transactionAsyncLocalStorage?.getStore();
+ if (!this.options.hasOwnProperty('session') && asyncLocalStorage?.session != null) {
+ options.session = asyncLocalStorage.session;
+ }
+
const readPreference = model &&
model.schema &&
model.schema.options &&
@@ -2019,7 +2095,7 @@ Query.prototype._optionsForExec = function(model) {
* Sets the lean option.
*
* Documents returned from queries with the `lean` option enabled are plain
- * javascript objects, not [Mongoose Documents](/docs/api/document.html). They have no
+ * javascript objects, not [Mongoose Documents](https://mongoosejs.com/docs/api/document.html). They have no
* `save` method, getters/setters, virtuals, or other Mongoose features.
*
* #### Example:
@@ -2031,9 +2107,9 @@ Query.prototype._optionsForExec = function(model) {
* const docs = await Model.find().lean();
* docs[0] instanceof mongoose.Document; // false
*
- * [Lean is great for high-performance, read-only cases](/docs/tutorials/lean.html),
+ * [Lean is great for high-performance, read-only cases](https://mongoosejs.com/docs/tutorials/lean.html),
* especially when combined
- * with [cursors](/docs/queries.html#streaming).
+ * with [cursors](https://mongoosejs.com/docs/queries.html#streaming).
*
* If you need virtuals, getters/setters, or defaults with `lean()`, you need
* to use a plugin. See:
@@ -2171,7 +2247,7 @@ Query.prototype.error = function error(err) {
*/
Query.prototype._unsetCastError = function _unsetCastError() {
- if (this._error != null && !(this._error instanceof CastError)) {
+ if (this._error == null || !(this._error instanceof CastError)) {
return;
}
return this.error(null);
@@ -2181,11 +2257,12 @@ Query.prototype._unsetCastError = function _unsetCastError() {
* Getter/setter around the current mongoose-specific options for this query
* Below are the current Mongoose-specific options.
*
- * - `populate`: an array representing what paths will be populated. Should have one entry for each call to [`Query.prototype.populate()`](#query_Query-populate)
- * - `lean`: if truthy, Mongoose will not [hydrate](/docs/api/model.html#model_Model-hydrate) any documents that are returned from this query. See [`Query.prototype.lean()`](#query_Query-lean) for more information.
- * - `strict`: controls how Mongoose handles keys that aren't in the schema for updates. This option is `true` by default, which means Mongoose will silently strip any paths in the update that aren't in the schema. See the [`strict` mode docs](/docs/guide.html#strict) for more information.
- * - `strictQuery`: controls how Mongoose handles keys that aren't in the schema for the query `filter`. This option is `false` by default for backwards compatibility, which means Mongoose will allow `Model.find({ foo: 'bar' })` even if `foo` is not in the schema. See the [`strictQuery` docs](/docs/guide.html#strictQuery) for more information.
- * - `nearSphere`: use `$nearSphere` instead of `near()`. See the [`Query.prototype.nearSphere()` docs](#query_Query-nearSphere)
+ * - `populate`: an array representing what paths will be populated. Should have one entry for each call to [`Query.prototype.populate()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.populate())
+ * - `lean`: if truthy, Mongoose will not [hydrate](https://mongoosejs.com/docs/api/model.html#Model.hydrate()) any documents that are returned from this query. See [`Query.prototype.lean()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.lean()) for more information.
+ * - `strict`: controls how Mongoose handles keys that aren't in the schema for updates. This option is `true` by default, which means Mongoose will silently strip any paths in the update that aren't in the schema. See the [`strict` mode docs](https://mongoosejs.com/docs/guide.html#strict) for more information.
+ * - `strictQuery`: controls how Mongoose handles keys that aren't in the schema for the query `filter`. This option is `false` by default, which means Mongoose will allow `Model.find({ foo: 'bar' })` even if `foo` is not in the schema. See the [`strictQuery` docs](https://mongoosejs.com/docs/guide.html#strictQuery) for more information.
+ * - `nearSphere`: use `$nearSphere` instead of `near()`. See the [`Query.prototype.nearSphere()` docs](https://mongoosejs.com/docs/api/query.html#Query.prototype.nearSphere())
+ * - `schemaLevelProjections`: if `false`, Mongoose will not apply schema-level `select: false` or `select: true` for this query
*
* Mongoose maintains a separate object for internal options because
* Mongoose sends `Query.prototype.options` to the MongoDB server, and the
@@ -2213,9 +2290,9 @@ Query.prototype.mongooseOptions = function(v) {
Query.prototype._castConditions = function() {
let sanitizeFilterOpt = undefined;
- if (this.model != null && utils.hasUserDefinedProperty(this.model.db.options, 'sanitizeFilter')) {
+ if (this.model?.db.options?.sanitizeFilter != null) {
sanitizeFilterOpt = this.model.db.options.sanitizeFilter;
- } else if (this.model != null && utils.hasUserDefinedProperty(this.model.base.options, 'sanitizeFilter')) {
+ } else if (this.model?.base.options?.sanitizeFilter != null) {
sanitizeFilterOpt = this.model.base.options.sanitizeFilter;
} else {
sanitizeFilterOpt = this._mongooseOptions.sanitizeFilter;
@@ -2246,28 +2323,25 @@ function _castArrayFilters(query) {
}
/**
- * Thunk around find()
+ * Execute a `find()`
*
- * @param {Function} [callback]
* @return {Query} this
* @api private
*/
-Query.prototype._find = wrapThunk(function(callback) {
+Query.prototype._find = async function _find() {
+ this._applyTranslateAliases();
this._castConditions();
if (this.error() != null) {
- callback(this.error());
- return null;
+ throw this.error();
}
- callback = _wrapThunkCallback(this, callback);
-
const mongooseOptions = this._mongooseOptions;
const _this = this;
const userProvidedFields = _this._userProvidedFields || {};
- applyGlobalMaxTimeMS(this.options, this.model);
- applyGlobalDiskUse(this.options, this.model);
+ applyGlobalMaxTimeMS(this.options, this.model.db.options, this.model.base.options);
+ applyGlobalDiskUse(this.options, this.model.db.options, this.model.base.options);
// Separate options to pass down to `completeMany()` in case we need to
// set a session on the document
@@ -2277,96 +2351,68 @@ Query.prototype._find = wrapThunk(function(callback) {
});
const options = this._optionsForExec();
+
const filter = this._conditions;
const fields = options.projection;
- const cb = (err, docs) => {
- if (err) {
- return callback(err);
- }
-
- if (docs.length === 0) {
- return callback(null, docs);
- }
- if (this.options.explain) {
- return callback(null, docs);
- }
- if (!mongooseOptions.populate) {
- const versionKey = _this.schema.options.versionKey;
- if (mongooseOptions.lean && mongooseOptions.lean.versionKey === false && versionKey) {
- docs.forEach((doc) => {
- if (versionKey in doc) {
- delete doc[versionKey];
- }
- });
- }
- return mongooseOptions.lean ?
- // call _completeManyLean here?
- _completeManyLean(_this.model.schema, docs, null, completeManyOptions, callback) :
- // callback(null, docs) :
- completeMany(_this.model, docs, fields, userProvidedFields, completeManyOptions, callback);
- }
+ const cursor = await this.mongooseCollection.find(filter, options);
+ if (options.explain) {
+ return cursor.explain();
+ }
- const pop = helpers.preparePopulationOptionsMQ(_this, mongooseOptions);
+ let docs = await cursor.toArray();
+ if (docs.length === 0) {
+ return docs;
+ }
- if (mongooseOptions.lean) {
- return _this.model.populate(docs, pop, callback);
+ if (!mongooseOptions.populate) {
+ const versionKey = _this.schema.options.versionKey;
+ if (mongooseOptions.lean && mongooseOptions.lean.versionKey === false && versionKey) {
+ docs.forEach((doc) => {
+ if (versionKey in doc) {
+ delete doc[versionKey];
+ }
+ });
}
+ return mongooseOptions.lean ?
+ _completeManyLean(_this.model.schema, docs, null, completeManyOptions) :
+ _this._completeMany(docs, fields, userProvidedFields, completeManyOptions);
+ }
- completeMany(_this.model, docs, fields, userProvidedFields, completeManyOptions, (err, docs) => {
- if (err != null) {
- return callback(err);
- }
- _this.model.populate(docs, pop, callback);
- });
- };
+ const pop = helpers.preparePopulationOptionsMQ(_this, mongooseOptions);
+ if (mongooseOptions.lean) {
+ return _this.model.populate(docs, pop);
+ }
- this._collection.collection.find(filter, options, (err, cursor) => {
- if (err != null) {
- return cb(err);
- }
+ docs = await _this._completeMany(docs, fields, userProvidedFields, completeManyOptions);
+ await this.model.populate(docs, pop);
- if (options.explain) {
- return cursor.explain(cb);
- }
- try {
- return cursor.toArray(cb);
- } catch (err) {
- return cb(err);
- }
- });
-});
+ return docs;
+};
/**
* Find all documents that match `selector`. The result will be an array of documents.
*
* If there are too many documents in the result to fit in memory, use
- * [`Query.prototype.cursor()`](#query_Query-cursor)
+ * [`Query.prototype.cursor()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.cursor())
*
* #### Example:
*
- * // Using async/await
* const arr = await Movie.find({ year: { $gte: 1980, $lte: 1989 } });
*
- * // Using callbacks
- * Movie.find({ year: { $gte: 1980, $lte: 1989 } }, function(err, arr) {});
- *
- * @param {Object|ObjectId} [filter] mongodb selector. If not specified, returns all documents.
- * @param {Function} [callback]
+ * @param {Object|ObjectId} [filter] mongodb filter. If not specified, returns all documents.
* @return {Query} this
* @api public
*/
-Query.prototype.find = function(conditions, callback) {
- this.op = 'find';
-
- if (typeof conditions === 'function') {
- callback = conditions;
- conditions = {};
+Query.prototype.find = function(conditions) {
+ if (typeof conditions === 'function' ||
+ typeof arguments[1] === 'function') {
+ throw new MongooseError('Query.prototype.find() no longer accepts a callback');
}
- conditions = utils.toObject(conditions);
+ this.op = 'find';
if (mquery.canMerge(conditions)) {
this.merge(conditions);
@@ -2376,13 +2422,6 @@ Query.prototype.find = function(conditions, callback) {
this.error(new ObjectParameterError(conditions, 'filter', 'find'));
}
- // if we don't have a callback, then just return the query object
- if (!callback) {
- return Query.base.find.call(this);
- }
-
- this.exec(callback);
-
return this;
};
@@ -2406,6 +2445,15 @@ Query.prototype.merge = function(source) {
// if source has a feature, apply it to ourselves
if (source._conditions) {
+ opts.omit = {};
+ if (this._conditions && this._conditions.$and && source._conditions.$and) {
+ opts.omit['$and'] = true;
+ this._conditions.$and = this._conditions.$and.concat(source._conditions.$and);
+ }
+ if (this._conditions && this._conditions.$or && source._conditions.$or) {
+ opts.omit['$or'] = true;
+ this._conditions.$or = this._conditions.$or.concat(source._conditions.$or);
+ }
utils.merge(this._conditions, source._conditions, opts);
}
@@ -2435,6 +2483,28 @@ Query.prototype.merge = function(source) {
utils.merge(this._conditions, { _id: source }, opts);
return this;
+ } else if (source && source.$__) {
+ source = source.toObject(internalToObjectOptions);
+ }
+
+ opts.omit = {};
+ if (Array.isArray(source.$and)) {
+ opts.omit['$and'] = true;
+ if (!this._conditions) {
+ this._conditions = {};
+ }
+ this._conditions.$and = (this._conditions.$and || []).concat(
+ source.$and.map(el => utils.isPOJO(el) ? utils.merge({}, el) : el)
+ );
+ }
+ if (Array.isArray(source.$or)) {
+ opts.omit['$or'] = true;
+ if (!this._conditions) {
+ this._conditions = {};
+ }
+ this._conditions.$or = (this._conditions.$or || []).concat(
+ source.$or.map(el => utils.isPOJO(el) ? utils.merge({}, el) : el)
+ );
}
// plain object
@@ -2466,17 +2536,16 @@ Query.prototype.collation = function(value) {
* @api private
*/
-Query.prototype._completeOne = function(doc, res, callback) {
- if (!doc && !this.options.rawResult) {
+Query.prototype._completeOne = function(doc, res, projection, callback) {
+ if (!doc && !this.options.includeResultMetadata) {
return callback(null, null);
}
const model = this.model;
- const projection = utils.clone(this._fields);
const userProvidedFields = this._userProvidedFields || {};
// `populate`, `lean`
const mongooseOptions = this._mongooseOptions;
- // `rawResult`
+
const options = this.options;
if (!options.lean && mongooseOptions.lean) {
options.lean = mongooseOptions.lean;
@@ -2501,58 +2570,91 @@ Query.prototype._completeOne = function(doc, res, callback) {
const pop = helpers.preparePopulationOptionsMQ(this, this._mongooseOptions);
if (mongooseOptions.lean) {
- return model.populate(doc, pop, (err, doc) => {
- if (err != null) {
- return callback(err);
+ return model.populate(doc, pop).then(
+ doc => {
+ _completeOneLean(model.schema, doc, null, res, options, callback);
+ },
+ error => {
+ callback(error);
}
- _completeOneLean(model.schema, doc, null, res, options, callback);
- });
+ );
}
completeOne(model, doc, res, options, projection, userProvidedFields, [], (err, doc) => {
if (err != null) {
return callback(err);
}
- model.populate(doc, pop, callback);
+ model.populate(doc, pop).then(res => { callback(null, res); }, err => { callback(err); });
});
};
/**
- * Thunk around findOne()
+ * Given a model and an array of docs, hydrates all the docs to be instances
+ * of the model. Used to initialize docs returned from the db from `find()`
+ *
+ * @param {Array} docs
+ * @param {Object} fields the projection used, including `select` from schemas
+ * @param {Object} userProvidedFields the user-specified projection
+ * @param {Object} [opts]
+ * @param {Array} [opts.populated]
+ * @param {ClientSession} [opts.session]
+ * @api private
+ */
+
+Query.prototype._completeMany = async function _completeMany(docs, fields, userProvidedFields, opts) {
+ const model = this.model;
+ return Promise.all(docs.map(doc => new Promise((resolve, reject) => {
+ const rawDoc = doc;
+ doc = helpers.createModel(model, doc, fields, userProvidedFields);
+ if (opts.session != null) {
+ doc.$session(opts.session);
+ }
+ doc.$init(rawDoc, opts, (err) => {
+ if (err != null) {
+ return reject(err);
+ }
+ resolve(doc);
+ });
+ })));
+};
+
+/**
+ * Internal helper to execute a findOne() operation
*
- * @param {Function} [callback]
* @see findOne https://www.mongodb.com/docs/manual/reference/method/db.collection.findOne/
* @api private
*/
-Query.prototype._findOne = wrapThunk(function(callback) {
+Query.prototype._findOne = async function _findOne() {
+ this._applyTranslateAliases();
this._castConditions();
if (this.error()) {
- callback(this.error());
- return null;
+ const err = this.error();
+ throw err;
}
- applyGlobalMaxTimeMS(this.options, this.model);
- applyGlobalDiskUse(this.options, this.model);
+ applyGlobalMaxTimeMS(this.options, this.model.db.options, this.model.base.options);
+ applyGlobalDiskUse(this.options, this.model.db.options, this.model.base.options);
- const conds = this._conditions;
const options = this._optionsForExec();
- this._collection.findOne(conds, options, (err, doc) => {
- if (err) {
- callback(err);
- return null;
- }
-
- this._completeOne(doc, null, _wrapThunkCallback(this, callback));
+ // don't pass in the conditions because we already merged them in
+ const doc = await this.mongooseCollection.findOne(this._conditions, options);
+ return new Promise((resolve, reject) => {
+ this._completeOne(doc, null, options.projection, (err, res) => {
+ if (err) {
+ return reject(err);
+ }
+ resolve(res);
+ });
});
-});
+};
/**
* Declares the query a findOne operation. When executed, the first found document is passed to the callback.
*
- * Passing a `callback` executes the query. The result of the query is a single document.
+ * The result of the query is a single document, or `null` if no document was found.
*
* * *Note:* `conditions` is optional, and if `conditions` is null or undefined,
* mongoose will send an empty `findOne` command to MongoDB, which will return
@@ -2565,44 +2667,29 @@ Query.prototype._findOne = wrapThunk(function(callback) {
*
* #### Example:
*
- * const query = Kitten.where({ color: 'white' });
- * query.findOne(function (err, kitten) {
- * if (err) return handleError(err);
- * if (kitten) {
- * // doc may be null if no document matched
- * }
- * });
+ * const query = Kitten.where({ color: 'white' });
+ * const kitten = await query.findOne();
*
* @param {Object} [filter] mongodb selector
* @param {Object} [projection] optional fields to return
- * @param {Object} [options] see [`setOptions()`](#query_Query-setOptions)
- * @param {Function} [callback] optional params are (error, document)
+ * @param {Object} [options] see [`setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions())
+ * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object.
* @return {Query} this
* @see findOne https://www.mongodb.com/docs/manual/reference/method/db.collection.findOne/
- * @see Query.select #query_Query-select
+ * @see Query.select https://mongoosejs.com/docs/api/query.html#Query.prototype.select()
* @api public
*/
-Query.prototype.findOne = function(conditions, projection, options, callback) {
- this.op = 'findOne';
- this._validateOp();
-
- if (typeof conditions === 'function') {
- callback = conditions;
- conditions = null;
- projection = null;
- options = null;
- } else if (typeof projection === 'function') {
- callback = projection;
- options = null;
- projection = null;
- } else if (typeof options === 'function') {
- callback = options;
- options = null;
+Query.prototype.findOne = function(conditions, projection, options) {
+ if (typeof conditions === 'function' ||
+ typeof projection === 'function' ||
+ typeof options === 'function' ||
+ typeof arguments[3] === 'function') {
+ throw new MongooseError('Query.prototype.findOne() no longer accepts a callback');
}
- // make sure we don't send in the whole Document to merge()
- conditions = utils.toObject(conditions);
+ this.op = 'findOne';
+ this._validateOp();
if (options) {
this.setOptions(options);
@@ -2620,24 +2707,20 @@ Query.prototype.findOne = function(conditions, projection, options, callback) {
this.error(new ObjectParameterError(conditions, 'filter', 'findOne'));
}
- if (!callback) {
- // already merged in the conditions, don't need to send them in.
- return Query.base.findOne.call(this);
- }
-
- this.exec(callback);
return this;
};
+
/**
- * Thunk around count()
+ * Execute a countDocuments query
*
- * @param {Function} [callback]
- * @see count https://www.mongodb.com/docs/manual/reference/method/db.collection.count/
+ * @see countDocuments https://mongodb.github.io/node-mongodb-native/4.9/classes/Collection.html#countDocuments
* @api private
*/
-Query.prototype._count = wrapThunk(function(callback) {
+Query.prototype._countDocuments = async function _countDocuments() {
+ this._applyTranslateAliases();
+
try {
this.cast(this.model);
} catch (err) {
@@ -2645,119 +2728,62 @@ Query.prototype._count = wrapThunk(function(callback) {
}
if (this.error()) {
- return callback(this.error());
+ throw this.error();
}
- applyGlobalMaxTimeMS(this.options, this.model);
- applyGlobalDiskUse(this.options, this.model);
+ applyGlobalMaxTimeMS(this.options, this.model.db.options, this.model.base.options);
+ applyGlobalDiskUse(this.options, this.model.db.options, this.model.base.options);
- const conds = this._conditions;
const options = this._optionsForExec();
- this._collection.count(conds, options, utils.tick(callback));
-});
+ const conds = this._conditions;
-/**
- * Thunk around countDocuments()
- *
- * @param {Function} [callback]
- * @see countDocuments https://mongodb.github.io/node-mongodb-native/4.9/classes/Collection.html#countDocuments
- * @api private
+ return this.mongooseCollection.countDocuments(conds, options);
+};
+
+/*!
+ * If `translateAliases` option is set, call `Model.translateAliases()`
+ * on the following query properties: filter, projection, update, distinct.
*/
-Query.prototype._countDocuments = wrapThunk(function(callback) {
- try {
- this.cast(this.model);
- } catch (err) {
- this.error(err);
+Query.prototype._applyTranslateAliases = function _applyTranslateAliases() {
+ let applyTranslateAliases = false;
+ if ('translateAliases' in this._mongooseOptions) {
+ applyTranslateAliases = this._mongooseOptions.translateAliases;
+ } else if (this.model?.schema?._userProvidedOptions?.translateAliases != null) {
+ applyTranslateAliases = this.model.schema._userProvidedOptions.translateAliases;
+ } else if (this.model?.base?.options?.translateAliases != null) {
+ applyTranslateAliases = this.model.base.options.translateAliases;
}
-
- if (this.error()) {
- return callback(this.error());
+ if (!applyTranslateAliases) {
+ return;
}
- applyGlobalMaxTimeMS(this.options, this.model);
- applyGlobalDiskUse(this.options, this.model);
-
- const conds = this._conditions;
- const options = this._optionsForExec();
-
- this._collection.collection.countDocuments(conds, options, utils.tick(callback));
-});
+ if (this.model?.schema?.aliases && Object.keys(this.model.schema.aliases).length > 0) {
+ this.model.translateAliases(this._conditions, true);
+ this.model.translateAliases(this._fields, true);
+ this.model.translateAliases(this._update, true);
+ if (this._distinct != null && this.model.schema.aliases[this._distinct] != null) {
+ this._distinct = this.model.schema.aliases[this._distinct];
+ }
+ }
+};
/**
- * Thunk around estimatedDocumentCount()
+ * Execute a estimatedDocumentCount() query
*
- * @param {Function} [callback]
* @see estimatedDocumentCount https://mongodb.github.io/node-mongodb-native/4.9/classes/Collection.html#estimatedDocumentCount
* @api private
*/
-Query.prototype._estimatedDocumentCount = wrapThunk(function(callback) {
+Query.prototype._estimatedDocumentCount = async function _estimatedDocumentCount() {
if (this.error()) {
- return callback(this.error());
+ throw this.error();
}
const options = this._optionsForExec();
- this._collection.collection.estimatedDocumentCount(options, utils.tick(callback));
-});
-
-/**
- * Specifies this query as a `count` query.
- *
- * This method is deprecated. If you want to count the number of documents in
- * a collection, e.g. `count({})`, use the [`estimatedDocumentCount()` function](#query_Query-estimatedDocumentCount)
- * instead. Otherwise, use the [`countDocuments()`](#query_Query-countDocuments) function instead.
- *
- * Passing a `callback` executes the query.
- *
- * This function triggers the following middleware.
- *
- * - `count()`
- *
- * #### Example:
- *
- * const countQuery = model.where({ 'color': 'black' }).count();
- *
- * query.count({ color: 'black' }).count(callback)
- *
- * query.count({ color: 'black' }, callback)
- *
- * query.where('color', 'black').count(function (err, count) {
- * if (err) return handleError(err);
- * console.log('there are %d kittens', count);
- * })
- *
- * @deprecated
- * @param {Object} [filter] count documents that match this object
- * @param {Function} [callback] optional params are (error, count)
- * @return {Query} this
- * @see count https://www.mongodb.com/docs/manual/reference/method/db.collection.count/
- * @api public
- */
-
-Query.prototype.count = function(filter, callback) {
- this.op = 'count';
- this._validateOp();
- if (typeof filter === 'function') {
- callback = filter;
- filter = undefined;
- }
-
- filter = utils.toObject(filter);
-
- if (mquery.canMerge(filter)) {
- this.merge(filter);
- }
-
- if (!callback) {
- return this;
- }
-
- this.exec(callback);
-
- return this;
+ return this.mongooseCollection.estimatedDocumentCount(options);
};
/**
@@ -2778,30 +2804,24 @@ Query.prototype.count = function(filter, callback) {
* await Model.find().estimatedDocumentCount();
*
* @param {Object} [options] passed transparently to the [MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/EstimatedDocumentCountOptions.html)
- * @param {Function} [callback] optional params are (error, count)
* @return {Query} this
* @see estimatedDocumentCount https://mongodb.github.io/node-mongodb-native/4.9/classes/Collection.html#estimatedDocumentCount
* @api public
*/
-Query.prototype.estimatedDocumentCount = function(options, callback) {
+Query.prototype.estimatedDocumentCount = function(options) {
+ if (typeof options === 'function' ||
+ typeof arguments[1] === 'function') {
+ throw new MongooseError('Query.prototype.estimatedDocumentCount() no longer accepts a callback');
+ }
+
this.op = 'estimatedDocumentCount';
this._validateOp();
- if (typeof options === 'function') {
- callback = options;
- options = undefined;
- }
- if (typeof options === 'object' && options != null) {
+ if (options != null) {
this.setOptions(options);
}
- if (!callback) {
- return this;
- }
-
- this.exec(callback);
-
return this;
};
@@ -2813,8 +2833,6 @@ Query.prototype.estimatedDocumentCount = function(options, callback) {
* [`$where` and a couple geospatial operators](https://mongodb.github.io/node-mongodb-native/4.9/classes/Collection.html#countDocuments).
* versus `count()`.
*
- * Passing a `callback` executes the query.
- *
* This function triggers the following middleware.
*
* - `countDocuments()`
@@ -2823,14 +2841,11 @@ Query.prototype.estimatedDocumentCount = function(options, callback) {
*
* const countQuery = model.where({ 'color': 'black' }).countDocuments();
*
- * query.countDocuments({ color: 'black' }).count(callback);
+ * query.countDocuments({ color: 'black' }).count().exec();
*
- * query.countDocuments({ color: 'black' }, callback);
+ * await query.countDocuments({ color: 'black' });
*
- * query.where('color', 'black').countDocuments(function(err, count) {
- * if (err) return handleError(err);
- * console.log('there are %d kittens', count);
- * });
+ * query.where('color', 'black').countDocuments().exec();
*
* The `countDocuments()` function is similar to `count()`, but there are a
* [few operators that `countDocuments()` does not support](https://mongodb.github.io/node-mongodb-native/4.9/classes/Collection.html#countDocuments).
@@ -2843,109 +2858,86 @@ Query.prototype.estimatedDocumentCount = function(options, callback) {
*
* @param {Object} [filter] mongodb selector
* @param {Object} [options]
- * @param {Function} [callback] optional params are (error, count)
* @return {Query} this
* @see countDocuments https://mongodb.github.io/node-mongodb-native/4.9/classes/Collection.html#countDocuments
* @api public
*/
-Query.prototype.countDocuments = function(conditions, options, callback) {
- this.op = 'countDocuments';
- this._validateOp();
- if (typeof conditions === 'function') {
- callback = conditions;
- conditions = undefined;
- options = undefined;
- }
- if (typeof options === 'function') {
- callback = options;
- options = undefined;
+Query.prototype.countDocuments = function(conditions, options) {
+ if (typeof conditions === 'function' ||
+ typeof options === 'function' ||
+ typeof arguments[2] === 'function') {
+ throw new MongooseError('Query.prototype.countDocuments() no longer accepts a callback');
}
- conditions = utils.toObject(conditions);
+ this.op = 'countDocuments';
+ this._validateOp();
if (mquery.canMerge(conditions)) {
this.merge(conditions);
}
- if (typeof options === 'object' && options != null) {
+ if (options != null) {
this.setOptions(options);
}
- if (!callback) {
- return this;
- }
-
- this.exec(callback);
-
return this;
};
/**
- * Thunk around distinct()
+ * Execute a `distinct()` query
*
- * @param {Function} [callback]
* @see distinct https://www.mongodb.com/docs/manual/reference/method/db.collection.distinct/
* @api private
*/
-Query.prototype.__distinct = wrapThunk(function __distinct(callback) {
+Query.prototype.__distinct = async function __distinct() {
+ this._applyTranslateAliases();
this._castConditions();
if (this.error()) {
- callback(this.error());
- return null;
+ throw this.error();
}
- applyGlobalMaxTimeMS(this.options, this.model);
- applyGlobalDiskUse(this.options, this.model);
+ applyGlobalMaxTimeMS(this.options, this.model.db.options, this.model.base.options);
+ applyGlobalDiskUse(this.options, this.model.db.options, this.model.base.options);
const options = this._optionsForExec();
- // don't pass in the conditions because we already merged them in
- this._collection.collection.
- distinct(this._distinct, this._conditions, options, callback);
-});
+ return this.mongooseCollection.
+ distinct(this._distinct, this._conditions, options);
+};
/**
* Declares or executes a distinct() operation.
*
- * Passing a `callback` executes the query.
- *
* This function does not trigger any middleware.
*
* #### Example:
*
- * distinct(field, conditions, callback)
+ * distinct(field, conditions, options)
* distinct(field, conditions)
- * distinct(field, callback)
* distinct(field)
- * distinct(callback)
* distinct()
*
* @param {String} [field]
* @param {Object|Query} [filter]
- * @param {Function} [callback] optional params are (error, arr)
+ * @param {Object} [options]
* @return {Query} this
* @see distinct https://www.mongodb.com/docs/manual/reference/method/db.collection.distinct/
* @api public
*/
-Query.prototype.distinct = function(field, conditions, callback) {
- this.op = 'distinct';
- this._validateOp();
- if (!callback) {
- if (typeof conditions === 'function') {
- callback = conditions;
- conditions = undefined;
- } else if (typeof field === 'function') {
- callback = field;
- field = undefined;
- conditions = undefined;
- }
+Query.prototype.distinct = function(field, conditions, options) {
+ if (typeof field === 'function' ||
+ typeof conditions === 'function' ||
+ typeof options === 'function' ||
+ typeof arguments[3] === 'function') {
+ throw new MongooseError('Query.prototype.distinct() no longer accepts a callback');
}
- conditions = utils.toObject(conditions);
+ this.op = 'distinct';
+ this._validateOp();
if (mquery.canMerge(conditions)) {
this.merge(conditions);
@@ -2959,8 +2951,8 @@ Query.prototype.distinct = function(field, conditions, callback) {
this._distinct = field;
}
- if (callback != null) {
- this.exec(callback);
+ if (options != null) {
+ this.setOptions(options);
}
return this;
@@ -2991,115 +2983,89 @@ Query.prototype.distinct = function(field, conditions, callback) {
* Cannot be used with `distinct()`
*
* @param {Object|String|Array>} arg
+ * @param {Object} [options]
+ * @param {Boolean} [options.override=false] If true, replace existing sort options with `arg`
* @return {Query} this
* @see cursor.sort https://www.mongodb.com/docs/manual/reference/method/cursor.sort/
* @api public
*/
-Query.prototype.sort = function(arg) {
- if (arguments.length > 1) {
- throw new Error('sort() only takes 1 Argument');
+Query.prototype.sort = function(arg, options) {
+ if (arguments.length > 2) {
+ throw new Error('sort() takes at most 2 arguments');
}
-
- return Query.base.sort.call(this, arg);
-};
-
-/**
- * Declare and/or execute this query as a remove() operation. `remove()` is
- * deprecated, you should use [`deleteOne()`](#query_Query-deleteOne)
- * or [`deleteMany()`](#query_Query-deleteMany) instead.
- *
- * This function does not trigger any middleware
- *
- * #### Example:
- *
- * Character.remove({ name: /Stark/ }, callback);
- *
- * This function calls the MongoDB driver's [`Collection#remove()` function](https://mongodb.github.io/node-mongodb-native/4.9/classes/Collection.html#remove).
- * The returned [promise](https://mongoosejs.com/docs/queries.html) resolves to an
- * object that contains 3 properties:
- *
- * - `ok`: `1` if no errors occurred
- * - `deletedCount`: the number of documents deleted
- * - `n`: the number of documents deleted. Equal to `deletedCount`.
- *
- * #### Example:
- *
- * const res = await Character.remove({ name: /Stark/ });
- * // Number of docs deleted
- * res.deletedCount;
- *
- * #### Note:
- *
- * Calling `remove()` creates a [Mongoose query](/docs/queries.html), and a query
- * does not execute until you either pass a callback, call [`Query#then()`](#query_Query-then),
- * or call [`Query#exec()`](#query_Query-exec).
- *
- * // not executed
- * const query = Character.remove({ name: /Stark/ });
- *
- * // executed
- * Character.remove({ name: /Stark/ }, callback);
- * Character.remove({ name: /Stark/ }).remove(callback);
- *
- * // executed without a callback
- * Character.exec();
- *
- * @param {Object|Query} [filter] mongodb selector
- * @param {Function} [callback] optional params are (error, mongooseDeleteResult)
- * @return {Query} this
- * @deprecated
- * @see DeleteResult https://mongodb.github.io/node-mongodb-native/4.9/interfaces/DeleteResult.html
- * @see remove https://mongodb.github.io/node-mongodb-native/4.9/classes/Collection.html#remove
- * @api public
- */
-
-Query.prototype.remove = function(filter, callback) {
- this.op = 'remove';
- if (typeof filter === 'function') {
- callback = filter;
- filter = null;
+ if (options != null && typeof options !== 'object') {
+ throw new Error('sort() options argument must be an object or nullish');
}
- filter = utils.toObject(filter);
-
- if (mquery.canMerge(filter)) {
- this.merge(filter);
-
- prepareDiscriminatorCriteria(this);
- } else if (filter != null) {
- this.error(new ObjectParameterError(filter, 'filter', 'remove'));
+ if (this.options.sort == null) {
+ this.options.sort = {};
}
-
- if (!callback) {
- return Query.base.remove.call(this);
+ if (options && options.override) {
+ this.options.sort = {};
+ }
+ const sort = this.options.sort;
+ if (typeof arg === 'string') {
+ const properties = arg.indexOf(' ') === -1 ? [arg] : arg.split(' ');
+ for (let property of properties) {
+ const ascend = '-' == property[0] ? -1 : 1;
+ if (ascend === -1) {
+ property = property.slice(1);
+ }
+ if (specialProperties.has(property)) {
+ continue;
+ }
+ sort[property] = ascend;
+ }
+ } else if (Array.isArray(arg)) {
+ for (const pair of arg) {
+ if (!Array.isArray(pair)) {
+ throw new TypeError('Invalid sort() argument, must be array of arrays');
+ }
+ const key = '' + pair[0];
+ if (specialProperties.has(key)) {
+ continue;
+ }
+ sort[key] = _handleSortValue(pair[1], key);
+ }
+ } else if (typeof arg === 'object' && arg != null && !(arg instanceof Map)) {
+ for (const key of Object.keys(arg)) {
+ if (specialProperties.has(key)) {
+ continue;
+ }
+ sort[key] = _handleSortValue(arg[key], key);
+ }
+ } else if (arg instanceof Map) {
+ for (let key of arg.keys()) {
+ key = '' + key;
+ if (specialProperties.has(key)) {
+ continue;
+ }
+ sort[key] = _handleSortValue(arg.get(key), key);
+ }
+ } else if (arg != null) {
+ throw new TypeError('Invalid sort() argument. Must be a string, object, array, or map.');
}
- this.exec(callback);
return this;
};
-/**
- * ignore
- * @param {Function} callback
- * @method _remove
- * @memberOf Query
- * @instance
- * @api private
+/*!
+ * Convert sort values
*/
-Query.prototype._remove = wrapThunk(function(callback) {
- this._castConditions();
-
- if (this.error() != null) {
- callback(this.error());
- return this;
+function _handleSortValue(val, key) {
+ if (val === 1 || val === 'asc' || val === 'ascending') {
+ return 1;
}
-
- callback = _wrapThunkCallback(this, callback);
-
- return Query.base.remove.call(this, callback);
-});
+ if (val === -1 || val === 'desc' || val === 'descending') {
+ return -1;
+ }
+ if (val?.$meta != null) {
+ return { $meta: val.$meta };
+ }
+ throw new TypeError('Invalid sort value: { ' + key + ': ' + val + ' }');
+}
/**
* Declare and/or execute this query as a `deleteOne()` operation. Works like
@@ -3112,9 +3078,6 @@ Query.prototype._remove = wrapThunk(function(callback) {
*
* await Character.deleteOne({ name: 'Eddard Stark' });
*
- * // Using callbacks:
- * Character.deleteOne({ name: 'Eddard Stark' }, callback);
- *
* This function calls the MongoDB driver's [`Collection#deleteOne()` function](https://mongodb.github.io/node-mongodb-native/4.9/classes/Collection.html#deleteOne).
* The returned [promise](https://mongoosejs.com/docs/queries.html) resolves to an
* object that contains 3 properties:
@@ -3130,28 +3093,19 @@ Query.prototype._remove = wrapThunk(function(callback) {
* res.deletedCount;
*
* @param {Object|Query} [filter] mongodb selector
- * @param {Object} [options] optional see [`Query.prototype.setOptions()`](#query_Query-setOptions)
- * @param {Function} [callback] optional params are (error, mongooseDeleteResult)
+ * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions())
* @return {Query} this
* @see DeleteResult https://mongodb.github.io/node-mongodb-native/4.9/interfaces/DeleteResult.html
* @see deleteOne https://mongodb.github.io/node-mongodb-native/4.9/classes/Collection.html#deleteOne
* @api public
*/
-Query.prototype.deleteOne = function(filter, options, callback) {
- this.op = 'deleteOne';
- if (typeof filter === 'function') {
- callback = filter;
- filter = null;
- options = null;
- } else if (typeof options === 'function') {
- callback = options;
- options = null;
- } else {
- this.setOptions(options);
+Query.prototype.deleteOne = function deleteOne(filter, options) {
+ if (typeof filter === 'function' || typeof options === 'function' || typeof arguments[2] === 'function') {
+ throw new MongooseError('Query.prototype.deleteOne() no longer accepts a callback');
}
-
- filter = utils.toObject(filter);
+ this.op = 'deleteOne';
+ this.setOptions(options);
if (mquery.canMerge(filter)) {
this.merge(filter);
@@ -3161,36 +3115,30 @@ Query.prototype.deleteOne = function(filter, options, callback) {
this.error(new ObjectParameterError(filter, 'filter', 'deleteOne'));
}
- if (!callback) {
- return Query.base.deleteOne.call(this);
- }
-
- this.exec.call(this, callback);
-
return this;
};
/**
* Internal thunk for `deleteOne()`
- * @param {Function} callback
+ *
* @method _deleteOne
* @instance
* @memberOf Query
* @api private
*/
-Query.prototype._deleteOne = wrapThunk(function(callback) {
+Query.prototype._deleteOne = async function _deleteOne() {
+ this._applyTranslateAliases();
this._castConditions();
if (this.error() != null) {
- callback(this.error());
- return this;
+ throw this.error();
}
- callback = _wrapThunkCallback(this, callback);
+ const options = this._optionsForExec();
- return Query.base.deleteOne.call(this, callback);
-});
+ return this.mongooseCollection.deleteOne(this._conditions, options);
+};
/**
* Declare and/or execute this query as a `deleteMany()` operation. Works like
@@ -3203,9 +3151,6 @@ Query.prototype._deleteOne = wrapThunk(function(callback) {
*
* await Character.deleteMany({ name: /Stark/, age: { $gte: 18 } });
*
- * // Using callbacks:
- * Character.deleteMany({ name: /Stark/, age: { $gte: 18 } }, callback);
- *
* This function calls the MongoDB driver's [`Collection#deleteMany()` function](https://mongodb.github.io/node-mongodb-native/4.9/classes/Collection.html#deleteMany).
* The returned [promise](https://mongoosejs.com/docs/queries.html) resolves to an
* object that contains 3 properties:
@@ -3221,28 +3166,19 @@ Query.prototype._deleteOne = wrapThunk(function(callback) {
* res.deletedCount;
*
* @param {Object|Query} [filter] mongodb selector
- * @param {Object} [options] optional see [`Query.prototype.setOptions()`](#query_Query-setOptions)
- * @param {Function} [callback] optional params are (error, mongooseDeleteResult)
+ * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions())
* @return {Query} this
* @see DeleteResult https://mongodb.github.io/node-mongodb-native/4.9/interfaces/DeleteResult.html
* @see deleteMany https://mongodb.github.io/node-mongodb-native/4.9/classes/Collection.html#deleteMany
* @api public
*/
-Query.prototype.deleteMany = function(filter, options, callback) {
- this.op = 'deleteMany';
- if (typeof filter === 'function') {
- callback = filter;
- filter = null;
- options = null;
- } else if (typeof options === 'function') {
- callback = options;
- options = null;
- } else {
- this.setOptions(options);
+Query.prototype.deleteMany = function(filter, options) {
+ if (typeof filter === 'function' || typeof options === 'function' || typeof arguments[2] === 'function') {
+ throw new MongooseError('Query.prototype.deleteMany() no longer accepts a callback');
}
-
- filter = utils.toObject(filter);
+ this.setOptions(options);
+ this.op = 'deleteMany';
if (mquery.canMerge(filter)) {
this.merge(filter);
@@ -3252,36 +3188,30 @@ Query.prototype.deleteMany = function(filter, options, callback) {
this.error(new ObjectParameterError(filter, 'filter', 'deleteMany'));
}
- if (!callback) {
- return Query.base.deleteMany.call(this);
- }
-
- this.exec.call(this, callback);
-
return this;
};
/**
- * Internal thunk around `deleteMany()`
- * @param {Function} callback
+ * Execute a `deleteMany()` query
+ *
* @method _deleteMany
* @instance
* @memberOf Query
* @api private
*/
-Query.prototype._deleteMany = wrapThunk(function(callback) {
+Query.prototype._deleteMany = async function _deleteMany() {
+ this._applyTranslateAliases();
this._castConditions();
if (this.error() != null) {
- callback(this.error());
- return this;
+ throw this.error();
}
- callback = _wrapThunkCallback(this, callback);
+ const options = this._optionsForExec();
- return Query.base.deleteMany.call(this, callback);
-});
+ return this.mongooseCollection.deleteMany(this._conditions, options);
+};
/**
* hydrates a document
@@ -3297,7 +3227,7 @@ Query.prototype._deleteMany = wrapThunk(function(callback) {
*/
function completeOne(model, doc, res, options, fields, userProvidedFields, pop, callback) {
- if (options.rawResult && doc == null) {
+ if (options.includeResultMetadata && doc == null) {
_init(null);
return null;
}
@@ -3306,11 +3236,11 @@ function completeOne(model, doc, res, options, fields, userProvidedFields, pop,
function _init(err, casted) {
if (err) {
- return immediate(() => callback(err));
+ return callback(err);
}
- if (options.rawResult) {
+ if (options.includeResultMetadata) {
if (doc && casted) {
if (options.session != null) {
casted.$session(options.session);
@@ -3319,12 +3249,12 @@ function completeOne(model, doc, res, options, fields, userProvidedFields, pop,
} else {
res.value = null;
}
- return immediate(() => callback(null, res));
+ return callback(null, res);
}
if (options.session != null) {
casted.$session(options.session);
}
- immediate(() => callback(null, casted));
+ callback(null, casted);
}
}
@@ -3347,11 +3277,10 @@ function prepareDiscriminatorCriteria(query) {
}
/**
- * Issues a mongodb [findAndModify](https://www.mongodb.com/docs/manual/reference/command/findAndModify/) update command.
+ * Issues a mongodb `findOneAndUpdate()` command.
*
* Finds a matching document, updates it according to the `update` arg, passing any `options`, and returns the found
- * document (if any) to the callback. The query executes if
- * `callback` is passed.
+ * document (if any).
*
* This function triggers the following middleware.
*
@@ -3364,26 +3293,14 @@ function prepareDiscriminatorCriteria(query) {
* - `fields`: {Object|String} - Field selection. Equivalent to `.select(fields).findOneAndUpdate()`
* - `sort`: if multiple docs are found by the conditions, sets the sort order to choose which doc to update
* - `maxTimeMS`: puts a time limit on the query - requires mongodb >= 2.6.0
- * - `runValidators`: if true, runs [update validators](/docs/validation.html#update-validators) on this command. Update validators validate the update operation against the model's schema.
+ * - `runValidators`: if true, runs [update validators](https://mongoosejs.com/docs/validation.html#update-validators) on this command. Update validators validate the update operation against the model's schema.
* - `setDefaultsOnInsert`: `true` by default. If `setDefaultsOnInsert` and `upsert` are true, mongoose will apply the [defaults](https://mongoosejs.com/docs/defaults.html) specified in the model's schema if a new document is created.
- * - `rawResult`: if true, returns the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/ModifyResult.html)
- *
- * #### Callback Signature
- *
- * function(error, doc) {
- * // error: any errors that occurred
- * // doc: the document before updates are applied if `new: false`, or after updates if `new = true`
- * }
*
* #### Example:
*
- * query.findOneAndUpdate(conditions, update, options, callback) // executes
* query.findOneAndUpdate(conditions, update, options) // returns Query
- * query.findOneAndUpdate(conditions, update, callback) // executes
* query.findOneAndUpdate(conditions, update) // returns Query
- * query.findOneAndUpdate(update, callback) // returns Query
* query.findOneAndUpdate(update) // returns Query
- * query.findOneAndUpdate(callback) // executes
* query.findOneAndUpdate() // returns Query
*
* @method findOneAndUpdate
@@ -3392,18 +3309,20 @@ function prepareDiscriminatorCriteria(query) {
* @param {Object|Query} [filter]
* @param {Object} [doc]
* @param {Object} [options]
- * @param {Boolean} [options.rawResult] if true, returns the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/ModifyResult.html)
+ * @param {Boolean} [options.includeResultMetadata] if true, returns the full [ModifyResult from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/ModifyResult.html) rather than just the document
* @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict)
- * @param {ClientSession} [options.session=null] The session associated with this query. See [transactions docs](/docs/transactions.html).
+ * @param {ClientSession} [options.session=null] The session associated with this query. See [transactions docs](https://mongoosejs.com/docs/transactions.html).
* @param {Boolean} [options.multipleCastError] by default, mongoose only returns the first error that occurred in casting the query. Turn on this option to aggregate all the cast errors.
* @param {Boolean} [options.new=false] By default, `findOneAndUpdate()` returns the document as it was **before** `update` was applied. If you set `new: true`, `findOneAndUpdate()` will instead give you the object after `update` was applied.
- * @param {Object} [options.lean] if truthy, mongoose will return the document as a plain JavaScript object rather than a mongoose document. See [`Query.lean()`](#query_Query-lean) and [the Mongoose lean tutorial](/docs/tutorials/lean.html).
- * @param {ClientSession} [options.session=null] The session associated with this query. See [transactions docs](/docs/transactions.html).
+ * @param {Object} [options.lean] if truthy, mongoose will return the document as a plain JavaScript object rather than a mongoose document. See [`Query.lean()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.lean()) and [the Mongoose lean tutorial](https://mongoosejs.com/docs/tutorials/lean.html).
+ * @param {ClientSession} [options.session=null] The session associated with this query. See [transactions docs](https://mongoosejs.com/docs/transactions.html).
* @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict)
- * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Note that this allows you to overwrite timestamps. Does nothing if schema-level timestamps are not set.
+ * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Note that this allows you to overwrite timestamps. Does nothing if schema-level timestamps are not set.
* @param {Boolean} [options.returnOriginal=null] An alias for the `new` option. `returnOriginal: false` is equivalent to `new: true`.
- * @param {Function} [callback] optional params are (error, doc), _unless_ `rawResult` is used, in which case params are (error, writeOpResult)
- * @see Tutorial /docs/tutorials/findoneandupdate.html
+ * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object.
+ * @param {Boolean} [options.overwriteDiscriminatorKey=false] Mongoose removes discriminator key updates from `update` by default, set `overwriteDiscriminatorKey` to `true` to allow updating the discriminator key
+ * @param {Boolean} [options.overwriteImmutable=false] Mongoose removes updated immutable properties from `update` by default (excluding $setOnInsert). Set `overwriteImmutable` to `true` to allow updating immutable properties using other update operators.
+ * @see Tutorial https://mongoosejs.com/docs/tutorials/findoneandupdate.html
* @see findAndModify command https://www.mongodb.com/docs/manual/reference/command/findAndModify/
* @see ModifyResult https://mongodb.github.io/node-mongodb-native/4.9/interfaces/ModifyResult.html
* @see findOneAndUpdate https://mongodb.github.io/node-mongodb-native/4.9/classes/Collection.html#findOneAndUpdate
@@ -3411,34 +3330,26 @@ function prepareDiscriminatorCriteria(query) {
* @api public
*/
-Query.prototype.findOneAndUpdate = function(filter, doc, options, callback) {
+Query.prototype.findOneAndUpdate = function(filter, doc, options) {
+ if (typeof filter === 'function' ||
+ typeof doc === 'function' ||
+ typeof options === 'function' ||
+ typeof arguments[3] === 'function') {
+ throw new MongooseError('Query.prototype.findOneAndUpdate() no longer accepts a callback');
+ }
+
this.op = 'findOneAndUpdate';
this._validateOp();
this._validate();
switch (arguments.length) {
- case 3:
- if (typeof options === 'function') {
- callback = options;
- options = {};
- }
- break;
case 2:
- if (typeof doc === 'function') {
- callback = doc;
- doc = filter;
- filter = undefined;
- }
options = undefined;
break;
case 1:
- if (typeof filter === 'function') {
- callback = filter;
- filter = options = doc = undefined;
- } else {
- doc = filter;
- filter = options = undefined;
- }
+ doc = filter;
+ filter = options = undefined;
+ break;
}
if (mquery.canMerge(filter)) {
@@ -3454,7 +3365,7 @@ Query.prototype.findOneAndUpdate = function(filter, doc, options, callback) {
this._mergeUpdate(doc);
}
- options = options ? utils.clone(options) : {};
+ options = options ? clone(options) : {};
if (options.projection) {
this.select(options.projection);
@@ -3476,251 +3387,184 @@ Query.prototype.findOneAndUpdate = function(filter, doc, options, callback) {
this.setOptions(options);
- if (!callback) {
- return this;
- }
-
- this.exec(callback);
-
return this;
};
/**
- * Thunk around findOneAndUpdate()
+ * Execute a findOneAndUpdate operation
*
- * @param {Function} [callback]
* @method _findOneAndUpdate
* @memberOf Query
* @api private
*/
-Query.prototype._findOneAndUpdate = wrapThunk(function(callback) {
- if (this.error() != null) {
- return callback(this.error());
- }
+Query.prototype._findOneAndUpdate = async function _findOneAndUpdate() {
+ this._applyTranslateAliases();
+ this._castConditions();
- this._findAndModify('update', callback);
-});
+ _castArrayFilters(this);
-/**
- * Issues a mongodb [findAndModify](https://www.mongodb.com/docs/manual/reference/command/findAndModify/) remove command.
- *
- * Finds a matching document, removes it, passing the found document (if any) to
- * the callback. Executes if `callback` is passed.
- *
- * This function triggers the following middleware.
- *
- * - `findOneAndRemove()`
- *
- * #### Available options
- *
- * - `sort`: if multiple docs are found by the conditions, sets the sort order to choose which doc to update
- * - `maxTimeMS`: puts a time limit on the query - requires mongodb >= 2.6.0
- * - `rawResult`: if true, resolves to the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/ModifyResult.html)
- *
- * #### Callback Signature
- *
- * function(error, doc) {
- * // error: any errors that occurred
- * // doc: the document before updates are applied if `new: false`, or after updates if `new = true`
- * }
- *
- * #### Example:
- *
- * A.where().findOneAndRemove(conditions, options, callback) // executes
- * A.where().findOneAndRemove(conditions, options) // return Query
- * A.where().findOneAndRemove(conditions, callback) // executes
- * A.where().findOneAndRemove(conditions) // returns Query
- * A.where().findOneAndRemove(callback) // executes
- * A.where().findOneAndRemove() // returns Query
- *
- * @method findOneAndRemove
- * @memberOf Query
- * @instance
- * @param {Object} [conditions]
- * @param {Object} [options]
- * @param {Boolean} [options.rawResult] if true, returns the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/ModifyResult.html)
- * @param {ClientSession} [options.session=null] The session associated with this query. See [transactions docs](/docs/transactions.html).
- * @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict)
- * @param {Function} [callback] optional params are (error, document)
- * @return {Query} this
- * @see findAndModify command https://www.mongodb.com/docs/manual/reference/command/findAndModify/
- * @api public
- */
+ if (this.error()) {
+ throw this.error();
+ }
-Query.prototype.findOneAndRemove = function(conditions, options, callback) {
- this.op = 'findOneAndRemove';
- this._validateOp();
- this._validate();
+ applyGlobalMaxTimeMS(this.options, this.model.db.options, this.model.base.options);
+ applyGlobalDiskUse(this.options, this.model.db.options, this.model.base.options);
- switch (arguments.length) {
- case 2:
- if (typeof options === 'function') {
- callback = options;
- options = {};
- }
- break;
- case 1:
- if (typeof conditions === 'function') {
- callback = conditions;
- conditions = undefined;
- options = undefined;
- }
- break;
+ if ('strict' in this.options) {
+ this._mongooseOptions.strict = this.options.strict;
}
+ const options = this._optionsForExec(this.model);
+ convertNewToReturnDocument(options);
- if (mquery.canMerge(conditions)) {
- this.merge(conditions);
+ this._update = this._castUpdate(this._update);
+
+ const _opts = Object.assign({}, options, {
+ setDefaultsOnInsert: this._mongooseOptions.setDefaultsOnInsert
+ });
+ this._update = setDefaultsOnInsert(this._conditions, this.model.schema,
+ this._update, _opts);
+
+ if (!this._update || Object.keys(this._update).length === 0) {
+ if (options.upsert) {
+ // still need to do the upsert to empty doc
+ const $set = clone(this._update);
+ delete $set._id;
+ this._update = { $set };
+ } else {
+ this._executionStack = null;
+ const res = await this._findOne();
+ return res;
+ }
+ } else if (this._update instanceof Error) {
+ throw this._update;
+ } else {
+ // In order to make MongoDB 2.6 happy (see
+ // https://jira.mongodb.org/browse/SERVER-12266 and related issues)
+ // if we have an actual update document but $set is empty, junk the $set.
+ if (this._update.$set && Object.keys(this._update.$set).length === 0) {
+ delete this._update.$set;
+ }
}
- options && this.setOptions(options);
+ const runValidators = _getOption(this, 'runValidators', false);
+ if (runValidators) {
+ await this.validate(this._update, options, false);
+ }
- if (!callback) {
- return this;
+ if (this._update.toBSON) {
+ this._update = this._update.toBSON();
}
- this.exec(callback);
+ let res = await this.mongooseCollection.findOneAndUpdate(this._conditions, this._update, options);
+ for (const fn of this._transforms) {
+ res = fn(res);
+ }
+ const doc = !options.includeResultMetadata ? res : res.value;
- return this;
+ return new Promise((resolve, reject) => {
+ this._completeOne(doc, res, options.projection, (err, res) => {
+ if (err) {
+ return reject(err);
+ }
+ resolve(res);
+ });
+ });
};
/**
* Issues a MongoDB [findOneAndDelete](https://www.mongodb.com/docs/manual/reference/method/db.collection.findOneAndDelete/) command.
*
- * Finds a matching document, removes it, and passes the found document (if any)
- * to the callback. Executes if `callback` is passed.
+ * Finds a matching document, removes it, and returns the found document (if any).
*
* This function triggers the following middleware.
*
* - `findOneAndDelete()`
*
- * This function differs slightly from `Model.findOneAndRemove()` in that
- * `findOneAndRemove()` becomes a [MongoDB `findAndModify()` command](https://www.mongodb.com/docs/manual/reference/method/db.collection.findAndModify/),
- * as opposed to a `findOneAndDelete()` command. For most mongoose use cases,
- * this distinction is purely pedantic. You should use `findOneAndDelete()`
- * unless you have a good reason not to.
- *
* #### Available options
*
* - `sort`: if multiple docs are found by the conditions, sets the sort order to choose which doc to update
* - `maxTimeMS`: puts a time limit on the query - requires mongodb >= 2.6.0
- * - `rawResult`: if true, resolves to the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/ModifyResult.html)
- *
- * #### Callback Signature
- *
- * function(error, doc) {
- * // error: any errors that occurred
- * // doc: the document before updates are applied if `new: false`, or after updates if `new = true`
- * }
*
* #### Example:
*
- * A.where().findOneAndDelete(conditions, options, callback) // executes
* A.where().findOneAndDelete(conditions, options) // return Query
- * A.where().findOneAndDelete(conditions, callback) // executes
* A.where().findOneAndDelete(conditions) // returns Query
- * A.where().findOneAndDelete(callback) // executes
* A.where().findOneAndDelete() // returns Query
*
* @method findOneAndDelete
* @memberOf Query
* @param {Object} [filter]
* @param {Object} [options]
- * @param {Boolean} [options.rawResult] if true, returns the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/ModifyResult.html)
- * @param {ClientSession} [options.session=null] The session associated with this query. See [transactions docs](/docs/transactions.html).
+ * @param {Boolean} [options.includeResultMetadata] if true, returns the full [ModifyResult from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/ModifyResult.html) rather than just the document
+ * @param {ClientSession} [options.session=null] The session associated with this query. See [transactions docs](https://mongoosejs.com/docs/transactions.html).
* @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict)
- * @param {Function} [callback] optional params are (error, document)
* @return {Query} this
* @see findAndModify command https://www.mongodb.com/docs/manual/reference/command/findAndModify/
* @api public
*/
-Query.prototype.findOneAndDelete = function(filter, options, callback) {
+Query.prototype.findOneAndDelete = function(filter, options) {
+ if (typeof filter === 'function' ||
+ typeof options === 'function' ||
+ typeof arguments[2] === 'function') {
+ throw new MongooseError('Query.prototype.findOneAndDelete() no longer accepts a callback');
+ }
+
this.op = 'findOneAndDelete';
this._validateOp();
this._validate();
- switch (arguments.length) {
- case 2:
- if (typeof options === 'function') {
- callback = options;
- options = {};
- }
- break;
- case 1:
- if (typeof filter === 'function') {
- callback = filter;
- filter = undefined;
- options = undefined;
- }
- break;
- }
-
if (mquery.canMerge(filter)) {
this.merge(filter);
- } else if (filter != null) {
- this.error(
- new ObjectParameterError(filter, 'filter', 'findOneAndDelete')
- );
}
options && this.setOptions(options);
- if (!callback) {
- return this;
- }
-
- this.exec(callback);
-
return this;
};
/**
- * Thunk around findOneAndDelete()
+ * Execute a `findOneAndDelete()` query
*
- * @param {Function} [callback]
* @return {Query} this
* @method _findOneAndDelete
* @memberOf Query
* @api private
*/
-Query.prototype._findOneAndDelete = wrapThunk(function(callback) {
+Query.prototype._findOneAndDelete = async function _findOneAndDelete() {
+ this._applyTranslateAliases();
this._castConditions();
if (this.error() != null) {
- callback(this.error());
- return null;
+ throw this.error();
}
+ const includeResultMetadata = this.options.includeResultMetadata;
+
const filter = this._conditions;
- const options = this._optionsForExec();
- let fields = null;
+ const options = this._optionsForExec(this.model);
- if (this._fields != null) {
- options.projection = this._castFields(utils.clone(this._fields));
- fields = options.projection;
- if (fields instanceof Error) {
- callback(fields);
- return null;
- }
+ let res = await this.mongooseCollection.findOneAndDelete(filter, options);
+ for (const fn of this._transforms) {
+ res = fn(res);
}
+ const doc = !includeResultMetadata ? res : res.value;
- this._collection.collection.findOneAndDelete(filter, options, _wrapThunkCallback(this, (err, res) => {
- if (err) {
- return callback(err);
- }
-
- const doc = res.value;
-
- return this._completeOne(doc, res, callback);
- }));
-});
+ return new Promise((resolve, reject) => {
+ this._completeOne(doc, res, options.projection, (err, res) => {
+ if (err) {
+ return reject(err);
+ }
+ resolve(res);
+ });
+ });
+};
/**
* Issues a MongoDB [findOneAndReplace](https://www.mongodb.com/docs/manual/reference/method/db.collection.findOneAndReplace/) command.
*
- * Finds a matching document, removes it, and passes the found document (if any)
- * to the callback. Executes if `callback` is passed.
+ * Finds a matching document, removes it, and returns the found document (if any).
*
* This function triggers the following middleware.
*
@@ -3730,22 +3574,12 @@ Query.prototype._findOneAndDelete = wrapThunk(function(callback) {
*
* - `sort`: if multiple docs are found by the conditions, sets the sort order to choose which doc to update
* - `maxTimeMS`: puts a time limit on the query - requires mongodb >= 2.6.0
- * - `rawResult`: if true, resolves to the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/ModifyResult.html)
- *
- * #### Callback Signature
- *
- * function(error, doc) {
- * // error: any errors that occurred
- * // doc: the document before updates are applied if `new: false`, or after updates if `new = true`
- * }
+ * - `includeResultMetadata`: if true, returns the full [ModifyResult from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/ModifyResult.html) rather than just the document
*
* #### Example:
*
- * A.where().findOneAndReplace(filter, replacement, options, callback); // executes
* A.where().findOneAndReplace(filter, replacement, options); // return Query
- * A.where().findOneAndReplace(filter, replacement, callback); // executes
* A.where().findOneAndReplace(filter); // returns Query
- * A.where().findOneAndReplace(callback); // executes
* A.where().findOneAndReplace(); // returns Query
*
* @method findOneAndReplace
@@ -3753,48 +3587,32 @@ Query.prototype._findOneAndDelete = wrapThunk(function(callback) {
* @param {Object} [filter]
* @param {Object} [replacement]
* @param {Object} [options]
- * @param {Boolean} [options.rawResult] if true, returns the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/ModifyResult.html)
- * @param {ClientSession} [options.session=null] The session associated with this query. See [transactions docs](/docs/transactions.html).
+ * @param {Boolean} [options.includeResultMetadata] if true, returns the full [ModifyResult from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/ModifyResult.html) rather than just the document
+ * @param {ClientSession} [options.session=null] The session associated with this query. See [transactions docs](https://mongoosejs.com/docs/transactions.html).
* @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict)
* @param {Boolean} [options.new=false] By default, `findOneAndUpdate()` returns the document as it was **before** `update` was applied. If you set `new: true`, `findOneAndUpdate()` will instead give you the object after `update` was applied.
- * @param {Object} [options.lean] if truthy, mongoose will return the document as a plain JavaScript object rather than a mongoose document. See [`Query.lean()`](#query_Query-lean) and [the Mongoose lean tutorial](/docs/tutorials/lean.html).
- * @param {ClientSession} [options.session=null] The session associated with this query. See [transactions docs](/docs/transactions.html).
+ * @param {Object} [options.lean] if truthy, mongoose will return the document as a plain JavaScript object rather than a mongoose document. See [`Query.lean()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.lean()) and [the Mongoose lean tutorial](https://mongoosejs.com/docs/tutorials/lean.html).
+ * @param {ClientSession} [options.session=null] The session associated with this query. See [transactions docs](https://mongoosejs.com/docs/transactions.html).
* @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict)
- * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Note that this allows you to overwrite timestamps. Does nothing if schema-level timestamps are not set.
+ * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Note that this allows you to overwrite timestamps. Does nothing if schema-level timestamps are not set.
* @param {Boolean} [options.returnOriginal=null] An alias for the `new` option. `returnOriginal: false` is equivalent to `new: true`.
- * @param {Function} [callback] optional params are (error, document)
+ * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object.
* @return {Query} this
* @api public
*/
-Query.prototype.findOneAndReplace = function(filter, replacement, options, callback) {
+Query.prototype.findOneAndReplace = function(filter, replacement, options) {
+ if (typeof filter === 'function' ||
+ typeof replacement === 'function' ||
+ typeof options === 'function' ||
+ typeof arguments[4] === 'function') {
+ throw new MongooseError('Query.prototype.findOneAndReplace() no longer accepts a callback');
+ }
+
this.op = 'findOneAndReplace';
this._validateOp();
this._validate();
- switch (arguments.length) {
- case 3:
- if (typeof options === 'function') {
- callback = options;
- options = void 0;
- }
- break;
- case 2:
- if (typeof replacement === 'function') {
- callback = replacement;
- replacement = void 0;
- }
- break;
- case 1:
- if (typeof filter === 'function') {
- callback = filter;
- filter = void 0;
- replacement = void 0;
- options = void 0;
- }
- break;
- }
-
if (mquery.canMerge(filter)) {
this.merge(filter);
} else if (filter != null) {
@@ -3804,9 +3622,6 @@ Query.prototype.findOneAndReplace = function(filter, replacement, options, callb
}
if (replacement != null) {
- if (hasDollarKeys(replacement)) {
- throw new Error('The replacement document must not contain atomic operators.');
- }
this._mergeUpdate(replacement);
}
@@ -3821,85 +3636,77 @@ Query.prototype.findOneAndReplace = function(filter, replacement, options, callb
options.returnOriginal = returnOriginal;
}
this.setOptions(options);
- this.setOptions({ overwrite: true });
-
- if (!callback) {
- return this;
- }
-
- this.exec(callback);
return this;
};
/**
- * Thunk around findOneAndReplace()
+ * Execute a findOneAndReplace() query
*
- * @param {Function} [callback]
* @return {Query} this
* @method _findOneAndReplace
* @instance
* @memberOf Query
* @api private
*/
-Query.prototype._findOneAndReplace = wrapThunk(function(callback) {
+Query.prototype._findOneAndReplace = async function _findOneAndReplace() {
+ this._applyTranslateAliases();
this._castConditions();
if (this.error() != null) {
- callback(this.error());
- return null;
+ throw this.error();
+ }
+
+ if ('strict' in this.options) {
+ this._mongooseOptions.strict = this.options.strict;
+ delete this.options.strict;
}
const filter = this._conditions;
const options = this._optionsForExec();
convertNewToReturnDocument(options);
- const runValidators = _getOption(this, 'runValidators', false);
- if (runValidators === false) {
- try {
- this._update = this._castUpdate(this._update, true);
- } catch (err) {
- const validationError = new ValidationError();
- validationError.errors[err.path] = err;
- callback(validationError);
- return null;
- }
-
- this._collection.collection.findOneAndReplace(filter, this._update || {}, options, _wrapThunkCallback(this, (err, res) => {
- if (err) {
- return callback(err);
- }
+ const includeResultMetadata = this.options.includeResultMetadata;
- const doc = res.value;
-
- return this._completeOne(doc, res, callback);
- }));
-
- return;
+ const modelOpts = { skipId: true };
+ if ('strict' in this._mongooseOptions) {
+ modelOpts.strict = this._mongooseOptions.strict;
}
+ const runValidators = _getOption(this, 'runValidators', false);
- let castedDoc = new this.model(this._update, null, true);
- this._update = castedDoc;
- castedDoc.validate(err => {
- if (err != null) {
- return callback(err);
+ try {
+ const update = new this.model(this._update, null, modelOpts);
+ if (runValidators) {
+ await update.validate();
+ } else if (update.$__.validationError) {
+ throw update.$__.validationError;
}
-
- if (castedDoc.toBSON) {
- castedDoc = castedDoc.toBSON();
+ this._update = update.toBSON();
+ } catch (err) {
+ if (err instanceof ValidationError) {
+ throw err;
}
+ const validationError = new ValidationError();
+ validationError.errors[err.path] = err;
+ throw validationError;
+ }
- this._collection.collection.findOneAndReplace(filter, castedDoc, options, _wrapThunkCallback(this, (err, res) => {
- if (err) {
- return callback(err);
- }
+ let res = await this.mongooseCollection.findOneAndReplace(filter, this._update, options);
- const doc = res.value;
+ for (const fn of this._transforms) {
+ res = fn(res);
+ }
- return this._completeOne(doc, res, callback);
- }));
+ const doc = !includeResultMetadata ? res : res.value;
+ return new Promise((resolve, reject) => {
+ this._completeOne(doc, res, options.projection, (err, res) => {
+ if (err) {
+ return reject(err);
+ }
+ resolve(res);
+ });
});
-});
+};
/**
* Support the `new` option as an alternative to `returnOriginal` for backwards
@@ -3922,25 +3729,6 @@ function convertNewToReturnDocument(options) {
}
}
-/**
- * Thunk around findOneAndRemove()
- *
- * @param {Function} [callback]
- * @return {Query} this
- * @method _findOneAndRemove
- * @memberOf Query
- * @instance
- * @api private
- */
-Query.prototype._findOneAndRemove = wrapThunk(function(callback) {
- if (this.error() != null) {
- callback(this.error());
- return;
- }
-
- this._findAndModify('remove', callback);
-});
-
/**
* Get options from query opts, falling back to the base mongoose object.
* @param {Query} query
@@ -3961,147 +3749,6 @@ function _getOption(query, option, def) {
return def;
}
-/**
- * Override mquery.prototype._findAndModify to provide casting etc.
- *
- * @param {String} type either "remove" or "update"
- * @param {Function} callback
- * @method _findAndModify
- * @memberOf Query
- * @instance
- * @api private
- */
-
-Query.prototype._findAndModify = function(type, callback) {
- if (typeof callback !== 'function') {
- throw new Error('Expected callback in _findAndModify');
- }
-
- const model = this.model;
- const schema = model.schema;
- const _this = this;
-
- const castedQuery = castQuery(this);
- if (castedQuery instanceof Error) {
- return callback(castedQuery);
- }
-
- _castArrayFilters(this);
-
- const opts = this._optionsForExec(model);
-
- if ('strict' in opts) {
- this._mongooseOptions.strict = opts.strict;
- }
-
- const isOverwriting = this.options.overwrite && !hasDollarKeys(this._update);
- if (isOverwriting) {
- this._update = new this.model(this._update, null, true);
- }
-
- if (type === 'remove') {
- opts.remove = true;
- } else {
- if (!('new' in opts) && !('returnOriginal' in opts) && !('returnDocument' in opts)) {
- opts.new = false;
- }
- if (!('upsert' in opts)) {
- opts.upsert = false;
- }
- if (opts.upsert || opts['new']) {
- opts.remove = false;
- }
-
- if (!isOverwriting) {
- try {
- this._update = this._castUpdate(this._update, opts.overwrite);
- } catch (err) {
- return callback(err);
- }
- const _opts = Object.assign({}, opts, {
- setDefaultsOnInsert: this._mongooseOptions.setDefaultsOnInsert
- });
- this._update = setDefaultsOnInsert(this._conditions, schema, this._update, _opts);
- if (!this._update || Object.keys(this._update).length === 0) {
- if (opts.upsert) {
- // still need to do the upsert to empty doc
- const doc = utils.clone(castedQuery);
- delete doc._id;
- this._update = { $set: doc };
- } else {
- this._executionStack = null;
- this.findOne(callback);
- return this;
- }
- } else if (this._update instanceof Error) {
- return callback(this._update);
- } else {
- // In order to make MongoDB 2.6 happy (see
- // https://jira.mongodb.org/browse/SERVER-12266 and related issues)
- // if we have an actual update document but $set is empty, junk the $set.
- if (this._update.$set && Object.keys(this._update.$set).length === 0) {
- delete this._update.$set;
- }
- }
- }
-
- if (Array.isArray(opts.arrayFilters)) {
- opts.arrayFilters = removeUnusedArrayFilters(this._update, opts.arrayFilters);
- }
- }
-
- if (opts.sort) convertSortToArray(opts);
-
- const cb = function(err, doc, res) {
- if (err) {
- return callback(err);
- }
-
- _this._completeOne(doc, res, callback);
- };
-
- const runValidators = _getOption(this, 'runValidators', false);
-
- // Bypass mquery
- const collection = _this._collection.collection;
- convertNewToReturnDocument(opts);
-
- if (type === 'remove') {
- collection.findOneAndDelete(castedQuery, opts, _wrapThunkCallback(_this, function(error, res) {
- return cb(error, res ? res.value : res, res);
- }));
-
- return this;
- }
-
- // honors legacy overwrite option for backward compatibility
- const updateMethod = isOverwriting ? 'findOneAndReplace' : 'findOneAndUpdate';
-
- if (runValidators) {
- this.validate(this._update, opts, isOverwriting, error => {
- if (error) {
- return callback(error);
- }
- if (this._update && this._update.toBSON) {
- this._update = this._update.toBSON();
- }
-
- collection[updateMethod](castedQuery, this._update, opts, _wrapThunkCallback(_this, function(error, res) {
- return cb(error, res ? res.value : res, res);
- }));
- });
- } else {
- if (this._update && this._update.toBSON) {
- this._update = this._update.toBSON();
- }
- collection[updateMethod](castedQuery, this._update, opts, _wrapThunkCallback(_this, function(error, res) {
- return cb(error, res ? res.value : res, res);
- }));
- }
-
- return this;
-};
-
/*!
* ignore
*/
@@ -4132,7 +3779,7 @@ function _completeOneLean(schema, doc, path, res, opts, callback) {
return;
}
}
- if (opts.rawResult) {
+ if (opts.includeResultMetadata) {
return callback(null, res);
}
return callback(null, doc);
@@ -4142,7 +3789,7 @@ function _completeOneLean(schema, doc, path, res, opts, callback) {
* ignore
*/
-function _completeManyLean(schema, docs, path, opts, callback) {
+function _completeManyLean(schema, docs, path, opts) {
if (opts.lean && typeof opts.lean.transform === 'function') {
for (const doc of docs) {
opts.lean.transform(doc);
@@ -4163,10 +3810,7 @@ function _completeManyLean(schema, docs, path, opts, callback) {
}
}
- if (!callback) {
- return;
- }
- return callback(null, docs);
+ return docs;
}
/**
* Override mquery.prototype._mergeUpdate to handle mongoose objects in
@@ -4180,13 +3824,14 @@ function _completeManyLean(schema, docs, path, opts, callback) {
*/
Query.prototype._mergeUpdate = function(doc) {
+ if (!this._update) {
+ this._update = Array.isArray(doc) ? [] : {};
+ }
+
if (doc == null || (typeof doc === 'object' && Object.keys(doc).length === 0)) {
return;
}
- if (!this._update) {
- this._update = Array.isArray(doc) ? [] : {};
- }
if (doc instanceof Query) {
if (Array.isArray(this._update)) {
throw new Error('Cannot mix array and object updates');
@@ -4207,72 +3852,33 @@ Query.prototype._mergeUpdate = function(doc) {
}
};
-/**
- * The mongodb driver 1.3.23 only supports the nested array sort
- * syntax. We must convert it or sorting findAndModify will not work.
- * @param {Object} opts
- * @param {Array|Object} opts.sort
- * @api private
- */
-
-function convertSortToArray(opts) {
- if (Array.isArray(opts.sort)) {
- return;
- }
- if (!utils.isObject(opts.sort)) {
- return;
- }
-
- const sort = [];
-
- for (const key in opts.sort) {
- if (utils.object.hasOwnProperty(opts.sort, key)) {
- sort.push([key, opts.sort[key]]);
- }
- }
-
- opts.sort = sort;
-}
-
/*!
* ignore
*/
-function _updateThunk(op, callback) {
+async function _updateThunk(op) {
+ this._applyTranslateAliases();
+
this._castConditions();
_castArrayFilters(this);
if (this.error() != null) {
- callback(this.error());
- return null;
+ throw this.error();
}
- callback = _wrapThunkCallback(this, callback);
-
const castedQuery = this._conditions;
const options = this._optionsForExec(this.model);
- this._update = utils.clone(this._update, options);
- const isOverwriting = this.options.overwrite && !hasDollarKeys(this._update);
+ this._update = clone(this._update, options);
+ const isOverwriting = op === 'replaceOne';
if (isOverwriting) {
- if (op === 'updateOne' || op === 'updateMany') {
- return callback(new MongooseError('The MongoDB server disallows ' +
- 'overwriting documents using `' + op + '`. See: ' +
- 'https://mongoosejs.com/docs/deprecations.html#update'));
- }
this._update = new this.model(this._update, null, true);
} else {
- try {
- this._update = this._castUpdate(this._update, options.overwrite);
- } catch (err) {
- callback(err);
- return null;
- }
+ this._update = this._castUpdate(this._update);
if (this._update == null || Object.keys(this._update).length === 0) {
- callback(null, { acknowledged: false });
- return null;
+ return { acknowledged: false };
}
const _opts = Object.assign({}, options, {
@@ -4286,27 +3892,16 @@ function _updateThunk(op, callback) {
options.arrayFilters = removeUnusedArrayFilters(this._update, options.arrayFilters);
}
- const runValidators = _getOption(this, 'runValidators', false);
- if (runValidators) {
- this.validate(this._update, options, isOverwriting, err => {
- if (err) {
- return callback(err);
- }
-
- if (this._update.toBSON) {
- this._update = this._update.toBSON();
- }
- this._collection[op](castedQuery, this._update, options, callback);
- });
- return null;
- }
-
+ const runValidators = _getOption(this, 'runValidators', false);
+ if (runValidators) {
+ await this.validate(this._update, options, isOverwriting);
+ }
+
if (this._update.toBSON) {
this._update = this._update.toBSON();
}
- this._collection[op](castedQuery, this._update, options, callback);
- return null;
+ return this.mongooseCollection[op](castedQuery, this._update, options);
}
/**
@@ -4316,209 +3911,77 @@ function _updateThunk(op, callback) {
* @param {Object} castedDoc the update, after casting
* @param {Object} options the options from `_optionsForExec()`
* @param {Boolean} isOverwriting
- * @param {Function} callback
* @method validate
* @memberOf Query
* @instance
* @api private
*/
-Query.prototype.validate = function validate(castedDoc, options, isOverwriting, callback) {
- return promiseOrCallback(callback, cb => {
- try {
- if (isOverwriting) {
- castedDoc.$validate(cb);
- } else {
- updateValidators(this, this.model.schema, castedDoc, options, cb);
- }
- } catch (err) {
- immediate(function() {
- cb(err);
+Query.prototype.validate = async function validate(castedDoc, options, isOverwriting) {
+ if (typeof arguments[3] === 'function') {
+ throw new MongooseError('Query.prototype.validate() no longer accepts a callback');
+ }
+
+ await _executePreHooks(this, 'validate');
+
+ if (isOverwriting) {
+ await castedDoc.$validate();
+ } else {
+ await new Promise((resolve, reject) => {
+ updateValidators(this, this.model.schema, castedDoc, options, (err) => {
+ if (err != null) {
+ return reject(err);
+ }
+ resolve();
});
- }
- });
-};
+ });
+ }
-/**
- * Internal thunk for .update()
- *
- * @param {Function} callback
- * @see Model.update #model_Model-update
- * @method _execUpdate
- * @memberOf Query
- * @instance
- * @api private
- */
-Query.prototype._execUpdate = wrapThunk(function(callback) {
- return _updateThunk.call(this, 'update', callback);
-});
+ await _executePostHooks(this, null, null, 'validate');
+};
/**
- * Internal thunk for .updateMany()
+ * Execute an updateMany query
*
- * @param {Function} callback
- * @see Model.update #model_Model-update
+ * @see Model.update https://mongoosejs.com/docs/api/model.html#Model.update()
* @method _updateMany
* @memberOf Query
* @instance
* @api private
*/
-Query.prototype._updateMany = wrapThunk(function(callback) {
- return _updateThunk.call(this, 'updateMany', callback);
-});
+Query.prototype._updateMany = async function _updateMany() {
+ return _updateThunk.call(this, 'updateMany');
+};
/**
- * Internal thunk for .updateOne()
+ * Execute an updateOne query
*
- * @param {Function} callback
- * @see Model.update #model_Model-update
+ * @see Model.update https://mongoosejs.com/docs/api/model.html#Model.update()
* @method _updateOne
* @memberOf Query
* @instance
* @api private
*/
-Query.prototype._updateOne = wrapThunk(function(callback) {
- return _updateThunk.call(this, 'updateOne', callback);
-});
+Query.prototype._updateOne = async function _updateOne() {
+ return _updateThunk.call(this, 'updateOne');
+};
/**
- * Internal thunk for .replaceOne()
+ * Execute a replaceOne query
*
- * @param {Function} callback
- * @see Model.replaceOne #model_Model-replaceOne
+ * @see Model.replaceOne https://mongoosejs.com/docs/api/model.html#Model.replaceOne()
* @method _replaceOne
* @memberOf Query
* @instance
* @api private
*/
-Query.prototype._replaceOne = wrapThunk(function(callback) {
- return _updateThunk.call(this, 'replaceOne', callback);
-});
-
-/**
- * Declare and/or execute this query as an update() operation.
- *
- * _All paths passed that are not [atomic](https://www.mongodb.com/docs/manual/tutorial/model-data-for-atomic-operations/#pattern) operations will become `$set` ops._
- *
- * This function triggers the following middleware.
- *
- * - `update()`
- *
- * #### Example:
- *
- * Model.where({ _id: id }).update({ title: 'words' });
- *
- * // becomes
- *
- * Model.where({ _id: id }).update({ $set: { title: 'words' }});
- *
- * #### Valid options:
- *
- * - `upsert` (boolean) whether to create the doc if it doesn't match (false)
- * - `multi` (boolean) whether multiple documents should be updated (false)
- * - `runValidators` (boolean) if true, runs [update validators](/docs/validation.html#update-validators) on this command. Update validators validate the update operation against the model's schema.
- * - `setDefaultsOnInsert` (boolean) `true` by default. If `setDefaultsOnInsert` and `upsert` are true, mongoose will apply the [defaults](https://mongoosejs.com/docs/defaults.html) specified in the model's schema if a new document is created.
- * - `strict` (boolean) overrides the `strict` option for this update
- * - `read`
- * - `writeConcern`
- *
- * #### Note:
- *
- * Passing an empty object `{}` as the doc will result in a no-op. The update operation will be ignored and the callback executed without sending the command to MongoDB.
- *
- * #### Note:
- *
- * The operation is only executed when a callback is passed. To force execution without a callback, we must first call update() and then execute it by using the `exec()` method.
- *
- * ```javascript
- * const q = Model.where({ _id: id });
- * q.update({ $set: { name: 'bob' }}).update(); // not executed
- *
- * q.update({ $set: { name: 'bob' }}).exec(); // executed
- *
- * // keys that are not [atomic](https://www.mongodb.com/docs/manual/tutorial/model-data-for-atomic-operations/#pattern) ops become `$set`.
- * // this executes the same command as the previous example.
- * q.update({ name: 'bob' }).exec();
- *
- * // multi updates
- * Model.where()
- * .update({ name: /^match/ }, { $set: { arr: [] }}, { multi: true }, callback)
- *
- * // more multi updates
- * Model.where()
- * .setOptions({ multi: true })
- * .update({ $set: { arr: [] }}, callback)
- *
- * // single update by default
- * Model.where({ email: 'address@example.com' })
- * .update({ $inc: { counter: 1 }}, callback)
- * ```
- *
- * API summary
- *
- * ```javascript
- * update(filter, doc, options, cb); // executes
- * update(filter, doc, options);
- * update(filter, doc, cb); // executes
- * update(filter, doc);
- * update(doc, cb); // executes
- * update(doc);
- * update(cb); // executes
- * update(true); // executes
- * update();
- * ```
- *
- * @param {Object} [filter]
- * @param {Object} [doc] the update command
- * @param {Object} [options]
- * @param {Boolean} [options.multipleCastError] by default, mongoose only returns the first error that occurred in casting the query. Turn on this option to aggregate all the cast errors.
- * @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict)
- * @param {Boolean} [options.upsert=false] if true, and no documents found, insert a new document
- * @param {Object} [options.writeConcern=null] sets the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/) for replica sets. Overrides the [schema-level write concern](/docs/guide.html#writeConcern)
- * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Does nothing if schema-level timestamps are not set.
- * @param {Function} [callback] params are (error, writeOpResult)
- * @return {Query} this
- * @see Model.update #model_Model-update
- * @see Query docs https://mongoosejs.com/docs/queries.html
- * @see update https://www.mongodb.com/docs/manual/reference/method/db.collection.update/
- * @see UpdateResult https://mongodb.github.io/node-mongodb-native/4.9/interfaces/UpdateResult.html
- * @see MongoDB docs https://www.mongodb.com/docs/manual/reference/command/update/#update-command-output
- * @api public
- */
-
-Query.prototype.update = function(conditions, doc, options, callback) {
- if (typeof options === 'function') {
- // .update(conditions, doc, callback)
- callback = options;
- options = null;
- } else if (typeof doc === 'function') {
- // .update(doc, callback);
- callback = doc;
- doc = conditions;
- conditions = {};
- options = null;
- } else if (typeof conditions === 'function') {
- // .update(callback)
- callback = conditions;
- conditions = undefined;
- doc = undefined;
- options = undefined;
- } else if (typeof conditions === 'object' && !doc && !options && !callback) {
- // .update(doc)
- doc = conditions;
- conditions = undefined;
- options = undefined;
- callback = undefined;
- }
-
- return _update(this, 'update', conditions, doc, options, callback);
+Query.prototype._replaceOne = async function _replaceOne() {
+ return _updateThunk.call(this, 'replaceOne');
};
/**
- * Declare and/or execute this query as an updateMany() operation. Same as
- * `update()`, except MongoDB will update _all_ documents that match
- * `filter` (as opposed to just the first one) regardless of the value of
- * the `multi` option.
+ * Declare and/or execute this query as an updateMany() operation.
+ * MongoDB will update _all_ documents that match `filter` (as opposed to just the first one).
*
* **Note** updateMany will _not_ fire update middleware. Use `pre('updateMany')`
* and `post('updateMany')` instead.
@@ -4534,16 +3997,18 @@ Query.prototype.update = function(conditions, doc, options, callback) {
* - `updateMany()`
*
* @param {Object} [filter]
- * @param {Object|Array} [update] the update command
+ * @param {Object|Array} [update] the update command. If array, this update will be treated as an update pipeline and not casted.
* @param {Object} [options]
* @param {Boolean} [options.multipleCastError] by default, mongoose only returns the first error that occurred in casting the query. Turn on this option to aggregate all the cast errors.
* @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict)
* @param {Boolean} [options.upsert=false] if true, and no documents found, insert a new document
- * @param {Object} [options.writeConcern=null] sets the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/) for replica sets. Overrides the [schema-level write concern](/docs/guide.html#writeConcern)
- * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Does nothing if schema-level timestamps are not set.
- * @param {Function} [callback] params are (error, writeOpResult)
+ * @param {Object} [options.writeConcern=null] sets the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/) for replica sets. Overrides the [schema-level write concern](https://mongoosejs.com/docs/guide.html#writeConcern)
+ * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Does nothing if schema-level timestamps are not set.
+ * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object.
+ * @param {Boolean} [options.overwriteDiscriminatorKey=false] Mongoose removes discriminator key updates from `update` by default, set `overwriteDiscriminatorKey` to `true` to allow updating the discriminator key
+ * @param {Boolean} [options.overwriteImmutable=false] Mongoose removes updated immutable properties from `update` by default (excluding $setOnInsert). Set `overwriteImmutable` to `true` to allow updating immutable properties using other update operators.
* @return {Query} this
- * @see Model.update #model_Model-update
+ * @see Model.update https://mongoosejs.com/docs/api/model.html#Model.update()
* @see Query docs https://mongoosejs.com/docs/queries.html
* @see update https://www.mongodb.com/docs/manual/reference/method/db.collection.update/
* @see UpdateResult https://mongodb.github.io/node-mongodb-native/4.9/interfaces/UpdateResult.html
@@ -4580,11 +4045,10 @@ Query.prototype.updateMany = function(conditions, doc, options, callback) {
};
/**
- * Declare and/or execute this query as an updateOne() operation. Same as
- * `update()`, except it does not support the `multi` option.
+ * Declare and/or execute this query as an updateOne() operation.
+ * MongoDB will update _only_ the first document that matches `filter`.
*
- * - MongoDB will update _only_ the first document that matches `filter` regardless of the value of the `multi` option.
- * - Use `replaceOne()` if you want to overwrite an entire document rather than using [atomic](https://www.mongodb.com/docs/manual/tutorial/model-data-for-atomic-operations/#pattern) operators like `$set`.
+ * - Use `replaceOne()` if you want to overwrite an entire document rather than using [atomic operators](https://www.mongodb.com/docs/manual/tutorial/model-data-for-atomic-operations/#pattern) like `$set`.
*
* **Note** updateOne will _not_ fire update middleware. Use `pre('updateOne')`
* and `post('updateOne')` instead.
@@ -4603,16 +4067,18 @@ Query.prototype.updateMany = function(conditions, doc, options, callback) {
* - `updateOne()`
*
* @param {Object} [filter]
- * @param {Object|Array} [update] the update command
+ * @param {Object|Array} [update] the update command. If array, this update will be treated as an update pipeline and not casted.
* @param {Object} [options]
* @param {Boolean} [options.multipleCastError] by default, mongoose only returns the first error that occurred in casting the query. Turn on this option to aggregate all the cast errors.
* @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict)
* @param {Boolean} [options.upsert=false] if true, and no documents found, insert a new document
- * @param {Object} [options.writeConcern=null] sets the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/) for replica sets. Overrides the [schema-level write concern](/docs/guide.html#writeConcern)
- * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Note that this allows you to overwrite timestamps. Does nothing if schema-level timestamps are not set.
- * @param {Function} [callback] params are (error, writeOpResult)
+ * @param {Object} [options.writeConcern=null] sets the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/) for replica sets. Overrides the [schema-level write concern](https://mongoosejs.com/docs/guide.html#writeConcern)
+ * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Note that this allows you to overwrite timestamps. Does nothing if schema-level timestamps are not set.
+ * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object.
+ * @param {Boolean} [options.overwriteDiscriminatorKey=false] Mongoose removes discriminator key updates from `update` by default, set `overwriteDiscriminatorKey` to `true` to allow updating the discriminator key
+ * @param {Boolean} [options.overwriteImmutable=false] Mongoose removes updated immutable properties from `update` by default (excluding $setOnInsert). Set `overwriteImmutable` to `true` to allow updating immutable properties using other update operators.
* @return {Query} this
- * @see Model.update #model_Model-update
+ * @see Model.update https://mongoosejs.com/docs/api/model.html#Model.update()
* @see Query docs https://mongoosejs.com/docs/queries.html
* @see update https://www.mongodb.com/docs/manual/reference/method/db.collection.update/
* @see UpdateResult https://mongodb.github.io/node-mongodb-native/4.9/interfaces/UpdateResult.html
@@ -4649,9 +4115,8 @@ Query.prototype.updateOne = function(conditions, doc, options, callback) {
};
/**
- * Declare and/or execute this query as a replaceOne() operation. Same as
- * `update()`, except MongoDB will replace the existing document and will
- * not accept any [atomic](https://www.mongodb.com/docs/manual/tutorial/model-data-for-atomic-operations/#pattern) operators (`$set`, etc.)
+ * Declare and/or execute this query as a replaceOne() operation.
+ * MongoDB will replace the existing document and will not accept any [atomic operators](https://www.mongodb.com/docs/manual/tutorial/model-data-for-atomic-operations/#pattern) (`$set`, etc.)
*
* **Note** replaceOne will _not_ fire update middleware. Use `pre('replaceOne')`
* and `post('replaceOne')` instead.
@@ -4675,11 +4140,11 @@ Query.prototype.updateOne = function(conditions, doc, options, callback) {
* @param {Boolean} [options.multipleCastError] by default, mongoose only returns the first error that occurred in casting the query. Turn on this option to aggregate all the cast errors.
* @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict)
* @param {Boolean} [options.upsert=false] if true, and no documents found, insert a new document
- * @param {Object} [options.writeConcern=null] sets the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/) for replica sets. Overrides the [schema-level write concern](/docs/guide.html#writeConcern)
- * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Does nothing if schema-level timestamps are not set.
- * @param {Function} [callback] params are (error, writeOpResult)
+ * @param {Object} [options.writeConcern=null] sets the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/) for replica sets. Overrides the [schema-level write concern](https://mongoosejs.com/docs/guide.html#writeConcern)
+ * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Does nothing if schema-level timestamps are not set.
+ * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object.
* @return {Query} this
- * @see Model.update #model_Model-update
+ * @see Model.update https://mongoosejs.com/docs/api/model.html#Model.update()
* @see Query docs https://mongoosejs.com/docs/queries.html
* @see update https://www.mongodb.com/docs/manual/reference/method/db.collection.update/
* @see UpdateResult https://mongodb.github.io/node-mongodb-native/4.9/interfaces/UpdateResult.html
@@ -4712,7 +4177,6 @@ Query.prototype.replaceOne = function(conditions, doc, options, callback) {
callback = undefined;
}
- this.setOptions({ overwrite: true });
return _update(this, 'replaceOne', conditions, doc, options, callback);
};
@@ -4731,7 +4195,6 @@ function _update(query, op, filter, doc, options, callback) {
// make sure we don't send in the whole Document to merge()
query.op = op;
query._validateOp();
- filter = utils.toObject(filter);
doc = doc || {};
// strict is an option used in the update checking, make sure it gets set
@@ -4762,7 +4225,7 @@ function _update(query, op, filter, doc, options, callback) {
return query;
}
- return Query.base[op].call(query, filter, void 0, options, callback);
+ return query;
}
/**
@@ -4837,28 +4300,24 @@ Query.prototype.orFail = function(err) {
}
break;
case 'replaceOne':
- case 'update':
case 'updateMany':
case 'updateOne':
- if (res && res.modifiedCount === 0) {
+ if (res && res.matchedCount === 0) {
throw _orFailError(err, this);
}
break;
case 'findOneAndDelete':
- case 'findOneAndRemove':
- if ((res && res.lastErrorObject && res.lastErrorObject.n) === 0) {
- throw _orFailError(err, this);
- }
- break;
case 'findOneAndUpdate':
case 'findOneAndReplace':
- if ((res && res.lastErrorObject && res.lastErrorObject.updatedExisting) === false) {
+ if (this.options.includeResultMetadata && res != null && res.value == null) {
+ throw _orFailError(err, this);
+ }
+ if (!this.options.includeResultMetadata && res == null) {
throw _orFailError(err, this);
}
break;
case 'deleteMany':
case 'deleteOne':
- case 'remove':
if (res.deletedCount === 0) {
throw _orFailError(err, this);
}
@@ -4891,6 +4350,17 @@ function _orFailError(err, query) {
return err;
}
+/**
+ * Wrapper function to call isPathSelectedInclusive on a query.
+ * @param {String} path
+ * @return {Boolean}
+ * @api public
+ */
+
+Query.prototype.isPathSelectedInclusive = function(path) {
+ return isPathSelectedInclusive(this._fields, path);
+};
+
/**
* Executes the query
*
@@ -4899,109 +4369,167 @@ function _orFailError(err, query) {
* const promise = query.exec();
* const promise = query.exec('update');
*
- * query.exec(callback);
- * query.exec('find', callback);
- *
* @param {String|Function} [operation]
- * @param {Function} [callback] optional params depend on the function being called
* @return {Promise}
* @api public
*/
-Query.prototype.exec = function exec(op, callback) {
- const _this = this;
+Query.prototype.exec = async function exec(op) {
+ if (typeof op === 'function' || (arguments.length >= 2 && typeof arguments[1] === 'function')) {
+ throw new MongooseError('Query.prototype.exec() no longer accepts a callback');
+ }
+
+ if (typeof op === 'string') {
+ this.op = op;
+ }
+
+ if (this.op == null) {
+ throw new MongooseError('Query must have `op` before executing');
+ }
+ if (this.model == null) {
+ throw new MongooseError('Query must have an associated model before executing');
+ }
+ this._validateOp();
+
+ if (!this.op) {
+ return;
+ }
+
if (this.options && this.options.sort) {
const keys = Object.keys(this.options.sort);
if (keys.includes('')) {
throw new Error('Invalid field "" passed to sort()');
}
}
- // Ensure that `exec()` is the first thing that shows up in
- // the stack when cast errors happen.
- const castError = new CastError();
- if (typeof op === 'function') {
- callback = op;
- op = null;
- } else if (typeof op === 'string') {
- this.op = op;
+ let thunk = '_' + this.op;
+ if (this.op === 'distinct') {
+ thunk = '__distinct';
}
- if (this.op == null) {
- throw new Error('Query must have `op` before executing');
+ if (this._executionStack != null) {
+ let str = this.toString();
+ if (str.length > 60) {
+ str = str.slice(0, 60) + '...';
+ }
+ const err = new MongooseError('Query was already executed: ' + str);
+ err.originalStack = this._executionStack;
+ throw err;
+ } else {
+ this._executionStack = new Error().stack;
+ }
+
+ let skipWrappedFunction = null;
+ try {
+ await _executePreExecHooks(this);
+ } catch (err) {
+ if (err instanceof Kareem.skipWrappedFunction) {
+ skipWrappedFunction = err;
+ } else {
+ throw err;
+ }
}
- this._validateOp();
- callback = this.model.$handleCallbackError(callback);
+ let res;
- return promiseOrCallback(callback, (cb) => {
- cb = this.model.$wrapCallback(cb);
+ let error = null;
+ try {
+ await _executePreHooks(this);
+ res = skipWrappedFunction ? skipWrappedFunction.args[0] : await this[thunk]();
- if (!_this.op) {
- cb();
- return;
+ for (const fn of this._transforms) {
+ res = fn(res);
+ }
+ } catch (err) {
+ if (err instanceof Kareem.skipWrappedFunction) {
+ res = err.args[0];
+ } else {
+ error = err;
}
- this._hooks.execPre('exec', this, [], (error) => {
- if (error != null) {
- return cb(_cleanCastErrorStack(castError, error));
- }
- let thunk = '_' + this.op;
- if (this.op === 'update') {
- thunk = '_execUpdate';
- } else if (this.op === 'distinct') {
- thunk = '__distinct';
- }
- this[thunk].call(this, (error, res) => {
- if (error) {
- return cb(_cleanCastErrorStack(castError, error));
- }
+ error = this.model.schema._transformDuplicateKeyError(error);
+ }
- this._hooks.execPost('exec', this, [], {}, (error) => {
- if (error) {
- return cb(_cleanCastErrorStack(castError, error));
- }
+ res = await _executePostHooks(this, res, error);
- cb(null, res);
- });
- });
- });
- }, this.model.events);
+ await _executePostExecHooks(this);
+
+ return res;
};
/*!
* ignore
*/
-function _cleanCastErrorStack(castError, error) {
- if (error instanceof CastError) {
- castError.copy(error);
- return castError;
- }
+function _executePostExecHooks(query) {
+ return new Promise((resolve, reject) => {
+ query._hooks.execPost('exec', query, [], {}, (error) => {
+ if (error) {
+ return reject(error);
+ }
- return error;
+ resolve();
+ });
+ });
}
/*!
* ignore
*/
-function _wrapThunkCallback(query, cb) {
- return function(error, res) {
+function _executePostHooks(query, res, error, op) {
+ if (query._queryMiddleware == null) {
if (error != null) {
- return cb(error);
+ throw error;
}
+ return res;
+ }
- for (const fn of query._transforms) {
- try {
- res = fn(res);
- } catch (error) {
- return cb(error);
+ return new Promise((resolve, reject) => {
+ const opts = error ? { error } : {};
+
+ query._queryMiddleware.execPost(op || query.op, query, [res], opts, (error, res) => {
+ if (error) {
+ return reject(error);
}
- }
- return cb(null, res);
- };
+ resolve(res);
+ });
+ });
+}
+
+/*!
+ * ignore
+ */
+
+function _executePreExecHooks(query) {
+ return new Promise((resolve, reject) => {
+ query._hooks.execPre('exec', query, [], (error) => {
+ if (error != null) {
+ return reject(error);
+ }
+ resolve();
+ });
+ });
+}
+
+/*!
+ * ignore
+ */
+
+function _executePreHooks(query, op) {
+ if (query._queryMiddleware == null) {
+ return;
+ }
+
+ return new Promise((resolve, reject) => {
+ query._queryMiddleware.execPre(op || query.op, query, [], (error) => {
+ if (error != null) {
+ return reject(error);
+ }
+ resolve();
+ });
+ });
}
/**
@@ -5037,7 +4565,41 @@ Query.prototype.catch = function(reject) {
};
/**
- * Add pre [middleware](/docs/middleware.html) to this query instance. Doesn't affect
+ * Executes the query returning a `Promise` which will be
+ * resolved with `.finally()` chained.
+ *
+ * More about [Promise `finally()` in JavaScript](https://thecodebarbarian.com/using-promise-finally-in-node-js.html).
+ *
+ * @param {Function} [onFinally]
+ * @return {Promise}
+ * @api public
+ */
+
+Query.prototype.finally = function(onFinally) {
+ return this.exec().finally(onFinally);
+};
+
+/**
+ * Returns a string representation of this query.
+ *
+ * More about [`toString()` in JavaScript](https://masteringjs.io/tutorials/fundamentals/tostring).
+ *
+ * #### Example:
+ * const q = Model.find();
+ * console.log(q); // Prints "Query { find }"
+ *
+ * @return {String}
+ * @api public
+ * @method [Symbol.toStringTag]
+ * @memberOf Query
+ */
+
+Query.prototype[Symbol.toStringTag] = function toString() {
+ return `Query { ${this.op} }`;
+};
+
+/**
+ * Add pre [middleware](https://mongoosejs.com/docs/middleware.html) to this query instance. Doesn't affect
* other queries.
*
* #### Example:
@@ -5063,7 +4625,7 @@ Query.prototype.pre = function(fn) {
};
/**
- * Add post [middleware](/docs/middleware.html) to this query instance. Doesn't affect
+ * Add post [middleware](https://mongoosejs.com/docs/middleware.html) to this query instance. Doesn't affect
* other queries.
*
* #### Example:
@@ -5092,7 +4654,6 @@ Query.prototype.post = function(fn) {
* Casts obj for an update command.
*
* @param {Object} obj
- * @param {Boolean} overwrite
* @return {Object} obj after casting its values
* @method _castUpdate
* @memberOf Query
@@ -5100,7 +4661,7 @@ Query.prototype.post = function(fn) {
* @api private
*/
-Query.prototype._castUpdate = function _castUpdate(obj, overwrite) {
+Query.prototype._castUpdate = function _castUpdate(obj) {
let schema = this.schema;
const discriminatorKey = schema.options.discriminatorKey;
@@ -5121,40 +4682,15 @@ Query.prototype._castUpdate = function _castUpdate(obj, overwrite) {
upsert = this.options.upsert;
}
- const filter = this._conditions;
- if (schema != null &&
- utils.hasUserDefinedProperty(filter, schema.options.discriminatorKey) &&
- typeof filter[schema.options.discriminatorKey] !== 'object' &&
- schema.discriminators != null) {
- const discriminatorValue = filter[schema.options.discriminatorKey];
- const byValue = getDiscriminatorByValue(this.model.discriminators, discriminatorValue);
- schema = schema.discriminators[discriminatorValue] ||
- (byValue && byValue.schema) ||
- schema;
- }
-
return castUpdate(schema, obj, {
- overwrite: overwrite,
strict: this._mongooseOptions.strict,
upsert: upsert,
arrayFilters: this.options.arrayFilters,
- overwriteDiscriminatorKey: this._mongooseOptions.overwriteDiscriminatorKey
+ overwriteDiscriminatorKey: this._mongooseOptions.overwriteDiscriminatorKey,
+ overwriteImmutable: this._mongooseOptions.overwriteImmutable
}, this, this._conditions);
};
-/**
- * castQuery
- * @api private
- */
-
-function castQuery(query) {
- try {
- return query.cast(query.model);
- } catch (err) {
- return err;
- }
-}
-
/**
* Specifies paths which should be populated with other documents.
*
@@ -5195,25 +4731,26 @@ function castQuery(query) {
* @param {Object} [options] Options for the population query (sort, etc)
* @param {String} [options.path=null] The path to populate.
* @param {boolean} [options.retainNullValues=false] by default, Mongoose removes null and undefined values from populated arrays. Use this option to make `populate()` retain `null` and `undefined` array entries.
- * @param {boolean} [options.getters=false] if true, Mongoose will call any getters defined on the `localField`. By default, Mongoose gets the raw value of `localField`. For example, you would need to set this option to `true` if you wanted to [add a `lowercase` getter to your `localField`](/docs/schematypes.html#schematype-options).
+ * @param {boolean} [options.getters=false] if true, Mongoose will call any getters defined on the `localField`. By default, Mongoose gets the raw value of `localField`. For example, you would need to set this option to `true` if you wanted to [add a `lowercase` getter to your `localField`](https://mongoosejs.com/docs/schematypes.html#schematype-options).
* @param {boolean} [options.clone=false] When you do `BlogPost.find().populate('author')`, blog posts with the same author will share 1 copy of an `author` doc. Enable this option to make Mongoose clone populated docs before assigning them.
* @param {Object|Function} [options.match=null] Add an additional filter to the populate query. Can be a filter object containing [MongoDB query syntax](https://www.mongodb.com/docs/manual/tutorial/query-documents/), or a function that returns a filter object.
* @param {Function} [options.transform=null] Function that Mongoose will call on every populated document that allows you to transform the populated document.
* @param {Object} [options.options=null] Additional options like `limit` and `lean`.
- * @see population /docs/populate.html
- * @see Query#select #query_Query-select
- * @see Model.populate #model_Model-populate
+ * @see population https://mongoosejs.com/docs/populate.html
+ * @see Query#select https://mongoosejs.com/docs/api/query.html#Query.prototype.select()
+ * @see Model.populate https://mongoosejs.com/docs/api/model.html#Model.populate()
* @return {Query} this
* @api public
*/
Query.prototype.populate = function() {
+ const args = Array.from(arguments);
// Bail when given no truthy arguments
- if (!Array.from(arguments).some(Boolean)) {
+ if (!args.some(Boolean)) {
return this;
}
- const res = utils.populate.apply(null, arguments);
+ const res = utils.populate.apply(null, args);
// Propagate readConcern and readPreference and lean from parent query,
// unless one already specified
@@ -5328,7 +4865,6 @@ function _getPopulatedPaths(list, arr, prefix) {
Query.prototype.cast = function(model, obj) {
obj || (obj = this._conditions);
-
model = model || this.model;
const discriminatorKey = model.schema.options.discriminatorKey;
if (obj != null &&
@@ -5340,12 +4876,14 @@ Query.prototype.cast = function(model, obj) {
if (this.options) {
if ('strict' in this.options) {
opts.strict = this.options.strict;
- opts.strictQuery = opts.strict;
}
if ('strictQuery' in this.options) {
opts.strictQuery = this.options.strictQuery;
}
}
+ if ('sanitizeFilter' in this._mongooseOptions) {
+ opts.sanitizeFilter = this._mongooseOptions.sanitizeFilter;
+ }
try {
return cast(model.schema, obj, opts, this);
@@ -5374,16 +4912,14 @@ Query.prototype._castFields = function _castFields(fields) {
elemMatchKeys,
keys,
key,
- out,
- i;
+ out;
if (fields) {
keys = Object.keys(fields);
elemMatchKeys = [];
- i = keys.length;
// collect $elemMatch args
- while (i--) {
+ for (let i = 0; i < keys.length; ++i) {
key = keys[i];
if (fields[key].$elemMatch) {
selected || (selected = {});
@@ -5402,8 +4938,7 @@ Query.prototype._castFields = function _castFields(fields) {
}
// apply the casted field args
- i = elemMatchKeys.length;
- while (i--) {
+ for (let i = 0; i < elemMatchKeys.length; ++i) {
key = elemMatchKeys[i];
fields[key] = out[key];
}
@@ -5422,7 +4957,21 @@ Query.prototype._applyPaths = function applyPaths() {
return;
}
this._fields = this._fields || {};
- helpers.applyPaths(this._fields, this.model.schema);
+
+ let sanitizeProjection = undefined;
+ if (this.model != null && utils.hasUserDefinedProperty(this.model.db.options, 'sanitizeProjection')) {
+ sanitizeProjection = this.model.db.options.sanitizeProjection;
+ } else if (this.model != null && utils.hasUserDefinedProperty(this.model.base.options, 'sanitizeProjection')) {
+ sanitizeProjection = this.model.base.options.sanitizeProjection;
+ } else {
+ sanitizeProjection = this._mongooseOptions.sanitizeProjection;
+ }
+
+ const schemaLevelProjections = this._mongooseOptions.schemaLevelProjections ?? true;
+
+ if (schemaLevelProjections) {
+ helpers.applyPaths(this._fields, this.model.schema, sanitizeProjection);
+ }
let _selectPopulatedPaths = true;
@@ -5474,7 +5023,7 @@ Query.prototype._applyPaths = function applyPaths() {
*
* @return {QueryCursor}
* @param {Object} [options]
- * @see QueryCursor /docs/api/querycursor.html
+ * @see QueryCursor https://mongoosejs.com/docs/api/querycursor.html
* @api public
*/
@@ -5483,30 +5032,17 @@ Query.prototype.cursor = function cursor(opts) {
this.setOptions(opts);
}
- const options = this._optionsForExec();
try {
this.cast(this.model);
} catch (err) {
- return (new QueryCursor(this, options))._markError(err);
+ return (new QueryCursor(this))._markError(err);
}
- return new QueryCursor(this, options);
+ return new QueryCursor(this);
};
// the rest of these are basically to support older Mongoose syntax with mquery
-/**
- * _DEPRECATED_ Alias of `maxScan`
- *
- * @deprecated
- * @see maxScan #query_Query-maxScan
- * @method maxscan
- * @memberOf Query
- * @instance
- */
-
-Query.prototype.maxscan = Query.base.maxScan;
-
/**
* Sets the tailable option (for use with capped collections).
*
@@ -5554,7 +5090,9 @@ Query.prototype.tailable = function(val, opts) {
}
}
- return Query.base.tailable.call(this, val);
+ this.options.tailable = arguments.length ? !!val : true;
+
+ return this;
};
/**
@@ -5734,7 +5272,7 @@ Query.prototype.near = function() {
* query.where('loc').near({ center: [10, 10], spherical: true });
*
* @deprecated
- * @see near() #query_Query-near
+ * @see near() https://mongoosejs.com/docs/api/query.html#Query.prototype.near()
* @see $near https://www.mongodb.com/docs/manual/reference/operator/near/
* @see $nearSphere https://www.mongodb.com/docs/manual/reference/operator/nearSphere/
* @see $maxDistance https://www.mongodb.com/docs/manual/reference/operator/maxDistance/
@@ -5789,7 +5327,7 @@ if (Symbol.asyncIterator != null) {
* @memberOf Query
* @instance
* @param {String|Array} [path]
- * @param {Array|Object} [coordinatePairs...]
+ * @param {...Array|Object} [coordinatePairs]
* @return {Query} this
* @see $polygon https://www.mongodb.com/docs/manual/reference/operator/polygon/
* @see MongoDB Geospatial Indexing https://www.mongodb.com/docs/manual/core/geospatial-indexes/
@@ -5811,7 +5349,7 @@ if (Symbol.asyncIterator != null) {
* @memberOf Query
* @instance
* @see $box https://www.mongodb.com/docs/manual/reference/operator/box/
- * @see within() Query#within #query_Query-within
+ * @see within() Query#within https://mongoosejs.com/docs/api/query.html#Query.prototype.within()
* @see MongoDB Geospatial Indexing https://www.mongodb.com/docs/manual/core/geospatial-indexes/
* @param {Object|Array} val1 Lower Left Coordinates OR a object of lower-left(ll) and upper-right(ur) Coordinates
* @param {Array} [val2] Upper Right Coordinates
@@ -5868,9 +5406,9 @@ Query.prototype.box = function(ll, ur) {
*/
/**
- * _DEPRECATED_ Alias for [circle](#query_Query-circle)
+ * _DEPRECATED_ Alias for [circle](https://mongoosejs.com/docs/api/query.html#Query.prototype.circle())
*
- * **Deprecated.** Use [circle](#query_Query-circle) instead.
+ * **Deprecated.** Use [circle](https://mongoosejs.com/docs/api/query.html#Query.prototype.circle()) instead.
*
* @deprecated
* @method center
@@ -5884,7 +5422,7 @@ Query.prototype.center = Query.base.circle;
/**
* _DEPRECATED_ Specifies a `$centerSphere` condition
*
- * **Deprecated.** Use [circle](#query_Query-circle) instead.
+ * **Deprecated.** Use [circle](https://mongoosejs.com/docs/api/query.html#Query.prototype.circle()) instead.
*
* #### Example:
*
diff --git a/lib/queryhelpers.js b/lib/queryHelpers.js
similarity index 75%
rename from lib/queryhelpers.js
rename to lib/queryHelpers.js
index 6948715a03c..0a6ae5ee3c0 100644
--- a/lib/queryhelpers.js
+++ b/lib/queryHelpers.js
@@ -4,6 +4,7 @@
* Module dependencies
*/
+const PopulateOptions = require('./options/populateOptions');
const checkEmbeddedDiscriminatorKeyProjection =
require('./helpers/discriminator/checkEmbeddedDiscriminatorKeyProjection');
const get = require('./helpers/get');
@@ -11,32 +12,7 @@ const getDiscriminatorByValue =
require('./helpers/discriminator/getDiscriminatorByValue');
const isDefiningProjection = require('./helpers/projection/isDefiningProjection');
const clone = require('./helpers/clone');
-
-/**
- * Prepare a set of path options for query population.
- *
- * @param {Query} query
- * @param {Object} options
- * @return {Array}
- */
-
-exports.preparePopulationOptions = function preparePopulationOptions(query, options) {
- const _populate = query.options.populate;
- const pop = Object.keys(_populate).reduce((vals, key) => vals.concat([_populate[key]]), []);
-
- // lean options should trickle through all queries
- if (options.lean != null) {
- pop
- .filter(p => (p && p.options && p.options.lean) == null)
- .forEach(makeLean(options.lean));
- }
-
- pop.forEach(opts => {
- opts._localModel = query.model;
- });
-
- return pop;
-};
+const isPathSelectedInclusive = require('./helpers/projection/isPathSelectedInclusive');
/**
* Prepare a set of path options for query population. This is the MongooseQuery
@@ -72,12 +48,18 @@ exports.preparePopulationOptionsMQ = function preparePopulationOptionsMQ(query,
}
const projection = query._fieldsForExec();
- pop.forEach(p => {
- p._queryProjection = projection;
- });
- pop.forEach(opts => {
- opts._localModel = query.model;
- });
+ for (let i = 0; i < pop.length; ++i) {
+ if (pop[i] instanceof PopulateOptions) {
+ pop[i] = new PopulateOptions({
+ ...pop[i],
+ _queryProjection: projection,
+ _localModel: query.model
+ });
+ } else {
+ pop[i]._queryProjection = projection;
+ pop[i]._localModel = query.model;
+ }
+ }
return pop;
};
@@ -144,17 +126,33 @@ exports.createModelAndInit = function createModelAndInit(model, doc, fields, use
* ignore
*/
-exports.applyPaths = function applyPaths(fields, schema) {
+exports.applyPaths = function applyPaths(fields, schema, sanitizeProjection) {
// determine if query is selecting or excluding fields
let exclude;
let keys;
- let keyIndex;
+ const minusPathsToSkip = new Set();
if (fields) {
keys = Object.keys(fields);
- keyIndex = keys.length;
- while (keyIndex--) {
+ // Collapse minus paths
+ const minusPaths = [];
+ for (let i = 0; i < keys.length; ++i) {
+ const key = keys[i];
+ if (keys[i][0] !== '-') {
+ continue;
+ }
+
+ delete fields[key];
+ if (key === '-_id') {
+ fields['_id'] = 0;
+ } else {
+ minusPaths.push(key.slice(1));
+ }
+ }
+
+ keys = Object.keys(fields);
+ for (let keyIndex = 0; keyIndex < keys.length; ++keyIndex) {
if (keys[keyIndex][0] === '+') {
continue;
}
@@ -163,25 +161,42 @@ exports.applyPaths = function applyPaths(fields, schema) {
if (!isDefiningProjection(field)) {
continue;
}
- // `_id: 1, name: 0` is a mixed inclusive/exclusive projection in
- // MongoDB 4.0 and earlier, but not in later versions.
if (keys[keyIndex] === '_id' && keys.length > 1) {
continue;
}
+ if (keys[keyIndex] === schema.options.discriminatorKey && keys.length > 1 && field != null && !field) {
+ continue;
+ }
exclude = !field;
break;
}
+
+ // Potentially add back minus paths based on schema-level path config
+ // and whether the projection is inclusive
+ for (const path of minusPaths) {
+ const type = schema.path(path);
+ // If the path isn't selected by default or the projection is not
+ // inclusive, minus path is treated as equivalent to `key: 0`.
+ // But we also allow using `-name` to remove `name` from an inclusive
+ // projection if `name` has schema-level `select: true`.
+ if ((!type || !type.selected) || exclude !== false) {
+ fields[path] = 0;
+ exclude = true;
+ } else if (type && type.selected && exclude === false) {
+ // Make a note of minus paths that are overwriting paths that are
+ // included by default.
+ minusPathsToSkip.add(path);
+ }
+ }
}
// if selecting, apply default schematype select:true fields
// if excluding, apply schematype select:false fields
-
const selected = [];
const excluded = [];
const stack = [];
analyzeSchema(schema);
-
switch (exclude) {
case true:
for (const fieldName of excluded) {
@@ -197,6 +212,12 @@ exports.applyPaths = function applyPaths(fields, schema) {
}
for (const fieldName of selected) {
+ if (minusPathsToSkip.has(fieldName)) {
+ continue;
+ }
+ if (isPathSelectedInclusive(fields, fieldName)) {
+ continue;
+ }
fields[fieldName] = fields[fieldName] || 1;
}
break;
@@ -270,33 +291,49 @@ exports.applyPaths = function applyPaths(fields, schema) {
}
function analyzePath(path, type) {
- const plusPath = '+' + path;
- const hasPlusPath = fields && plusPath in fields;
- if (hasPlusPath) {
- // forced inclusion
- delete fields[plusPath];
+ if (fields == null) {
+ return;
}
+ // If schema-level selected not set, nothing to do
if (typeof type.selected !== 'boolean') {
return;
}
+ // User overwriting default exclusion
+ if (type.selected === false && fields[path]) {
+ if (sanitizeProjection) {
+ fields[path] = 0;
+ }
+
+ return;
+ }
+
// If set to 0, we're explicitly excluding the discriminator key. Can't do this for all fields,
// because we have tests that assert that using `-path` to exclude schema-level `select: true`
// fields counts as an exclusive projection. See gh-11546
- if (exclude && type.selected && path === schema.options.discriminatorKey && fields[path] != null && !fields[path]) {
+ if (!exclude && type.selected && path === schema.options.discriminatorKey && fields[path] != null && !fields[path]) {
delete fields[path];
return;
}
+ if (exclude === false && type.selected && fields[path] != null && !fields[path]) {
+ delete fields[path];
+ return;
+ }
+
+ const plusPath = '+' + path;
+ const hasPlusPath = fields && plusPath in fields;
if (hasPlusPath) {
// forced inclusion
delete fields[plusPath];
// if there are other fields being included, add this one
// if no other included fields, leave this out (implied inclusion)
- if (exclude === false && keys.length > 1 && !~keys.indexOf(path)) {
+ if (exclude === false && keys.length > 1 && !~keys.indexOf(path) && !sanitizeProjection) {
fields[path] = 1;
+ } else if (exclude == null && sanitizeProjection && type.selected === false) {
+ fields[path] = 0;
}
return;
diff --git a/lib/schema.js b/lib/schema.js
index fd43b15010f..9f27e2ce236 100644
--- a/lib/schema.js
+++ b/lib/schema.js
@@ -7,27 +7,30 @@
const EventEmitter = require('events').EventEmitter;
const Kareem = require('kareem');
const MongooseError = require('./error/mongooseError');
-const SchemaType = require('./schematype');
-const SchemaTypeOptions = require('./options/SchemaTypeOptions');
-const VirtualOptions = require('./options/VirtualOptions');
-const VirtualType = require('./virtualtype');
+const SchemaType = require('./schemaType');
+const SchemaTypeOptions = require('./options/schemaTypeOptions');
+const VirtualOptions = require('./options/virtualOptions');
+const VirtualType = require('./virtualType');
const addAutoId = require('./helpers/schema/addAutoId');
+const clone = require('./helpers/clone');
const get = require('./helpers/get');
const getConstructorName = require('./helpers/getConstructorName');
const getIndexes = require('./helpers/schema/getIndexes');
+const handleReadPreferenceAliases = require('./helpers/query/handleReadPreferenceAliases');
const idGetter = require('./helpers/schema/idGetter');
+const isIndexSpecEqual = require('./helpers/indexes/isIndexSpecEqual');
const merge = require('./helpers/schema/merge');
const mpath = require('mpath');
-const readPref = require('./driver').get().ReadPreference;
+const setPopulatedVirtualValue = require('./helpers/populate/setPopulatedVirtualValue');
const setupTimestamps = require('./helpers/timestamps/setupTimestamps');
const utils = require('./utils');
const validateRef = require('./helpers/populate/validateRef');
-const util = require('util');
+
+const hasNumericSubpathRegex = /\.\d+(\.|$)/;
let MongooseTypes;
-const queryHooks = require('./helpers/query/applyQueryMiddleware').
- middlewareFunctions;
+const queryHooks = require('./constants').queryMiddlewareFunctions;
const documentHooks = require('./helpers/model/applyHooks').middlewareFunctions;
const hookNames = queryHooks.concat(documentHooks).
reduce((s, hook) => s.add(hook), new Set());
@@ -36,6 +39,8 @@ const isPOJO = utils.isPOJO;
let id = 0;
+const numberRE = /^\d+$/;
+
/**
* Schema constructor.
*
@@ -46,37 +51,41 @@ let id = 0;
* const Tree = mongoose.model('Tree', schema);
*
* // setting schema options
- * new Schema({ name: String }, { _id: false, autoIndex: false })
+ * new Schema({ name: String }, { id: false, autoIndex: false })
*
* #### Options:
*
- * - [autoIndex](/docs/guide.html#autoIndex): bool - defaults to null (which means use the connection's autoIndex option)
- * - [autoCreate](/docs/guide.html#autoCreate): bool - defaults to null (which means use the connection's autoCreate option)
- * - [bufferCommands](/docs/guide.html#bufferCommands): bool - defaults to true
- * - [bufferTimeoutMS](/docs/guide.html#bufferTimeoutMS): number - defaults to 10000 (10 seconds). If `bufferCommands` is enabled, the amount of time Mongoose will wait for connectivity to be restablished before erroring out.
- * - [capped](/docs/guide.html#capped): bool | number | object - defaults to false
- * - [collection](/docs/guide.html#collection): string - no default
- * - [discriminatorKey](/docs/guide.html#discriminatorKey): string - defaults to `__t`
- * - [id](/docs/guide.html#id): bool - defaults to true
- * - [_id](/docs/guide.html#_id): bool - defaults to true
- * - [minimize](/docs/guide.html#minimize): bool - controls [document#toObject](#document_Document-toObject) behavior when called manually - defaults to true
- * - [read](/docs/guide.html#read): string
- * - [writeConcern](/docs/guide.html#writeConcern): object - defaults to null, use to override [the MongoDB server's default write concern settings](https://www.mongodb.com/docs/manual/reference/write-concern/)
- * - [shardKey](/docs/guide.html#shardKey): object - defaults to `null`
- * - [strict](/docs/guide.html#strict): bool - defaults to true
- * - [strictQuery](/docs/guide.html#strictQuery): bool - defaults to false
- * - [toJSON](/docs/guide.html#toJSON) - object - no default
- * - [toObject](/docs/guide.html#toObject) - object - no default
- * - [typeKey](/docs/guide.html#typeKey) - string - defaults to 'type'
- * - [validateBeforeSave](/docs/guide.html#validateBeforeSave) - bool - defaults to `true`
- * - [versionKey](/docs/guide.html#versionKey): string or object - defaults to "__v"
- * - [optimisticConcurrency](/docs/guide.html#optimisticConcurrency): bool - defaults to false. Set to true to enable [optimistic concurrency](https://thecodebarbarian.com/whats-new-in-mongoose-5-10-optimistic-concurrency.html).
- * - [collation](/docs/guide.html#collation): object - defaults to null (which means use no collation)
- * - [timeseries](/docs/guide.html#timeseries): object - defaults to null (which means this schema's collection won't be a timeseries collection)
- * - [selectPopulatedPaths](/docs/guide.html#selectPopulatedPaths): boolean - defaults to `true`
- * - [skipVersioning](/docs/guide.html#skipVersioning): object - paths to exclude from versioning
- * - [timestamps](/docs/guide.html#timestamps): object or boolean - defaults to `false`. If true, Mongoose adds `createdAt` and `updatedAt` properties to your schema and manages those properties for you.
- * - [pluginTags](/docs/guide.html#pluginTags): array of strings - defaults to `undefined`. If set and plugin called with `tags` option, will only apply that plugin to schemas with a matching tag.
+ * - [autoIndex](https://mongoosejs.com/docs/guide.html#autoIndex): bool - defaults to null (which means use the connection's autoIndex option)
+ * - [autoCreate](https://mongoosejs.com/docs/guide.html#autoCreate): bool - defaults to null (which means use the connection's autoCreate option)
+ * - [bufferCommands](https://mongoosejs.com/docs/guide.html#bufferCommands): bool - defaults to true
+ * - [bufferTimeoutMS](https://mongoosejs.com/docs/guide.html#bufferTimeoutMS): number - defaults to 10000 (10 seconds). If `bufferCommands` is enabled, the amount of time Mongoose will wait for connectivity to be restablished before erroring out.
+ * - [capped](https://mongoosejs.com/docs/guide.html#capped): bool | number | object - defaults to false
+ * - [collection](https://mongoosejs.com/docs/guide.html#collection): string - no default
+ * - [discriminatorKey](https://mongoosejs.com/docs/guide.html#discriminatorKey): string - defaults to `__t`
+ * - [id](https://mongoosejs.com/docs/guide.html#id): bool - defaults to true
+ * - [_id](https://mongoosejs.com/docs/guide.html#_id): bool - defaults to true
+ * - [minimize](https://mongoosejs.com/docs/guide.html#minimize): bool - controls [document#toObject](https://mongoosejs.com/docs/api/document.html#Document.prototype.toObject()) behavior when called manually - defaults to true
+ * - [read](https://mongoosejs.com/docs/guide.html#read): string
+ * - [readConcern](https://mongoosejs.com/docs/guide.html#readConcern): object - defaults to null, use to set a default [read concern](https://www.mongodb.com/docs/manual/reference/read-concern/) for all queries.
+ * - [writeConcern](https://mongoosejs.com/docs/guide.html#writeConcern): object - defaults to null, use to override [the MongoDB server's default write concern settings](https://www.mongodb.com/docs/manual/reference/write-concern/)
+ * - [shardKey](https://mongoosejs.com/docs/guide.html#shardKey): object - defaults to `null`
+ * - [strict](https://mongoosejs.com/docs/guide.html#strict): bool - defaults to true
+ * - [strictQuery](https://mongoosejs.com/docs/guide.html#strictQuery): bool - defaults to false
+ * - [toJSON](https://mongoosejs.com/docs/guide.html#toJSON) - object - no default
+ * - [toObject](https://mongoosejs.com/docs/guide.html#toObject) - object - no default
+ * - [typeKey](https://mongoosejs.com/docs/guide.html#typeKey) - string - defaults to 'type'
+ * - [validateBeforeSave](https://mongoosejs.com/docs/guide.html#validateBeforeSave) - bool - defaults to `true`
+ * - [validateModifiedOnly](https://mongoosejs.com/docs/api/document.html#Document.prototype.validate()) - bool - defaults to `false`
+ * - [versionKey](https://mongoosejs.com/docs/guide.html#versionKey): string or object - defaults to "__v"
+ * - [optimisticConcurrency](https://mongoosejs.com/docs/guide.html#optimisticConcurrency): bool - defaults to false. Set to true to enable [optimistic concurrency](https://thecodebarbarian.com/whats-new-in-mongoose-5-10-optimistic-concurrency.html).
+ * - [collation](https://mongoosejs.com/docs/guide.html#collation): object - defaults to null (which means use no collation)
+ * - [timeseries](https://mongoosejs.com/docs/guide.html#timeseries): object - defaults to null (which means this schema's collection won't be a timeseries collection)
+ * - [selectPopulatedPaths](https://mongoosejs.com/docs/guide.html#selectPopulatedPaths): boolean - defaults to `true`
+ * - [skipVersioning](https://mongoosejs.com/docs/guide.html#skipVersioning): object - paths to exclude from versioning
+ * - [timestamps](https://mongoosejs.com/docs/guide.html#timestamps): object or boolean - defaults to `false`. If true, Mongoose adds `createdAt` and `updatedAt` properties to your schema and manages those properties for you.
+ * - [pluginTags](https://mongoosejs.com/docs/guide.html#pluginTags): array of strings - defaults to `undefined`. If set and plugin called with `tags` option, will only apply that plugin to schemas with a matching tag.
+ * - [virtuals](https://mongoosejs.com/docs/tutorials/virtuals.html#virtuals-via-schema-options): object - virtuals to define, alias for [`.virtual`](https://mongoosejs.com/docs/api/schema.html#Schema.prototype.virtual())
+ * - [collectionOptions]: object with options passed to [`createCollection()`](https://www.mongodb.com/docs/manual/reference/method/db.createCollection/) when calling `Model.createCollection()` or `autoCreate` set to true.
*
* #### Options for Nested Schemas:
*
@@ -108,6 +117,7 @@ function Schema(obj, options) {
this.inherits = {};
this.callQueue = [];
this._indexes = [];
+ this._searchIndexes = [];
this.methods = (options && options.methods) || {};
this.methodOptions = {};
this.statics = (options && options.statics) || {};
@@ -395,24 +405,45 @@ Schema.prototype._clone = function _clone(Constructor) {
const s = new Constructor({}, this._userProvidedOptions);
s.base = this.base;
s.obj = this.obj;
- s.options = utils.clone(this.options);
+ s.options = clone(this.options);
s.callQueue = this.callQueue.map(function(f) { return f; });
- s.methods = utils.clone(this.methods);
- s.methodOptions = utils.clone(this.methodOptions);
- s.statics = utils.clone(this.statics);
- s.query = utils.clone(this.query);
+ s.methods = clone(this.methods);
+ s.methodOptions = clone(this.methodOptions);
+ s.statics = clone(this.statics);
+ s.query = clone(this.query);
s.plugins = Array.prototype.slice.call(this.plugins);
- s._indexes = utils.clone(this._indexes);
+ s._indexes = clone(this._indexes);
+ s._searchIndexes = clone(this._searchIndexes);
s.s.hooks = this.s.hooks.clone();
- s.tree = utils.clone(this.tree);
- s.paths = utils.clone(this.paths);
- s.nested = utils.clone(this.nested);
- s.subpaths = utils.clone(this.subpaths);
- s.singleNestedPaths = utils.clone(this.singleNestedPaths);
- s.childSchemas = gatherChildSchemas(s);
+ s.tree = clone(this.tree);
+ s.paths = Object.fromEntries(
+ Object.entries(this.paths).map(([key, value]) => ([key, value.clone()]))
+ );
+ s.nested = clone(this.nested);
+ s.subpaths = clone(this.subpaths);
+ for (const schemaType of Object.values(s.paths)) {
+ if (schemaType.$isSingleNested) {
+ const path = schemaType.path;
+ for (const key of Object.keys(schemaType.schema.paths)) {
+ s.singleNestedPaths[path + '.' + key] = schemaType.schema.paths[key];
+ }
+ for (const key of Object.keys(schemaType.schema.singleNestedPaths)) {
+ s.singleNestedPaths[path + '.' + key] =
+ schemaType.schema.singleNestedPaths[key];
+ }
+ for (const key of Object.keys(schemaType.schema.subpaths)) {
+ s.singleNestedPaths[path + '.' + key] =
+ schemaType.schema.subpaths[key];
+ }
+ for (const key of Object.keys(schemaType.schema.nested)) {
+ s.singleNestedPaths[path + '.' + key] = 'nested';
+ }
+ }
+ }
+ s._gatherChildSchemas();
- s.virtuals = utils.clone(this.virtuals);
+ s.virtuals = clone(this.virtuals);
s.$globalPluginsApplied = this.$globalPluginsApplied;
s.$isRootDiscriminator = this.$isRootDiscriminator;
s.$implicitlyCreated = this.$implicitlyCreated;
@@ -427,7 +458,7 @@ Schema.prototype._clone = function _clone(Constructor) {
s.discriminators = Object.assign({}, this.discriminators);
}
if (this._applyDiscriminators != null) {
- s._applyDiscriminators = Object.assign({}, this._applyDiscriminators);
+ s._applyDiscriminators = new Map(this._applyDiscriminators);
}
s.aliases = Object.assign({}, this.aliases);
@@ -529,16 +560,14 @@ Schema.prototype.omit = function(paths, options) {
*/
Schema.prototype.defaultOptions = function(options) {
- this._userProvidedOptions = options == null ? {} : utils.clone(options);
+ this._userProvidedOptions = options == null ? {} : clone(options);
const baseOptions = this.base && this.base.options || {};
const strict = 'strict' in baseOptions ? baseOptions.strict : true;
+ const strictQuery = 'strictQuery' in baseOptions ? baseOptions.strictQuery : false;
const id = 'id' in baseOptions ? baseOptions.id : true;
- options = utils.options({
- strict: strict,
- strictQuery: 'strict' in this._userProvidedOptions ?
- this._userProvidedOptions.strict :
- 'strictQuery' in baseOptions ?
- baseOptions.strictQuery : strict,
+ options = {
+ strict,
+ strictQuery,
bufferCommands: true,
capped: false, // { size, max, autoIndexId }
versionKey: '__v',
@@ -549,20 +578,27 @@ Schema.prototype.defaultOptions = function(options) {
shardKey: null,
read: null,
validateBeforeSave: true,
+ validateModifiedOnly: false,
// the following are only applied at construction time
_id: true,
id: id,
- typeKey: 'type'
- }, utils.clone(options));
-
- if (options.read) {
- options.read = readPref(options.read);
- }
+ typeKey: 'type',
+ ...options
+ };
if (options.versionKey && typeof options.versionKey !== 'string') {
throw new MongooseError('`versionKey` must be falsy or string, got `' + (typeof options.versionKey) + '`');
}
+ if (typeof options.read === 'string') {
+ options.read = handleReadPreferenceAliases(options.read);
+ } else if (Array.isArray(options.read) && typeof options.read[0] === 'string') {
+ options.read = {
+ mode: handleReadPreferenceAliases(options.read[0]),
+ tags: options.read[1]
+ };
+ }
+
if (options.optimisticConcurrency && !options.versionKey) {
throw new MongooseError('Must set `versionKey` if using `optimisticConcurrency`');
}
@@ -591,15 +627,46 @@ Schema.prototype.defaultOptions = function(options) {
*
* @param {String} name the name of the discriminator
* @param {Schema} schema the discriminated Schema
+ * @param {Object} [options] discriminator options
+ * @param {String} [options.value] the string stored in the `discriminatorKey` property. If not specified, Mongoose uses the `name` parameter.
+ * @param {Boolean} [options.clone=true] By default, `discriminator()` clones the given `schema`. Set to `false` to skip cloning.
+ * @param {Boolean} [options.overwriteModels=false] by default, Mongoose does not allow you to define a discriminator with the same name as another discriminator. Set this to allow overwriting discriminators with the same name.
+ * @param {Boolean} [options.mergeHooks=true] By default, Mongoose merges the base schema's hooks with the discriminator schema's hooks. Set this option to `false` to make Mongoose use the discriminator schema's hooks instead.
+ * @param {Boolean} [options.mergePlugins=true] By default, Mongoose merges the base schema's plugins with the discriminator schema's plugins. Set this option to `false` to make Mongoose use the discriminator schema's plugins instead.
* @return {Schema} the Schema instance
* @api public
*/
-Schema.prototype.discriminator = function(name, schema) {
- this._applyDiscriminators = Object.assign(this._applyDiscriminators || {}, { [name]: schema });
+Schema.prototype.discriminator = function(name, schema, options) {
+ this._applyDiscriminators = this._applyDiscriminators || new Map();
+ this._applyDiscriminators.set(name, { schema, options });
return this;
};
+/*!
+ * Get this schema's default toObject/toJSON options, including Mongoose global
+ * options.
+ */
+
+Schema.prototype._defaultToObjectOptions = function(json) {
+ const path = json ? 'toJSON' : 'toObject';
+ if (this._defaultToObjectOptionsMap && this._defaultToObjectOptionsMap[path]) {
+ return this._defaultToObjectOptionsMap[path];
+ }
+
+ const baseOptions = this.base &&
+ this.base.options &&
+ this.base.options[path] || {};
+ const schemaOptions = this.options[path] || {};
+ // merge base default options with Schema's set default options if available.
+ // `clone` is necessary here because `utils.options` directly modifies the second input.
+ const defaultOptions = Object.assign({}, baseOptions, schemaOptions);
+
+ this._defaultToObjectOptionsMap = this._defaultToObjectOptionsMap || {};
+ this._defaultToObjectOptionsMap[path] = defaultOptions;
+ return defaultOptions;
+};
+
/**
* Adds key path / schema type pairs to this schema.
*
@@ -657,6 +724,23 @@ Schema.prototype.add = function add(obj, prefix) {
if (key === '_id' && val === false) {
continue;
}
+ // Deprecate setting schema paths to primitive types (gh-7558)
+ let isMongooseTypeString = false;
+ if (typeof val === 'string') {
+ // Handle the case in which the type is specified as a string (eg. 'date', 'oid', ...)
+ const MongooseTypes = this.base != null ? this.base.Schema.Types : Schema.Types;
+ const upperVal = val.charAt(0).toUpperCase() + val.substring(1);
+ isMongooseTypeString = MongooseTypes[upperVal] != null;
+ }
+ if (
+ key !== '_id' &&
+ ((typeof val !== 'object' && typeof val !== 'function' && !isMongooseTypeString) ||
+ val == null)
+ ) {
+ throw new TypeError(`Invalid schema configuration: \`${val}\` is not ` +
+ `a valid type at path \`${key}\`. See ` +
+ 'https://bit.ly/mongoose-schematypes for a list of valid schema types.');
+ }
if (val instanceof VirtualType || (val.constructor && val.constructor.name || null) === 'VirtualType') {
this.virtual(val);
continue;
@@ -679,19 +763,6 @@ Schema.prototype.add = function add(obj, prefix) {
for (const key in val[0].discriminators) {
schemaType.discriminator(key, val[0].discriminators[key]);
}
- } else if (val[0] != null && val[0].instanceOfSchema && utils.isPOJO(val[0]._applyDiscriminators)) {
- const applyDiscriminators = val[0]._applyDiscriminators || [];
- const schemaType = this.path(prefix + key);
- for (const disc in applyDiscriminators) {
- schemaType.discriminator(disc, applyDiscriminators[disc]);
- }
- }
- else if (val != null && val.instanceOfSchema && utils.isPOJO(val._applyDiscriminators)) {
- const applyDiscriminators = val._applyDiscriminators || [];
- const schemaType = this.path(prefix + key);
- for (const disc in applyDiscriminators) {
- schemaType.discriminator(disc, applyDiscriminators[disc]);
- }
}
} else if (Object.keys(val).length < 1) {
// Special-case: {} always interpreted as Mixed path so leaf at this node
@@ -713,8 +784,25 @@ Schema.prototype.add = function add(obj, prefix) {
if (prefix) {
this.nested[prefix.substring(0, prefix.length - 1)] = true;
}
- const _schema = new Schema(_typeDef);
- const schemaWrappedPath = Object.assign({}, val, { type: _schema });
+
+ const childSchemaOptions = {};
+ if (this._userProvidedOptions.typeKey) {
+ childSchemaOptions.typeKey = this._userProvidedOptions.typeKey;
+ }
+ // propagate 'strict' option to child schema
+ if (this._userProvidedOptions.strict != null) {
+ childSchemaOptions.strict = this._userProvidedOptions.strict;
+ }
+ if (this._userProvidedOptions.toObject != null) {
+ childSchemaOptions.toObject = utils.omit(this._userProvidedOptions.toObject, ['transform']);
+ }
+ if (this._userProvidedOptions.toJSON != null) {
+ childSchemaOptions.toJSON = utils.omit(this._userProvidedOptions.toJSON, ['transform']);
+ }
+
+ const _schema = new Schema(_typeDef, childSchemaOptions);
+ _schema.$implicitlyCreated = true;
+ const schemaWrappedPath = Object.assign({}, val, { [typeKey]: _schema });
this.path(prefix + key, schemaWrappedPath);
} else {
// Either the type is non-POJO or we interpret it as Mixed anyway
@@ -811,7 +899,7 @@ Schema.prototype.removeIndex = function removeIndex(index) {
if (typeof index === 'object') {
for (let i = this._indexes.length - 1; i >= 0; --i) {
- if (util.isDeepStrictEqual(this._indexes[i][0], index)) {
+ if (isIndexSpecEqual(this._indexes[i][0], index)) {
this._indexes.splice(i, 1);
}
}
@@ -853,6 +941,28 @@ Schema.prototype.clearIndexes = function clearIndexes() {
return this;
};
+/**
+ * Add an [Atlas search index](https://www.mongodb.com/docs/atlas/atlas-search/create-index/) that Mongoose will create using `Model.createSearchIndex()`.
+ * This function only works when connected to MongoDB Atlas.
+ *
+ * #### Example:
+ *
+ * const ToySchema = new Schema({ name: String, color: String, price: Number });
+ * ToySchema.searchIndex({ name: 'test', definition: { mappings: { dynamic: true } } });
+ *
+ * @param {Object} description index options, including `name` and `definition`
+ * @param {String} description.name
+ * @param {Object} description.definition
+ * @return {Schema} the Schema instance
+ * @api public
+ */
+
+Schema.prototype.searchIndex = function searchIndex(description) {
+ this._searchIndexes.push(description);
+
+ return this;
+};
+
/**
* Reserved document keys.
*
@@ -935,6 +1045,9 @@ reserved.collection = 1;
Schema.prototype.path = function(path, obj) {
if (obj === undefined) {
+ if (this.paths[path] != null) {
+ return this.paths[path];
+ }
// Convert to '.$' to check subpaths re: gh-6405
const cleanPath = _pathToPositionalSyntax(path);
let schematype = _getPath(this, path, cleanPath);
@@ -955,17 +1068,17 @@ Schema.prototype.path = function(path, obj) {
}
// subpaths?
- return /\.\d+\.?.*$/.test(path)
- ? getPositionalPath(this, path)
+ return hasNumericSubpathRegex.test(path)
+ ? getPositionalPath(this, path, cleanPath)
: undefined;
}
// some path names conflict with document methods
const firstPieceOfPath = path.split('.')[0];
- if (reserved[firstPieceOfPath] && !this.options.supressReservedKeysWarning) {
+ if (reserved[firstPieceOfPath] && !this.options.suppressReservedKeysWarning) {
const errorMessage = `\`${firstPieceOfPath}\` is a reserved schema pathname and may break some functionality. ` +
'You are allowed to use it, but use at your own risk. ' +
- 'To disable this warning pass `supressReservedKeysWarning` as a schema option.';
+ 'To disable this warning pass `suppressReservedKeysWarning` as a schema option.';
utils.warn(errorMessage);
}
@@ -1000,7 +1113,7 @@ Schema.prototype.path = function(path, obj) {
branch = branch[sub];
}
- branch[last] = utils.clone(obj);
+ branch[last] = clone(obj);
this.paths[path] = this.interpretAsType(path, obj, this.options);
const schemaType = this.paths[path];
@@ -1013,6 +1126,13 @@ Schema.prototype.path = function(path, obj) {
this.paths[mapPath] = schemaType.$__schemaType;
this.mapPaths.push(this.paths[mapPath]);
+ if (schemaType.$__schemaType.$isSingleNested) {
+ this.childSchemas.push({
+ schema: schemaType.$__schemaType.schema,
+ model: schemaType.$__schemaType.caster,
+ path: path
+ });
+ }
}
if (schemaType.$isSingleNested) {
@@ -1041,7 +1161,8 @@ Schema.prototype.path = function(path, obj) {
schemaType.caster.base = this.base;
this.childSchemas.push({
schema: schemaType.schema,
- model: schemaType.caster
+ model: schemaType.caster,
+ path: path
});
} else if (schemaType.$isMongooseDocumentArray) {
Object.defineProperty(schemaType.schema, 'base', {
@@ -1054,7 +1175,8 @@ Schema.prototype.path = function(path, obj) {
schemaType.casterConstructor.base = this.base;
this.childSchemas.push({
schema: schemaType.schema,
- model: schemaType.casterConstructor
+ model: schemaType.casterConstructor,
+ path: path
});
}
@@ -1070,15 +1192,14 @@ Schema.prototype.path = function(path, obj) {
if (_schemaType.$isMongooseDocumentArray) {
_schemaType.$embeddedSchemaType._arrayPath = arrayPath;
_schemaType.$embeddedSchemaType._arrayParentPath = path;
- _schemaType = _schemaType.$embeddedSchemaType.clone();
+ _schemaType = _schemaType.$embeddedSchemaType;
} else {
_schemaType.caster._arrayPath = arrayPath;
_schemaType.caster._arrayParentPath = path;
- _schemaType = _schemaType.caster.clone();
+ _schemaType = _schemaType.caster;
}
- _schemaType.path = arrayPath;
- toAdd.push(_schemaType);
+ this.subpaths[arrayPath] = _schemaType;
}
for (const _schemaType of toAdd) {
@@ -1090,22 +1211,22 @@ Schema.prototype.path = function(path, obj) {
for (const key of Object.keys(schemaType.schema.paths)) {
const _schemaType = schemaType.schema.paths[key];
this.subpaths[path + '.' + key] = _schemaType;
- if (typeof _schemaType === 'object' && _schemaType != null) {
- _schemaType.$isUnderneathDocArray = true;
+ if (typeof _schemaType === 'object' && _schemaType != null && _schemaType.$parentSchemaDocArray == null) {
+ _schemaType.$parentSchemaDocArray = schemaType;
}
}
for (const key of Object.keys(schemaType.schema.subpaths)) {
const _schemaType = schemaType.schema.subpaths[key];
this.subpaths[path + '.' + key] = _schemaType;
- if (typeof _schemaType === 'object' && _schemaType != null) {
- _schemaType.$isUnderneathDocArray = true;
+ if (typeof _schemaType === 'object' && _schemaType != null && _schemaType.$parentSchemaDocArray == null) {
+ _schemaType.$parentSchemaDocArray = schemaType;
}
}
for (const key of Object.keys(schemaType.schema.singleNestedPaths)) {
const _schemaType = schemaType.schema.singleNestedPaths[key];
this.subpaths[path + '.' + key] = _schemaType;
- if (typeof _schemaType === 'object' && _schemaType != null) {
- _schemaType.$isUnderneathDocArray = true;
+ if (typeof _schemaType === 'object' && _schemaType != null && _schemaType.$parentSchemaDocArray == null) {
+ _schemaType.$parentSchemaDocArray = schemaType;
}
}
}
@@ -1117,18 +1238,32 @@ Schema.prototype.path = function(path, obj) {
* ignore
*/
-function gatherChildSchemas(schema) {
+Schema.prototype._gatherChildSchemas = function _gatherChildSchemas() {
const childSchemas = [];
- for (const path of Object.keys(schema.paths)) {
- const schematype = schema.paths[path];
+ for (const path of Object.keys(this.paths)) {
+ if (typeof path !== 'string') {
+ continue;
+ }
+ const schematype = this.paths[path];
if (schematype.$isMongooseDocumentArray || schematype.$isSingleNested) {
- childSchemas.push({ schema: schematype.schema, model: schematype.caster });
+ childSchemas.push({
+ schema: schematype.schema,
+ model: schematype.caster,
+ path: path
+ });
+ } else if (schematype.$isSchemaMap && schematype.$__schemaType.$isSingleNested) {
+ childSchemas.push({
+ schema: schematype.$__schemaType.schema,
+ model: schematype.$__schemaType.caster,
+ path: path
+ });
}
}
+ this.childSchemas = childSchemas;
return childSchemas;
-}
+};
/*!
* ignore
@@ -1139,10 +1274,18 @@ function _getPath(schema, path, cleanPath) {
return schema.paths[path];
}
if (schema.subpaths.hasOwnProperty(cleanPath)) {
- return schema.subpaths[cleanPath];
+ const subpath = schema.subpaths[cleanPath];
+ if (subpath === 'nested') {
+ return undefined;
+ }
+ return subpath;
}
if (schema.singleNestedPaths.hasOwnProperty(cleanPath) && typeof schema.singleNestedPaths[cleanPath] === 'object') {
- return schema.singleNestedPaths[cleanPath];
+ const singleNestedPath = schema.singleNestedPaths[cleanPath];
+ if (singleNestedPath === 'nested') {
+ return undefined;
+ }
+ return singleNestedPath;
}
return null;
@@ -1211,6 +1354,7 @@ Schema.prototype.interpretAsType = function(path, obj, options) {
return clone;
}
+
// If this schema has an associated Mongoose object, use the Mongoose object's
// copy of SchemaTypes re: gh-7158 gh-6933
const MongooseTypes = this.base != null ? this.base.Schema.Types : Schema.Types;
@@ -1246,12 +1390,16 @@ Schema.prototype.interpretAsType = function(path, obj, options) {
// new Schema({ path: [new Schema({ ... })] })
if (cast && cast.instanceOfSchema) {
if (!(cast instanceof Schema)) {
- throw new TypeError('Schema for array path `' + path +
- '` is from a different copy of the Mongoose module. ' +
- 'Please make sure you\'re using the same version ' +
- 'of Mongoose everywhere with `npm list mongoose`. If you are still ' +
- 'getting this error, please add `new Schema()` around the path: ' +
- `${path}: new Schema(...)`);
+ if (this.options._isMerging) {
+ cast = new Schema(cast);
+ } else {
+ throw new TypeError('Schema for array path `' + path +
+ '` is from a different copy of the Mongoose module. ' +
+ 'Please make sure you\'re using the same version ' +
+ 'of Mongoose everywhere with `npm list mongoose`. If you are still ' +
+ 'getting this error, please add `new Schema()` around the path: ' +
+ `${path}: new Schema(...)`);
+ }
}
return new MongooseTypes.DocumentArray(path, cast, obj);
}
@@ -1259,18 +1407,26 @@ Schema.prototype.interpretAsType = function(path, obj, options) {
cast[options.typeKey] &&
cast[options.typeKey].instanceOfSchema) {
if (!(cast[options.typeKey] instanceof Schema)) {
- throw new TypeError('Schema for array path `' + path +
- '` is from a different copy of the Mongoose module. ' +
- 'Please make sure you\'re using the same version ' +
- 'of Mongoose everywhere with `npm list mongoose`. If you are still ' +
- 'getting this error, please add `new Schema()` around the path: ' +
- `${path}: new Schema(...)`);
+ if (this.options._isMerging) {
+ cast[options.typeKey] = new Schema(cast[options.typeKey]);
+ } else {
+ throw new TypeError('Schema for array path `' + path +
+ '` is from a different copy of the Mongoose module. ' +
+ 'Please make sure you\'re using the same version ' +
+ 'of Mongoose everywhere with `npm list mongoose`. If you are still ' +
+ 'getting this error, please add `new Schema()` around the path: ' +
+ `${path}: new Schema(...)`);
+ }
}
return new MongooseTypes.DocumentArray(path, cast[options.typeKey], obj, cast);
}
-
- if (Array.isArray(cast)) {
- return new MongooseTypes.Array(path, this.interpretAsType(path, cast, options), obj);
+ if (typeof cast !== 'undefined') {
+ if (Array.isArray(cast) || cast.type === Array || cast.type == 'Array') {
+ if (cast && cast.type == 'Array') {
+ cast.type = Array;
+ }
+ return new MongooseTypes.Array(path, this.interpretAsType(path, cast, options), obj);
+ }
}
// Handle both `new Schema({ arr: [{ subpath: String }] })` and `new Schema({ arr: [{ type: { subpath: string } }] })`
@@ -1295,12 +1451,19 @@ Schema.prototype.interpretAsType = function(path, obj, options) {
if (options.hasOwnProperty('strictQuery')) {
childSchemaOptions.strictQuery = options.strictQuery;
}
+ if (options.hasOwnProperty('toObject')) {
+ childSchemaOptions.toObject = utils.omit(options.toObject, ['transform']);
+ }
+ if (options.hasOwnProperty('toJSON')) {
+ childSchemaOptions.toJSON = utils.omit(options.toJSON, ['transform']);
+ }
if (this._userProvidedOptions.hasOwnProperty('_id')) {
childSchemaOptions._id = this._userProvidedOptions._id;
} else if (Schema.Types.DocumentArray.defaultOptions._id != null) {
childSchemaOptions._id = Schema.Types.DocumentArray.defaultOptions._id;
}
+
const childSchema = new Schema(castFromTypeKey, childSchemaOptions);
childSchema.$implicitlyCreated = true;
return new MongooseTypes.DocumentArray(path, childSchema, obj);
@@ -1314,7 +1477,6 @@ Schema.prototype.interpretAsType = function(path, obj, options) {
type = cast[options.typeKey] && (options.typeKey !== 'type' || !cast.type.type)
? cast[options.typeKey]
: cast;
-
if (Array.isArray(type)) {
return new MongooseTypes.Array(path, this.interpretAsType(path, type, options), obj);
}
@@ -1344,7 +1506,6 @@ Schema.prototype.interpretAsType = function(path, obj, options) {
}
if (type && type.instanceOfSchema) {
-
return new MongooseTypes.Subdocument(type, path, obj);
}
@@ -1530,9 +1691,6 @@ Schema.prototype.indexedPaths = function indexedPaths() {
*/
Schema.prototype.pathType = function(path) {
- // Convert to '.$' to check subpaths re: gh-6405
- const cleanPath = _pathToPositionalSyntax(path);
-
if (this.paths.hasOwnProperty(path)) {
return 'real';
}
@@ -1542,6 +1700,10 @@ Schema.prototype.pathType = function(path) {
if (this.nested.hasOwnProperty(path)) {
return 'nested';
}
+
+ // Convert to '.$' to check subpaths re: gh-6405
+ const cleanPath = _pathToPositionalSyntax(path);
+
if (this.subpaths.hasOwnProperty(cleanPath) || this.subpaths.hasOwnProperty(path)) {
return 'real';
}
@@ -1558,7 +1720,7 @@ Schema.prototype.pathType = function(path) {
}
if (/\.\d+\.|\.\d+$/.test(path)) {
- return getPositionalPathType(this, path);
+ return getPositionalPathType(this, path, cleanPath);
}
return 'adhocOrUndefined';
};
@@ -1602,7 +1764,7 @@ Schema.prototype.setupTimestamp = function(timestamps) {
* @api private
*/
-function getPositionalPathType(self, path) {
+function getPositionalPathType(self, path, cleanPath) {
const subpaths = path.split(/\.(\d+)\.|\.(\d+)$/).filter(Boolean);
if (subpaths.length < 2) {
return self.paths.hasOwnProperty(subpaths[0]) ?
@@ -1653,7 +1815,7 @@ function getPositionalPathType(self, path) {
val = val.schema.path(subpath);
}
- self.subpaths[path] = val;
+ self.subpaths[cleanPath] = val;
if (val) {
return 'real';
}
@@ -1668,9 +1830,9 @@ function getPositionalPathType(self, path) {
* ignore
*/
-function getPositionalPath(self, path) {
- getPositionalPathType(self, path);
- return self.subpaths[path];
+function getPositionalPath(self, path, cleanPath) {
+ getPositionalPathType(self, path, cleanPath);
+ return self.subpaths[cleanPath];
}
/**
@@ -1834,7 +1996,7 @@ Schema.prototype.post = function(name) {
* @param {Function} plugin The Plugin's callback
* @param {Object} [opts] Options to pass to the plugin
* @param {Boolean} [opts.deduplicate=false] If true, ignore duplicate plugins (same `fn` argument using `===`)
- * @see plugins /docs/plugins.html
+ * @see plugins https://mongoosejs.com/docs/plugins.html
* @api public
*/
@@ -1844,6 +2006,7 @@ Schema.prototype.plugin = function(fn, opts) {
'got "' + (typeof fn) + '"');
}
+
if (opts && opts.deduplicate) {
for (const plugin of this.plugins) {
if (plugin.fn === fn) {
@@ -1885,7 +2048,7 @@ Schema.prototype.plugin = function(fn, opts) {
* fizz.purr();
* fizz.scratch();
*
- * NOTE: `Schema.method()` adds instance methods to the `Schema.methods` object. You can also add instance methods directly to the `Schema.methods` object as seen in the [guide](/docs/guide.html#methods)
+ * NOTE: `Schema.method()` adds instance methods to the `Schema.methods` object. You can also add instance methods directly to the `Schema.methods` object as seen in the [guide](https://mongoosejs.com/docs/guide.html#methods)
*
* @param {String|Object} name The Method Name for a single function, or a Object of "string-function" pairs.
* @param {Function} [fn] The Function in a single-function definition.
@@ -1896,11 +2059,11 @@ Schema.prototype.method = function(name, fn, options) {
if (typeof name !== 'string') {
for (const i in name) {
this.methods[i] = name[i];
- this.methodOptions[i] = utils.clone(options);
+ this.methodOptions[i] = clone(options);
}
} else {
this.methods[name] = fn;
- this.methodOptions[name] = utils.clone(options);
+ this.methodOptions[name] = clone(options);
}
return this;
};
@@ -1935,7 +2098,7 @@ Schema.prototype.method = function(name, fn, options) {
* @param {String|Object} name The Method Name for a single function, or a Object of "string-function" pairs.
* @param {Function} [fn] The Function in a single-function definition.
* @api public
- * @see Statics /docs/guide.html#statics
+ * @see Statics https://mongoosejs.com/docs/guide.html#statics
*/
Schema.prototype.static = function(name, fn) {
@@ -1970,6 +2133,24 @@ Schema.prototype.index = function(fields, options) {
if (options.expires) {
utils.expires(options);
}
+ for (const key in fields) {
+ if (this.aliases[key]) {
+ fields = utils.renameObjKey(fields, key, this.aliases[key]);
+ }
+ }
+ for (const field of Object.keys(fields)) {
+ if (fields[field] === 'ascending' || fields[field] === 'asc') {
+ fields[field] = 1;
+ } else if (fields[field] === 'descending' || fields[field] === 'desc') {
+ fields[field] = -1;
+ }
+ }
+
+ for (const existingIndex of this.indexes()) {
+ if (options.name == null && existingIndex[1].name == null && isIndexSpecEqual(existingIndex[0], fields)) {
+ utils.warn(`Duplicate schema index on ${JSON.stringify(fields)} found. This is often due to declaring an index using both "index: true" and "schema.index()". Please remove the duplicate index definition.`);
+ }
+ }
this._indexes.push([fields, options]);
return this;
@@ -1986,18 +2167,28 @@ Schema.prototype.index = function(fields, options) {
*
* @param {String} key The name of the option to set the value to
* @param {Object} [value] The value to set the option to, if not passed, the option will be reset to default
- * @see Schema #schema_Schema
+ * @param {Array} [tags] tags to add to read preference if key === 'read'
+ * @see Schema https://mongoosejs.com/docs/api/schema.html#Schema()
* @api public
*/
-Schema.prototype.set = function(key, value, _tags) {
+Schema.prototype.set = function(key, value, tags) {
if (arguments.length === 1) {
return this.options[key];
}
switch (key) {
case 'read':
- this.options[key] = readPref(value, _tags);
+ if (typeof value === 'string') {
+ this.options[key] = { mode: handleReadPreferenceAliases(value), tags };
+ } else if (Array.isArray(value) && typeof value[0] === 'string') {
+ this.options[key] = {
+ mode: handleReadPreferenceAliases(value[0]),
+ tags: value[1]
+ };
+ } else {
+ this.options[key] = value;
+ }
this._userProvidedOptions[key] = this.options[key];
break;
case 'timestamps':
@@ -2021,9 +2212,43 @@ Schema.prototype.set = function(key, value, _tags) {
break;
}
+ // Propagate `strict` and `strictQuery` changes down to implicitly created schemas
+ if (key === 'strict') {
+ _propagateOptionsToImplicitlyCreatedSchemas(this, { strict: value });
+ }
+ if (key === 'strictQuery') {
+ _propagateOptionsToImplicitlyCreatedSchemas(this, { strictQuery: value });
+ }
+ if (key === 'toObject') {
+ value = { ...value };
+ // Avoid propagating transform to implicitly created schemas re: gh-3279
+ delete value.transform;
+ _propagateOptionsToImplicitlyCreatedSchemas(this, { toObject: value });
+ }
+ if (key === 'toJSON') {
+ value = { ...value };
+ // Avoid propagating transform to implicitly created schemas re: gh-3279
+ delete value.transform;
+ _propagateOptionsToImplicitlyCreatedSchemas(this, { toJSON: value });
+ }
+
return this;
};
+/*!
+ * Recursively set options on implicitly created schemas
+ */
+
+function _propagateOptionsToImplicitlyCreatedSchemas(baseSchema, options) {
+ for (const { schema } of baseSchema.childSchemas) {
+ if (!schema.$implicitlyCreated) {
+ continue;
+ }
+ Object.assign(schema.options, options);
+ _propagateOptionsToImplicitlyCreatedSchemas(schema, options);
+ }
+}
+
/**
* Gets a schema option.
*
@@ -2077,7 +2302,7 @@ Object.defineProperty(Schema, 'indexTypes', {
* // [ { registeredAt: 1 }, { background: true } ] ]
* userSchema.indexes();
*
- * [Plugins](/docs/plugins.html) can use the return value of this function to modify a schema's indexes.
+ * [Plugins](https://mongoosejs.com/docs/plugins.html) can use the return value of this function to modify a schema's indexes.
* For example, the below plugin makes every index unique by default.
*
* function myPlugin(schema) {
@@ -2101,12 +2326,14 @@ Schema.prototype.indexes = function() {
*
* @param {String} name The name of the Virtual
* @param {Object} [options]
- * @param {String|Model} [options.ref] model name or model instance. Marks this as a [populate virtual](/docs/populate.html#populate-virtuals).
- * @param {String|Function} [options.localField] Required for populate virtuals. See [populate virtual docs](/docs/populate.html#populate-virtuals) for more information.
- * @param {String|Function} [options.foreignField] Required for populate virtuals. See [populate virtual docs](/docs/populate.html#populate-virtuals) for more information.
+ * @param {String|Model} [options.ref] model name or model instance. Marks this as a [populate virtual](https://mongoosejs.com/docs/populate.html#populate-virtuals).
+ * @param {String|Function} [options.localField] Required for populate virtuals. See [populate virtual docs](https://mongoosejs.com/docs/populate.html#populate-virtuals) for more information.
+ * @param {String|Function} [options.foreignField] Required for populate virtuals. See [populate virtual docs](https://mongoosejs.com/docs/populate.html#populate-virtuals) for more information.
* @param {Boolean|Function} [options.justOne=false] Only works with populate virtuals. If [truthy](https://masteringjs.io/tutorials/fundamentals/truthy), will be a single doc or `null`. Otherwise, the populate virtual will be an array.
* @param {Boolean} [options.count=false] Only works with populate virtuals. If [truthy](https://masteringjs.io/tutorials/fundamentals/truthy), this populate virtual will contain the number of documents rather than the documents themselves when you `populate()`.
- * @param {Function|null} [options.get=null] Adds a [getter](/docs/tutorials/getters-setters.html) to this virtual to transform the populated doc.
+ * @param {Function|null} [options.get=null] Adds a [getter](https://mongoosejs.com/docs/tutorials/getters-setters.html) to this virtual to transform the populated doc.
+ * @param {Object|Function} [options.match=null] Apply a default [`match` option to populate](https://mongoosejs.com/docs/populate.html#match), adding an additional filter to the populate query.
+ * @param {Boolean} [options.applyToArray=false] If true and the given `name` is a direct child of an array, apply the virtual to the array rather than the elements.
* @return {VirtualType}
*/
@@ -2125,7 +2352,10 @@ Schema.prototype.virtual = function(name, options) {
throw new Error('Reference virtuals require `foreignField` option');
}
- this.pre('init', function virtualPreInit(obj) {
+ const virtual = this.virtual(name);
+ virtual.options = options;
+
+ this.pre('init', function virtualPreInit(obj, opts) {
if (mpath.has(name, obj)) {
const _v = mpath.get(name, obj);
if (!this.$$populatedVirtuals) {
@@ -2142,36 +2372,38 @@ Schema.prototype.virtual = function(name, options) {
_v == null ? [] : [_v];
}
+ if (opts?.hydratedPopulatedDocs && !options.count) {
+ const modelNames = virtual._getModelNamesForPopulate(this);
+ const populatedVal = this.$$populatedVirtuals[name];
+ if (!Array.isArray(populatedVal) && !populatedVal.$__ && modelNames?.length === 1) {
+ const PopulateModel = this.db.model(modelNames[0]);
+ this.$$populatedVirtuals[name] = PopulateModel.hydrate(populatedVal);
+ } else if (Array.isArray(populatedVal) && modelNames?.length === 1) {
+ const PopulateModel = this.db.model(modelNames[0]);
+ for (let i = 0; i < populatedVal.length; ++i) {
+ if (!populatedVal[i].$__) {
+ populatedVal[i] = PopulateModel.hydrate(populatedVal[i]);
+ }
+ }
+ }
+ }
+
mpath.unset(name, obj);
}
});
- const virtual = this.virtual(name);
- virtual.options = options;
-
virtual.
- set(function(_v) {
+ set(function(v) {
if (!this.$$populatedVirtuals) {
this.$$populatedVirtuals = {};
}
- if (options.justOne || options.count) {
- this.$$populatedVirtuals[name] = Array.isArray(_v) ?
- _v[0] :
- _v;
-
- if (typeof this.$$populatedVirtuals[name] !== 'object') {
- this.$$populatedVirtuals[name] = options.count ? _v : null;
- }
- } else {
- this.$$populatedVirtuals[name] = Array.isArray(_v) ?
- _v :
- _v == null ? [] : [_v];
-
- this.$$populatedVirtuals[name] = this.$$populatedVirtuals[name].filter(function(doc) {
- return doc && typeof doc === 'object';
- });
- }
+ return setPopulatedVirtualValue(
+ this.$$populatedVirtuals,
+ name,
+ v,
+ options
+ );
});
if (typeof options.get === 'function') {
@@ -2179,11 +2411,15 @@ Schema.prototype.virtual = function(name, options) {
}
// Workaround for gh-8198: if virtual is under document array, make a fake
- // virtual. See gh-8210
+ // virtual. See gh-8210, gh-13189
const parts = name.split('.');
let cur = parts[0];
for (let i = 0; i < parts.length - 1; ++i) {
- if (this.paths[cur] != null && this.paths[cur].$isMongooseDocumentArray) {
+ if (this.paths[cur] == null) {
+ continue;
+ }
+
+ if (this.paths[cur].$isMongooseDocumentArray || this.paths[cur].$isSingleNested) {
const remnant = parts.slice(i + 1).join('.');
this.paths[cur].schema.virtual(remnant, options);
break;
@@ -2210,6 +2446,15 @@ Schema.prototype.virtual = function(name, options) {
return mem[part];
}, this.tree);
+ if (options && options.applyToArray && parts.length > 1) {
+ const path = this.path(parts.slice(0, -1).join('.'));
+ if (path && path.$isMongooseArray) {
+ return path.virtual(parts[parts.length - 1], options);
+ } else {
+ throw new MongooseError(`Path "${path}" is not an array`);
+ }
+ }
+
return virtuals[name];
};
@@ -2315,6 +2560,11 @@ Schema.prototype.removeVirtual = function(path) {
for (const virtual of path) {
delete this.paths[virtual];
delete this.virtuals[virtual];
+ if (virtual.indexOf('.') !== -1) {
+ mpath.unset(virtual, this.tree);
+ } else {
+ delete this.tree[virtual];
+ }
}
}
return this;
@@ -2323,9 +2573,9 @@ Schema.prototype.removeVirtual = function(path) {
/**
* Loads an ES6 class into a schema. Maps [setters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/set) + [getters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get), [static methods](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/static),
* and [instance methods](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes#Class_body_and_method_definitions)
- * to schema [virtuals](/docs/guide.html#virtuals),
- * [statics](/docs/guide.html#statics), and
- * [methods](/docs/guide.html#methods).
+ * to schema [virtuals](https://mongoosejs.com/docs/guide.html#virtuals),
+ * [statics](https://mongoosejs.com/docs/guide.html#statics), and
+ * [methods](https://mongoosejs.com/docs/guide.html#methods).
*
* #### Example:
*
@@ -2449,26 +2699,30 @@ Schema.prototype._getSchema = function(path) {
// If there is no foundschema.schema we are dealing with
// a path like array.$
if (p !== parts.length) {
+ if (p + 1 === parts.length && foundschema.$embeddedSchemaType && (parts[p] === '$' || isArrayFilter(parts[p]))) {
+ return foundschema.$embeddedSchemaType;
+ }
+
if (foundschema.schema) {
let ret;
if (parts[p] === '$' || isArrayFilter(parts[p])) {
if (p + 1 === parts.length) {
// comments.$
- return foundschema;
+ return foundschema.$embeddedSchemaType;
}
// comments.$.comments.$.title
ret = search(parts.slice(p + 1), foundschema.schema);
if (ret) {
- ret.$isUnderneathDocArray = ret.$isUnderneathDocArray ||
- !foundschema.schema.$isSingleNested;
+ ret.$parentSchemaDocArray = ret.$parentSchemaDocArray ||
+ (foundschema.schema.$isSingleNested ? null : foundschema);
}
return ret;
}
// this is the last path of the selector
ret = search(parts.slice(p), foundschema.schema);
if (ret) {
- ret.$isUnderneathDocArray = ret.$isUnderneathDocArray ||
- !foundschema.schema.$isSingleNested;
+ ret.$parentSchemaDocArray = ret.$parentSchemaDocArray ||
+ (foundschema.schema.$isSingleNested ? null : foundschema);
}
return ret;
}
@@ -2506,6 +2760,9 @@ Schema.prototype._getSchema = function(path) {
// Re: gh-5628, because `schema.path()` doesn't take $ into account.
parts[i] = '0';
}
+ if (numberRE.test(parts[i])) {
+ parts[i] = '$';
+ }
}
return search(parts, _this);
};
@@ -2571,6 +2828,39 @@ Schema.prototype._getPathType = function(path) {
return search(path.split('.'), _this);
};
+/**
+ * Transforms the duplicate key error by checking for duplicate key error messages by path.
+ * If no duplicate key error messages are found, returns the original error.
+ *
+ * @param {Error} error The error to transform
+ * @returns {Error} The transformed error
+ * @api private
+ */
+
+Schema.prototype._transformDuplicateKeyError = function _transformDuplicateKeyError(error) {
+ if (!this._duplicateKeyErrorMessagesByPath) {
+ return error;
+ }
+ if (error.code !== 11000 && error.code !== 11001) {
+ return error;
+ }
+
+ if (error.keyPattern != null) {
+ const keyPattern = error.keyPattern;
+ const keys = Object.keys(keyPattern);
+ if (keys.length !== 1) {
+ return error;
+ }
+ const firstKey = keys[0];
+ if (!this._duplicateKeyErrorMessagesByPath.hasOwnProperty(firstKey)) {
+ return error;
+ }
+ return new MongooseError(this._duplicateKeyErrorMessagesByPath[firstKey], { cause: error });
+ }
+
+ return error;
+};
+
/*!
* ignore
*/
@@ -2589,7 +2879,7 @@ function isArrayFilter(piece) {
*/
Schema.prototype._preCompile = function _preCompile() {
- idGetter(this);
+ this.plugin(idGetter, { deduplicate: true });
};
/*!
@@ -2610,14 +2900,18 @@ module.exports = exports = Schema;
*
* #### Types:
*
- * - [String](/docs/schematypes.html#strings)
- * - [Number](/docs/schematypes.html#numbers)
- * - [Boolean](/docs/schematypes.html#booleans) | Bool
- * - [Array](/docs/schematypes.html#arrays)
- * - [Buffer](/docs/schematypes.html#buffers)
- * - [Date](/docs/schematypes.html#dates)
- * - [ObjectId](/docs/schematypes.html#objectids) | Oid
- * - [Mixed](/docs/schematypes.html#mixed)
+ * - [String](https://mongoosejs.com/docs/schematypes.html#strings)
+ * - [Number](https://mongoosejs.com/docs/schematypes.html#numbers)
+ * - [Boolean](https://mongoosejs.com/docs/schematypes.html#booleans) | Bool
+ * - [Array](https://mongoosejs.com/docs/schematypes.html#arrays)
+ * - [Buffer](https://mongoosejs.com/docs/schematypes.html#buffers)
+ * - [Date](https://mongoosejs.com/docs/schematypes.html#dates)
+ * - [ObjectId](https://mongoosejs.com/docs/schematypes.html#objectids) | Oid
+ * - [Mixed](https://mongoosejs.com/docs/schematypes.html#mixed)
+ * - [UUID](https://mongoosejs.com/docs/schematypes.html#uuid)
+ * - [BigInt](https://mongoosejs.com/docs/schematypes.html#bigint)
+ * - [Double] (https://mongoosejs.com/docs/schematypes.html#double)
+ * - [Int32](https://mongoosejs.com/docs/schematypes.html#int32)
*
* Using this exposed access to the `Mixed` SchemaType, we can use them in our schema.
*
diff --git a/lib/schema/array.js b/lib/schema/array.js
index ce5fc61cfa8..e424731e4d6 100644
--- a/lib/schema/array.js
+++ b/lib/schema/array.js
@@ -7,12 +7,16 @@
const $exists = require('./operators/exists');
const $type = require('./operators/type');
const MongooseError = require('../error/mongooseError');
-const SchemaArrayOptions = require('../options/SchemaArrayOptions');
-const SchemaType = require('../schematype');
+const SchemaArrayOptions = require('../options/schemaArrayOptions');
+const SchemaType = require('../schemaType');
const CastError = SchemaType.CastError;
const Mixed = require('./mixed');
+const VirtualOptions = require('../options/virtualOptions');
+const VirtualType = require('../virtualType');
const arrayDepth = require('../helpers/arrayDepth');
const cast = require('../cast');
+const clone = require('../helpers/clone');
+const getConstructorName = require('../helpers/getConstructorName');
const isOperator = require('../helpers/query/isOperator');
const util = require('util');
const utils = require('../utils');
@@ -53,7 +57,7 @@ function SchemaArray(key, cast, options, schemaOptions) {
if (utils.isPOJO(cast)) {
if (cast[typeKey]) {
// support { type: Woot }
- castOptions = utils.clone(cast); // do not alter user arguments
+ castOptions = clone(cast); // do not alter user arguments
delete castOptions[typeKey];
cast = cast[typeKey];
} else {
@@ -110,7 +114,7 @@ function SchemaArray(key, cast, options, schemaOptions) {
fn = typeof defaultArr === 'function';
}
- if (!('defaultValue' in this) || this.defaultValue !== void 0) {
+ if (!('defaultValue' in this) || this.defaultValue != null) {
const defaultFn = function() {
// Leave it up to `cast()` to convert the array
return fn
@@ -169,6 +173,20 @@ SchemaArray.defaultOptions = {};
*/
SchemaArray.set = SchemaType.set;
+SchemaArray.setters = [];
+
+/**
+ * Attaches a getter for all Array instances
+ *
+ * @param {Function} getter
+ * @return {this}
+ * @function get
+ * @static
+ * @api public
+ */
+
+SchemaArray.get = SchemaType.get;
+
/*!
* Inherits from SchemaType.
*/
@@ -202,6 +220,12 @@ SchemaArray._checkRequired = SchemaType.prototype.checkRequired;
SchemaArray.checkRequired = SchemaType.checkRequired;
+/*!
+ * Virtuals defined on this array itself.
+ */
+
+SchemaArray.prototype.virtuals = null;
+
/**
* Check if the given value satisfies the `required` validator.
*
@@ -274,13 +298,6 @@ SchemaArray.prototype.applyGetters = function(value, scope) {
}
const ret = SchemaType.prototype.applyGetters.call(this, value, scope);
- if (Array.isArray(ret)) {
- const rawValue = utils.isMongooseArray(ret) ? ret.__array : ret;
- const len = rawValue.length;
- for (let i = 0; i < len; ++i) {
- rawValue[i] = this.caster.applyGetters(rawValue[i], scope);
- }
- }
return ret;
};
@@ -358,7 +375,11 @@ SchemaArray.prototype.cast = function(value, doc, init, prev, options) {
options = options || emptyOpts;
let rawValue = utils.isMongooseArray(value) ? value.__array : value;
- value = MongooseArray(rawValue, options.path || this._arrayPath || this.path, doc, this);
+ let path = options.path || this.path;
+ if (options.arrayPathIndex != null) {
+ path += '.' + options.arrayPathIndex;
+ }
+ value = MongooseArray(rawValue, path, doc, this);
rawValue = value.__array;
if (init && doc != null && doc.$__ != null && doc.$populated(this.path)) {
@@ -477,6 +498,68 @@ SchemaArray.prototype.clone = function() {
return schematype;
};
+SchemaArray.prototype._castForQuery = function(val, context) {
+ let Constructor = this.casterConstructor;
+
+ if (val &&
+ Constructor.discriminators &&
+ Constructor.schema &&
+ Constructor.schema.options &&
+ Constructor.schema.options.discriminatorKey) {
+ if (typeof val[Constructor.schema.options.discriminatorKey] === 'string' &&
+ Constructor.discriminators[val[Constructor.schema.options.discriminatorKey]]) {
+ Constructor = Constructor.discriminators[val[Constructor.schema.options.discriminatorKey]];
+ } else {
+ const constructorByValue = getDiscriminatorByValue(Constructor.discriminators, val[Constructor.schema.options.discriminatorKey]);
+ if (constructorByValue) {
+ Constructor = constructorByValue;
+ }
+ }
+ }
+
+ const proto = this.casterConstructor.prototype;
+ const protoCastForQuery = proto && proto.castForQuery;
+ const protoCast = proto && proto.cast;
+ const constructorCastForQuery = Constructor.castForQuery;
+ const caster = this.caster;
+
+ if (Array.isArray(val)) {
+ this.setters.reverse().forEach(setter => {
+ val = setter.call(this, val, this);
+ });
+ val = val.map(function(v) {
+ if (utils.isObject(v) && v.$elemMatch) {
+ return v;
+ }
+ if (protoCastForQuery) {
+ v = protoCastForQuery.call(caster, null, v, context);
+ return v;
+ } else if (protoCast) {
+ v = protoCast.call(caster, v);
+ return v;
+ } else if (constructorCastForQuery) {
+ v = constructorCastForQuery.call(caster, null, v, context);
+ return v;
+ }
+ if (v != null) {
+ v = new Constructor(v);
+ return v;
+ }
+ return v;
+ });
+ } else if (protoCastForQuery) {
+ val = protoCastForQuery.call(caster, null, val, context);
+ } else if (protoCast) {
+ val = protoCast.call(caster, val);
+ } else if (constructorCastForQuery) {
+ val = constructorCastForQuery.call(caster, null, val, context);
+ } else if (val != null) {
+ val = new Constructor(val);
+ }
+
+ return val;
+};
+
/**
* Casts values for queries.
*
@@ -485,74 +568,49 @@ SchemaArray.prototype.clone = function() {
* @api private
*/
-SchemaArray.prototype.castForQuery = function($conditional, value) {
+SchemaArray.prototype.castForQuery = function($conditional, val, context) {
let handler;
- let val;
- if (arguments.length === 2) {
+ if ($conditional != null) {
handler = this.$conditionalHandlers[$conditional];
if (!handler) {
throw new Error('Can\'t use ' + $conditional + ' with Array.');
}
- val = handler.call(this, value);
+ return handler.call(this, val, context);
} else {
- val = $conditional;
- let Constructor = this.casterConstructor;
-
- if (val &&
- Constructor.discriminators &&
- Constructor.schema &&
- Constructor.schema.options &&
- Constructor.schema.options.discriminatorKey) {
- if (typeof val[Constructor.schema.options.discriminatorKey] === 'string' &&
- Constructor.discriminators[val[Constructor.schema.options.discriminatorKey]]) {
- Constructor = Constructor.discriminators[val[Constructor.schema.options.discriminatorKey]];
- } else {
- const constructorByValue = getDiscriminatorByValue(Constructor.discriminators, val[Constructor.schema.options.discriminatorKey]);
- if (constructorByValue) {
- Constructor = constructorByValue;
- }
- }
- }
+ return this._castForQuery(val, context);
+ }
+};
- const proto = this.casterConstructor.prototype;
- let method = proto && (proto.castForQuery || proto.cast);
- if (!method && Constructor.castForQuery) {
- method = Constructor.castForQuery;
- }
- const caster = this.caster;
+/**
+ * Add a virtual to this array. Specifically to this array, not the individual elements.
+ *
+ * @param {String} name
+ * @param {Object} [options]
+ * @api private
+ */
- if (Array.isArray(val)) {
- this.setters.reverse().forEach(setter => {
- val = setter.call(this, val, this);
- });
- val = val.map(function(v) {
- if (utils.isObject(v) && v.$elemMatch) {
- return v;
- }
- if (method) {
- v = method.call(caster, v);
- return v;
- }
- if (v != null) {
- v = new Constructor(v);
- return v;
- }
- return v;
- });
- } else if (method) {
- val = method.call(caster, val);
- } else if (val != null) {
- val = new Constructor(val);
- }
+SchemaArray.prototype.virtual = function virtual(name, options) {
+ if (name instanceof VirtualType || getConstructorName(name) === 'VirtualType') {
+ return this.virtual(name.path, name.options);
}
+ options = new VirtualOptions(options);
- return val;
+ if (utils.hasUserDefinedProperty(options, ['ref', 'refPath'])) {
+ throw new MongooseError('Cannot set populate virtual as a property of an array');
+ }
+
+ const virtual = new VirtualType(options, name);
+ if (this.virtuals === null) {
+ this.virtuals = {};
+ }
+ this.virtuals[name] = virtual;
+ return virtual;
};
-function cast$all(val) {
+function cast$all(val, context) {
if (!Array.isArray(val)) {
val = [val];
}
@@ -570,38 +628,21 @@ function cast$all(val) {
return cast(this.casterConstructor.schema, o, null, this && this.$$context)[this.path];
}, this);
- return this.castForQuery(val);
+ return this.castForQuery(null, val, context);
}
-function cast$elemMatch(val) {
+function cast$elemMatch(val, context) {
const keys = Object.keys(val);
const numKeys = keys.length;
for (let i = 0; i < numKeys; ++i) {
const key = keys[i];
const value = val[key];
if (isOperator(key) && value != null) {
- val[key] = this.castForQuery(key, value);
+ val[key] = this.castForQuery(key, value, context);
}
}
- // Is this an embedded discriminator and is the discriminator key set?
- // If so, use the discriminator schema. See gh-7449
- const discriminatorKey = this &&
- this.casterConstructor &&
- this.casterConstructor.schema &&
- this.casterConstructor.schema.options &&
- this.casterConstructor.schema.options.discriminatorKey;
- const discriminators = this &&
- this.casterConstructor &&
- this.casterConstructor.schema &&
- this.casterConstructor.schema.discriminators || {};
- if (discriminatorKey != null &&
- val[discriminatorKey] != null &&
- discriminators[val[discriminatorKey]] != null) {
- return cast(discriminators[val[discriminatorKey]], val, null, this && this.$$context);
- }
-
- return cast(this.casterConstructor.schema, val, null, this && this.$$context);
+ return val;
}
const handle = SchemaArray.prototype.$conditionalHandlers = {};
@@ -615,14 +656,14 @@ handle.$and = createLogicalQueryOperatorHandler('$and');
handle.$nor = createLogicalQueryOperatorHandler('$nor');
function createLogicalQueryOperatorHandler(op) {
- return function logicalQueryOperatorHandler(val) {
+ return function logicalQueryOperatorHandler(val, context) {
if (!Array.isArray(val)) {
throw new TypeError('conditional ' + op + ' requires an array');
}
const ret = [];
for (const obj of val) {
- ret.push(cast(this.casterConstructor.schema, obj, null, this && this.$$context));
+ ret.push(cast(this.casterConstructor.schema ?? context.schema, obj, null, this && this.$$context));
}
return ret;
@@ -647,9 +688,9 @@ handle.$gt =
handle.$gte =
handle.$lt =
handle.$lte =
-handle.$ne =
handle.$not =
-handle.$regex = SchemaArray.prototype.castForQuery;
+handle.$regex =
+handle.$ne = SchemaArray.prototype._castForQuery;
// `$in` is special because you can also include an empty array in the query
// like `$in: [1, []]`, see gh-5913
diff --git a/lib/schema/bigint.js b/lib/schema/bigint.js
new file mode 100644
index 00000000000..4dcebcbd41d
--- /dev/null
+++ b/lib/schema/bigint.js
@@ -0,0 +1,247 @@
+'use strict';
+
+/*!
+ * Module dependencies.
+ */
+
+const CastError = require('../error/cast');
+const SchemaType = require('../schemaType');
+const castBigInt = require('../cast/bigint');
+
+/**
+ * BigInt SchemaType constructor.
+ *
+ * @param {String} path
+ * @param {Object} options
+ * @inherits SchemaType
+ * @api public
+ */
+
+function SchemaBigInt(path, options) {
+ SchemaType.call(this, path, options, 'BigInt');
+}
+
+/**
+ * This schema type's name, to defend against minifiers that mangle
+ * function names.
+ *
+ * @api public
+ */
+SchemaBigInt.schemaName = 'BigInt';
+
+SchemaBigInt.defaultOptions = {};
+
+/*!
+ * Inherits from SchemaType.
+ */
+SchemaBigInt.prototype = Object.create(SchemaType.prototype);
+SchemaBigInt.prototype.constructor = SchemaBigInt;
+
+/*!
+ * ignore
+ */
+
+SchemaBigInt._cast = castBigInt;
+
+/**
+ * Sets a default option for all BigInt instances.
+ *
+ * #### Example:
+ *
+ * // Make all bigints required by default
+ * mongoose.Schema.BigInt.set('required', true);
+ *
+ * @param {String} option The option you'd like to set the value for
+ * @param {Any} value value for option
+ * @return {undefined}
+ * @function set
+ * @static
+ * @api public
+ */
+
+SchemaBigInt.set = SchemaType.set;
+
+SchemaBigInt.setters = [];
+
+/**
+ * Attaches a getter for all BigInt instances
+ *
+ * #### Example:
+ *
+ * // Convert bigints to numbers
+ * mongoose.Schema.BigInt.get(v => v == null ? v : Number(v));
+ *
+ * @param {Function} getter
+ * @return {this}
+ * @function get
+ * @static
+ * @api public
+ */
+
+SchemaBigInt.get = SchemaType.get;
+
+/**
+ * Get/set the function used to cast arbitrary values to bigints.
+ *
+ * #### Example:
+ *
+ * // Make Mongoose cast empty string '' to false.
+ * const original = mongoose.Schema.Types.BigInt.cast();
+ * mongoose.Schema.BigInt.cast(v => {
+ * if (v === '') {
+ * return false;
+ * }
+ * return original(v);
+ * });
+ *
+ * // Or disable casting entirely
+ * mongoose.Schema.BigInt.cast(false);
+ *
+ * @param {Function} caster
+ * @return {Function}
+ * @function get
+ * @static
+ * @api public
+ */
+
+SchemaBigInt.cast = function cast(caster) {
+ if (arguments.length === 0) {
+ return this._cast;
+ }
+ if (caster === false) {
+ caster = this._defaultCaster;
+ }
+ this._cast = caster;
+
+ return this._cast;
+};
+
+/*!
+ * ignore
+ */
+
+SchemaBigInt._checkRequired = v => v != null;
+
+/**
+ * Override the function the required validator uses to check whether a value
+ * passes the `required` check.
+ *
+ * @param {Function} fn
+ * @return {Function}
+ * @function checkRequired
+ * @static
+ * @api public
+ */
+
+SchemaBigInt.checkRequired = SchemaType.checkRequired;
+
+/**
+ * Check if the given value satisfies a required validator.
+ *
+ * @param {Any} value
+ * @return {Boolean}
+ * @api public
+ */
+
+SchemaBigInt.prototype.checkRequired = function(value) {
+ return this.constructor._checkRequired(value);
+};
+
+/**
+ * Casts to bigint
+ *
+ * @param {Object} value
+ * @param {Object} model this value is optional
+ * @api private
+ */
+
+SchemaBigInt.prototype.cast = function(value) {
+ let castBigInt;
+ if (typeof this._castFunction === 'function') {
+ castBigInt = this._castFunction;
+ } else if (typeof this.constructor.cast === 'function') {
+ castBigInt = this.constructor.cast();
+ } else {
+ castBigInt = SchemaBigInt.cast();
+ }
+
+ try {
+ return castBigInt(value);
+ } catch (error) {
+ throw new CastError('BigInt', value, this.path, error, this);
+ }
+};
+
+/*!
+ * ignore
+ */
+
+SchemaBigInt.$conditionalHandlers = {
+ ...SchemaType.prototype.$conditionalHandlers,
+ $gt: handleSingle,
+ $gte: handleSingle,
+ $lt: handleSingle,
+ $lte: handleSingle
+};
+
+/*!
+ * ignore
+ */
+
+function handleSingle(val, context) {
+ return this.castForQuery(null, val, context);
+}
+
+/**
+ * Casts contents for queries.
+ *
+ * @param {String} $conditional
+ * @param {any} val
+ * @api private
+ */
+
+SchemaBigInt.prototype.castForQuery = function($conditional, val, context) {
+ let handler;
+ if ($conditional != null) {
+ handler = SchemaBigInt.$conditionalHandlers[$conditional];
+
+ if (handler) {
+ return handler.call(this, val);
+ }
+
+ return this.applySetters(val, context);
+ }
+
+ try {
+ return this.applySetters(val, context);
+ } catch (err) {
+ if (err instanceof CastError && err.path === this.path && this.$fullPath != null) {
+ err.path = this.$fullPath;
+ }
+ throw err;
+ }
+};
+
+/**
+ *
+ * @api private
+ */
+
+SchemaBigInt.prototype._castNullish = function _castNullish(v) {
+ if (typeof v === 'undefined') {
+ return v;
+ }
+ const castBigInt = typeof this.constructor.cast === 'function' ?
+ this.constructor.cast() :
+ SchemaBigInt.cast();
+ if (castBigInt == null) {
+ return v;
+ }
+ return v;
+};
+
+/*!
+ * Module exports.
+ */
+
+module.exports = SchemaBigInt;
diff --git a/lib/schema/boolean.js b/lib/schema/boolean.js
index f09c8dd415e..1cbade08c6d 100644
--- a/lib/schema/boolean.js
+++ b/lib/schema/boolean.js
@@ -5,9 +5,8 @@
*/
const CastError = require('../error/cast');
-const SchemaType = require('../schematype');
+const SchemaType = require('../schemaType');
const castBoolean = require('../cast/boolean');
-const utils = require('../utils');
/**
* Boolean SchemaType constructor.
@@ -65,6 +64,27 @@ SchemaBoolean._cast = castBoolean;
SchemaBoolean.set = SchemaType.set;
+SchemaBoolean.setters = [];
+
+/**
+ * Attaches a getter for all Boolean instances
+ *
+ * #### Example:
+ *
+ * mongoose.Schema.Boolean.get(v => v === true ? 'yes' : 'no');
+ *
+ * const Order = mongoose.model('Order', new Schema({ isPaid: Boolean }));
+ * new Order({ isPaid: false }).isPaid; // 'no'
+ *
+ * @param {Function} getter
+ * @return {this}
+ * @function get
+ * @static
+ * @api public
+ */
+
+SchemaBoolean.get = SchemaType.get;
+
/**
* Get/set the function used to cast arbitrary values to booleans.
*
@@ -156,6 +176,8 @@ SchemaBoolean.prototype.checkRequired = function(value) {
* new M({ b: 'affirmative' }).b; // true
*
* @property convertToTrue
+ * @static
+ * @memberOf SchemaBoolean
* @type {Set}
* @api public
*/
@@ -176,6 +198,8 @@ Object.defineProperty(SchemaBoolean, 'convertToTrue', {
* new M({ b: 'nay' }).b; // false
*
* @property convertToFalse
+ * @static
+ * @memberOf SchemaBoolean
* @type {Set}
* @api public
*/
@@ -210,8 +234,7 @@ SchemaBoolean.prototype.cast = function(value) {
}
};
-SchemaBoolean.$conditionalHandlers =
- utils.options(SchemaType.prototype.$conditionalHandlers, {});
+SchemaBoolean.$conditionalHandlers = { ...SchemaType.prototype.$conditionalHandlers };
/**
* Casts contents for queries.
@@ -221,19 +244,26 @@ SchemaBoolean.$conditionalHandlers =
* @api private
*/
-SchemaBoolean.prototype.castForQuery = function($conditional, val) {
+SchemaBoolean.prototype.castForQuery = function($conditional, val, context) {
let handler;
- if (arguments.length === 2) {
+ if ($conditional != null) {
handler = SchemaBoolean.$conditionalHandlers[$conditional];
if (handler) {
return handler.call(this, val);
}
- return this._castForQuery(val);
+ return this.applySetters(val, context);
}
- return this._castForQuery($conditional);
+ try {
+ return this.applySetters(val, context);
+ } catch (err) {
+ if (err instanceof CastError && err.path === this.path && this.$fullPath != null) {
+ err.path = this.$fullPath;
+ }
+ throw err;
+ }
};
/**
diff --git a/lib/schema/buffer.js b/lib/schema/buffer.js
index abe0859314f..e5cec2e0158 100644
--- a/lib/schema/buffer.js
+++ b/lib/schema/buffer.js
@@ -5,8 +5,8 @@
'use strict';
const MongooseBuffer = require('../types/buffer');
-const SchemaBufferOptions = require('../options/SchemaBufferOptions');
-const SchemaType = require('../schematype');
+const SchemaBufferOptions = require('../options/schemaBufferOptions');
+const SchemaType = require('../schemaType');
const handleBitwiseOperator = require('./operators/bitwise');
const utils = require('../utils');
@@ -70,6 +70,28 @@ SchemaBuffer._checkRequired = v => !!(v && v.length);
SchemaBuffer.set = SchemaType.set;
+SchemaBuffer.setters = [];
+
+/**
+ * Attaches a getter for all Buffer instances
+ *
+ * #### Example:
+ *
+ * // Always convert to string when getting an ObjectId
+ * mongoose.Schema.Types.Buffer.get(v => v.toString('hex'));
+ *
+ * const Model = mongoose.model('Test', new Schema({ buf: Buffer } }));
+ * typeof (new Model({ buf: Buffer.fromString('hello') }).buf); // 'string'
+ *
+ * @param {Function} getter
+ * @return {this}
+ * @function get
+ * @static
+ * @api public
+ */
+
+SchemaBuffer.get = SchemaType.get;
+
/**
* Override the function the required validator uses to check whether a string
* passes the `required` check.
@@ -197,6 +219,14 @@ SchemaBuffer.prototype.cast = function(value, doc, init) {
return ret;
}
+ if (utils.isPOJO(value) && (value.$binary instanceof Binary || typeof value.$binary === 'string')) {
+ const buf = this.cast(Buffer.from(value.$binary, 'base64'));
+ if (value.$type != null) {
+ buf._subtype = value.$type;
+ return buf;
+ }
+ }
+
throw new CastError('Buffer', value, this.path, null, this);
};
@@ -224,21 +254,21 @@ SchemaBuffer.prototype.subtype = function(subtype) {
/*!
* ignore
*/
-function handleSingle(val) {
- return this.castForQuery(val);
+function handleSingle(val, context) {
+ return this.castForQuery(null, val, context);
}
-SchemaBuffer.prototype.$conditionalHandlers =
- utils.options(SchemaType.prototype.$conditionalHandlers, {
- $bitsAllClear: handleBitwiseOperator,
- $bitsAnyClear: handleBitwiseOperator,
- $bitsAllSet: handleBitwiseOperator,
- $bitsAnySet: handleBitwiseOperator,
- $gt: handleSingle,
- $gte: handleSingle,
- $lt: handleSingle,
- $lte: handleSingle
- });
+SchemaBuffer.prototype.$conditionalHandlers = {
+ ...SchemaType.prototype.$conditionalHandlers,
+ $bitsAllClear: handleBitwiseOperator,
+ $bitsAnyClear: handleBitwiseOperator,
+ $bitsAllSet: handleBitwiseOperator,
+ $bitsAnySet: handleBitwiseOperator,
+ $gt: handleSingle,
+ $gte: handleSingle,
+ $lt: handleSingle,
+ $lte: handleSingle
+};
/**
* Casts contents for queries.
@@ -248,17 +278,25 @@ SchemaBuffer.prototype.$conditionalHandlers =
* @api private
*/
-SchemaBuffer.prototype.castForQuery = function($conditional, val) {
+SchemaBuffer.prototype.castForQuery = function($conditional, val, context) {
let handler;
- if (arguments.length === 2) {
+ if ($conditional != null) {
handler = this.$conditionalHandlers[$conditional];
if (!handler) {
throw new Error('Can\'t use ' + $conditional + ' with Buffer.');
}
return handler.call(this, val);
}
- val = $conditional;
- const casted = this._castForQuery(val);
+
+ let casted;
+ try {
+ casted = this.applySetters(val, context);
+ } catch (err) {
+ if (err instanceof CastError && err.path === this.path && this.$fullPath != null) {
+ err.path = this.$fullPath;
+ }
+ throw err;
+ }
return casted ? casted.toObject({ transform: false, virtuals: false }) : casted;
};
diff --git a/lib/schema/date.js b/lib/schema/date.js
index feafe4709ef..6cbfee83865 100644
--- a/lib/schema/date.js
+++ b/lib/schema/date.js
@@ -5,8 +5,8 @@
'use strict';
const MongooseError = require('../error/index');
-const SchemaDateOptions = require('../options/SchemaDateOptions');
-const SchemaType = require('../schematype');
+const SchemaDateOptions = require('../options/schemaDateOptions');
+const SchemaType = require('../schemaType');
const castDate = require('../cast/date');
const getConstructorName = require('../helpers/getConstructorName');
const utils = require('../utils');
@@ -70,6 +70,28 @@ SchemaDate._cast = castDate;
SchemaDate.set = SchemaType.set;
+SchemaDate.setters = [];
+
+/**
+ * Attaches a getter for all Date instances
+ *
+ * #### Example:
+ *
+ * // Always convert Dates to string
+ * mongoose.Date.get(v => v.toString());
+ *
+ * const Model = mongoose.model('Test', new Schema({ date: { type: Date, default: () => new Date() } }));
+ * typeof (new Model({}).date); // 'string'
+ *
+ * @param {Function} getter
+ * @return {this}
+ * @function get
+ * @static
+ * @api public
+ */
+
+SchemaDate.get = SchemaType.get;
+
/**
* Get/set the function used to cast arbitrary values to dates.
*
@@ -234,7 +256,7 @@ SchemaDate.prototype.checkRequired = function(value, doc) {
* @param {Date} value minimum date
* @param {String} [message] optional custom error message
* @return {SchemaType} this
- * @see Customized Error Messages #error_messages_MongooseError-messages
+ * @see Customized Error Messages https://mongoosejs.com/docs/api/error.html#Error.messages
* @api public
*/
@@ -296,7 +318,7 @@ SchemaDate.prototype.min = function(value, message) {
* @param {Date} maximum date
* @param {String} [message] optional custom error message
* @return {SchemaType} this
- * @see Customized Error Messages #error_messages_MongooseError-messages
+ * @see Customized Error Messages https://mongoosejs.com/docs/api/error.html#Error.messages
* @api public
*/
@@ -366,13 +388,13 @@ function handleSingle(val) {
return this.cast(val);
}
-SchemaDate.prototype.$conditionalHandlers =
- utils.options(SchemaType.prototype.$conditionalHandlers, {
- $gt: handleSingle,
- $gte: handleSingle,
- $lt: handleSingle,
- $lte: handleSingle
- });
+SchemaDate.prototype.$conditionalHandlers = {
+ ...SchemaType.prototype.$conditionalHandlers,
+ $gt: handleSingle,
+ $gte: handleSingle,
+ $lt: handleSingle,
+ $lte: handleSingle
+};
/**
@@ -383,9 +405,16 @@ SchemaDate.prototype.$conditionalHandlers =
* @api private
*/
-SchemaDate.prototype.castForQuery = function($conditional, val) {
- if (arguments.length !== 2) {
- return this._castForQuery($conditional);
+SchemaDate.prototype.castForQuery = function($conditional, val, context) {
+ if ($conditional == null) {
+ try {
+ return this.applySetters(val, context);
+ } catch (err) {
+ if (err instanceof CastError && err.path === this.path && this.$fullPath != null) {
+ err.path = this.$fullPath;
+ }
+ throw err;
+ }
}
const handler = this.$conditionalHandlers[$conditional];
diff --git a/lib/schema/decimal128.js b/lib/schema/decimal128.js
index 6ac87416fe6..70fcfecc607 100644
--- a/lib/schema/decimal128.js
+++ b/lib/schema/decimal128.js
@@ -4,10 +4,9 @@
'use strict';
-const SchemaType = require('../schematype');
+const SchemaType = require('../schemaType');
const CastError = SchemaType.CastError;
const castDecimal128 = require('../cast/decimal128');
-const utils = require('../utils');
const isBsonType = require('../helpers/isBsonType');
/**
@@ -19,7 +18,7 @@ const isBsonType = require('../helpers/isBsonType');
* @api public
*/
-function Decimal128(key, options) {
+function SchemaDecimal128(key, options) {
SchemaType.call(this, key, options, 'Decimal128');
}
@@ -29,21 +28,21 @@ function Decimal128(key, options) {
*
* @api public
*/
-Decimal128.schemaName = 'Decimal128';
+SchemaDecimal128.schemaName = 'Decimal128';
-Decimal128.defaultOptions = {};
+SchemaDecimal128.defaultOptions = {};
/*!
* Inherits from SchemaType.
*/
-Decimal128.prototype = Object.create(SchemaType.prototype);
-Decimal128.prototype.constructor = Decimal128;
+SchemaDecimal128.prototype = Object.create(SchemaType.prototype);
+SchemaDecimal128.prototype.constructor = SchemaDecimal128;
/*!
* ignore
*/
-Decimal128._cast = castDecimal128;
+SchemaDecimal128._cast = castDecimal128;
/**
* Sets a default option for all Decimal128 instances.
@@ -64,7 +63,26 @@ Decimal128._cast = castDecimal128;
* @api public
*/
-Decimal128.set = SchemaType.set;
+SchemaDecimal128.set = SchemaType.set;
+
+SchemaDecimal128.setters = [];
+
+/**
+ * Attaches a getter for all Decimal128 instances
+ *
+ * #### Example:
+ *
+ * // Automatically convert Decimal128s to Numbers
+ * mongoose.Schema.Decimal128.get(v => v == null ? v : Number(v));
+ *
+ * @param {Function} getter
+ * @return {this}
+ * @function get
+ * @static
+ * @api public
+ */
+
+SchemaDecimal128.get = SchemaType.get;
/**
* Get/set the function used to cast arbitrary values to decimals.
@@ -88,7 +106,7 @@ Decimal128.set = SchemaType.set;
* @api public
*/
-Decimal128.cast = function cast(caster) {
+SchemaDecimal128.cast = function cast(caster) {
if (arguments.length === 0) {
return this._cast;
}
@@ -104,7 +122,7 @@ Decimal128.cast = function cast(caster) {
* ignore
*/
-Decimal128._defaultCaster = v => {
+SchemaDecimal128._defaultCaster = v => {
if (v != null && !isBsonType(v, 'Decimal128')) {
throw new Error();
}
@@ -115,7 +133,7 @@ Decimal128._defaultCaster = v => {
* ignore
*/
-Decimal128._checkRequired = v => isBsonType(v, 'Decimal128');
+SchemaDecimal128._checkRequired = v => isBsonType(v, 'Decimal128');
/**
* Override the function the required validator uses to check whether a string
@@ -128,7 +146,7 @@ Decimal128._checkRequired = v => isBsonType(v, 'Decimal128');
* @api public
*/
-Decimal128.checkRequired = SchemaType.checkRequired;
+SchemaDecimal128.checkRequired = SchemaType.checkRequired;
/**
* Check if the given value satisfies a required validator.
@@ -139,7 +157,7 @@ Decimal128.checkRequired = SchemaType.checkRequired;
* @api public
*/
-Decimal128.prototype.checkRequired = function checkRequired(value, doc) {
+SchemaDecimal128.prototype.checkRequired = function checkRequired(value, doc) {
if (SchemaType._isRef(this, value, doc, true)) {
return !!value;
}
@@ -148,7 +166,7 @@ Decimal128.prototype.checkRequired = function checkRequired(value, doc) {
// plugins like mongoose-float use `inherits()` for pre-ES6.
const _checkRequired = typeof this.constructor.checkRequired === 'function' ?
this.constructor.checkRequired() :
- Decimal128.checkRequired();
+ SchemaDecimal128.checkRequired();
return _checkRequired(value);
};
@@ -162,7 +180,7 @@ Decimal128.prototype.checkRequired = function checkRequired(value, doc) {
* @api private
*/
-Decimal128.prototype.cast = function(value, doc, init) {
+SchemaDecimal128.prototype.cast = function(value, doc, init) {
if (SchemaType._isRef(this, value, doc, init)) {
if (isBsonType(value, 'Decimal128')) {
return value;
@@ -177,7 +195,7 @@ Decimal128.prototype.cast = function(value, doc, init) {
} else if (typeof this.constructor.cast === 'function') {
castDecimal128 = this.constructor.cast();
} else {
- castDecimal128 = Decimal128.cast();
+ castDecimal128 = SchemaDecimal128.cast();
}
try {
@@ -195,16 +213,16 @@ function handleSingle(val) {
return this.cast(val);
}
-Decimal128.prototype.$conditionalHandlers =
- utils.options(SchemaType.prototype.$conditionalHandlers, {
- $gt: handleSingle,
- $gte: handleSingle,
- $lt: handleSingle,
- $lte: handleSingle
- });
+SchemaDecimal128.prototype.$conditionalHandlers = {
+ ...SchemaType.prototype.$conditionalHandlers,
+ $gt: handleSingle,
+ $gte: handleSingle,
+ $lt: handleSingle,
+ $lte: handleSingle
+};
/*!
* Module exports.
*/
-module.exports = Decimal128;
+module.exports = SchemaDecimal128;
diff --git a/lib/schema/documentarray.js b/lib/schema/documentArray.js
similarity index 78%
rename from lib/schema/documentarray.js
rename to lib/schema/documentArray.js
index 3867c512aa2..413dc4a8fbc 100644
--- a/lib/schema/documentarray.js
+++ b/lib/schema/documentArray.js
@@ -4,18 +4,21 @@
* Module dependencies.
*/
-const ArrayType = require('./array');
const CastError = require('../error/cast');
-const DocumentArrayElement = require('./DocumentArrayElement');
+const DocumentArrayElement = require('./documentArrayElement');
const EventEmitter = require('events').EventEmitter;
+const SchemaArray = require('./array');
const SchemaDocumentArrayOptions =
- require('../options/SchemaDocumentArrayOptions');
-const SchemaType = require('../schematype');
+ require('../options/schemaDocumentArrayOptions');
+const SchemaType = require('../schemaType');
+const cast = require('../cast');
const discriminator = require('../helpers/model/discriminator');
const handleIdOption = require('../helpers/schema/handleIdOption');
const handleSpreadDoc = require('../helpers/document/handleSpreadDoc');
+const isOperator = require('../helpers/query/isOperator');
const utils = require('../utils');
const getConstructor = require('../helpers/discriminator/getConstructor');
+const InvalidSchemaOptionError = require('../error/invalidSchemaOption');
const arrayAtomicsSymbol = require('../helpers/symbols').arrayAtomicsSymbol;
const arrayPathSymbol = require('../helpers/symbols').arrayPathSymbol;
@@ -35,9 +38,12 @@ let Subdocument;
* @api public
*/
-function DocumentArrayPath(key, schema, options, schemaOptions) {
- const schemaTypeIdOption = DocumentArrayPath.defaultOptions &&
- DocumentArrayPath.defaultOptions._id;
+function SchemaDocumentArray(key, schema, options, schemaOptions) {
+ if (schema.options && schema.options.timeseries) {
+ throw new InvalidSchemaOptionError(key, 'timeseries');
+ }
+ const schemaTypeIdOption = SchemaDocumentArray.defaultOptions &&
+ SchemaDocumentArray.defaultOptions._id;
if (schemaTypeIdOption != null) {
schemaOptions = schemaOptions || {};
schemaOptions._id = schemaTypeIdOption;
@@ -52,7 +58,7 @@ function DocumentArrayPath(key, schema, options, schemaOptions) {
const EmbeddedDocument = _createConstructor(schema, options);
EmbeddedDocument.prototype.$basePath = key;
- ArrayType.call(this, key, EmbeddedDocument, options);
+ SchemaArray.call(this, key, EmbeddedDocument, options);
this.schema = schema;
this.schemaOptions = schemaOptions || {};
@@ -63,7 +69,7 @@ function DocumentArrayPath(key, schema, options, schemaOptions) {
const fn = this.defaultValue;
- if (!('defaultValue' in this) || fn !== void 0) {
+ if (!('defaultValue' in this) || fn != null) {
this.default(function() {
let arr = fn.call(this);
if (arr != null && !Array.isArray(arr)) {
@@ -92,7 +98,7 @@ function DocumentArrayPath(key, schema, options, schemaOptions) {
*
* @api public
*/
-DocumentArrayPath.schemaName = 'DocumentArray';
+SchemaDocumentArray.schemaName = 'DocumentArray';
/**
* Options for all document arrays.
@@ -102,21 +108,22 @@ DocumentArrayPath.schemaName = 'DocumentArray';
* @api public
*/
-DocumentArrayPath.options = { castNonArrays: true };
+SchemaDocumentArray.options = { castNonArrays: true };
/*!
- * Inherits from ArrayType.
+ * Inherits from SchemaArray.
*/
-DocumentArrayPath.prototype = Object.create(ArrayType.prototype);
-DocumentArrayPath.prototype.constructor = DocumentArrayPath;
-DocumentArrayPath.prototype.OptionsConstructor = SchemaDocumentArrayOptions;
+SchemaDocumentArray.prototype = Object.create(SchemaArray.prototype);
+SchemaDocumentArray.prototype.constructor = SchemaDocumentArray;
+SchemaDocumentArray.prototype.OptionsConstructor = SchemaDocumentArrayOptions;
+SchemaDocumentArray.prototype.$conditionalHandlers = { ...SchemaArray.prototype.$conditionalHandlers };
/*!
* ignore
*/
function _createConstructor(schema, options, baseClass) {
- Subdocument || (Subdocument = require('../types/ArraySubdocument'));
+ Subdocument || (Subdocument = require('../types/arraySubdocument'));
// compile an embedded document for this schema
function EmbeddedDocument() {
@@ -173,12 +180,12 @@ function _createConstructor(schema, options, baseClass) {
* @param {Object|string} [options] If string, same as `options.value`.
* @param {String} [options.value] the string stored in the `discriminatorKey` property. If not specified, Mongoose uses the `name` parameter.
* @param {Boolean} [options.clone=true] By default, `discriminator()` clones the given `schema`. Set to `false` to skip cloning.
- * @see discriminators /docs/discriminators.html
+ * @see discriminators https://mongoosejs.com/docs/discriminators.html
* @return {Function} the constructor Mongoose will use for creating instances of this discriminator model
* @api public
*/
-DocumentArrayPath.prototype.discriminator = function(name, schema, options) {
+SchemaDocumentArray.prototype.discriminator = function(name, schema, options) {
if (typeof name === 'function') {
name = utils.getFunctionName(name);
}
@@ -191,7 +198,7 @@ DocumentArrayPath.prototype.discriminator = function(name, schema, options) {
schema = schema.clone();
}
- schema = discriminator(this.casterConstructor, name, schema, tiedValue);
+ schema = discriminator(this.casterConstructor, name, schema, tiedValue, null, null, options?.overwriteExisting);
const EmbeddedDocument = _createConstructor(schema, null, this.casterConstructor);
EmbeddedDocument.baseCasterConstructor = this.casterConstructor;
@@ -215,9 +222,9 @@ DocumentArrayPath.prototype.discriminator = function(name, schema, options) {
* @api private
*/
-DocumentArrayPath.prototype.doValidate = function(array, fn, scope, options) {
+SchemaDocumentArray.prototype.doValidate = function(array, fn, scope, options) {
// lazy load
- MongooseDocumentArray || (MongooseDocumentArray = require('../types/DocumentArray'));
+ MongooseDocumentArray || (MongooseDocumentArray = require('../types/documentArray'));
const _this = this;
try {
@@ -275,7 +282,7 @@ DocumentArrayPath.prototype.doValidate = function(array, fn, scope, options) {
continue;
}
- doc.$__validate(callback);
+ doc.$__validate(null, options, callback);
}
}
};
@@ -291,7 +298,7 @@ DocumentArrayPath.prototype.doValidate = function(array, fn, scope, options) {
* @api private
*/
-DocumentArrayPath.prototype.doValidateSync = function(array, scope, options) {
+SchemaDocumentArray.prototype.doValidateSync = function(array, scope, options) {
const schemaTypeError = SchemaType.prototype.doValidateSync.call(this, array, scope);
if (schemaTypeError != null) {
return schemaTypeError;
@@ -326,7 +333,7 @@ DocumentArrayPath.prototype.doValidateSync = function(array, scope, options) {
continue;
}
- const subdocValidateError = doc.validateSync();
+ const subdocValidateError = doc.validateSync(options);
if (subdocValidateError && resultError == null) {
resultError = subdocValidateError;
@@ -340,7 +347,7 @@ DocumentArrayPath.prototype.doValidateSync = function(array, scope, options) {
* ignore
*/
-DocumentArrayPath.prototype.getDefault = function(scope, init, options) {
+SchemaDocumentArray.prototype.getDefault = function(scope, init, options) {
let ret = typeof this.defaultValue === 'function'
? this.defaultValue.call(scope)
: this.defaultValue;
@@ -354,7 +361,7 @@ DocumentArrayPath.prototype.getDefault = function(scope, init, options) {
}
// lazy load
- MongooseDocumentArray || (MongooseDocumentArray = require('../types/DocumentArray'));
+ MongooseDocumentArray || (MongooseDocumentArray = require('../types/documentArray'));
if (!Array.isArray(ret)) {
ret = [ret];
@@ -391,9 +398,9 @@ const initDocumentOptions = Object.freeze({ skipId: false, willInit: true });
* @api private
*/
-DocumentArrayPath.prototype.cast = function(value, doc, init, prev, options) {
+SchemaDocumentArray.prototype.cast = function(value, doc, init, prev, options) {
// lazy load
- MongooseDocumentArray || (MongooseDocumentArray = require('../types/DocumentArray'));
+ MongooseDocumentArray || (MongooseDocumentArray = require('../types/documentArray'));
// Skip casting if `value` is the same as the previous value, no need to cast. See gh-9266
if (value != null && value[arrayPathSymbol] != null && value === prev) {
@@ -408,7 +415,7 @@ DocumentArrayPath.prototype.cast = function(value, doc, init, prev, options) {
const path = options.path || this.path;
if (!Array.isArray(value)) {
- if (!init && !DocumentArrayPath.options.castNonArrays) {
+ if (!init && !SchemaDocumentArray.options.castNonArrays) {
throw new CastError('DocumentArray', value, this.path, null, this);
}
// gh-2442 mark whole array as modified if we're initializing a doc from
@@ -422,7 +429,7 @@ DocumentArrayPath.prototype.cast = function(value, doc, init, prev, options) {
// We need to create a new array, otherwise change tracking will
// update the old doc (gh-4449)
if (!options.skipDocumentArrayCast || utils.isMongooseDocumentArray(value)) {
- value = new MongooseDocumentArray(value, path, doc);
+ value = new MongooseDocumentArray(value, path, doc, this);
}
if (prev != null) {
@@ -443,19 +450,9 @@ DocumentArrayPath.prototype.cast = function(value, doc, init, prev, options) {
const Constructor = getConstructor(this.casterConstructor, rawArray[i]);
- // Check if the document has a different schema (re gh-3701)
- if (rawArray[i].$__ != null && !(rawArray[i] instanceof Constructor)) {
- const spreadDoc = handleSpreadDoc(rawArray[i], true);
- if (rawArray[i] !== spreadDoc) {
- rawArray[i] = spreadDoc;
- } else {
- rawArray[i] = rawArray[i].toObject({
- transform: false,
- // Special case: if different model, but same schema, apply virtuals
- // re: gh-7898
- virtuals: rawArray[i].schema === Constructor.schema
- });
- }
+ const spreadDoc = handleSpreadDoc(rawArray[i], true);
+ if (rawArray[i] !== spreadDoc) {
+ rawArray[i] = spreadDoc;
}
if (rawArray[i] instanceof Subdocument) {
@@ -516,7 +513,7 @@ DocumentArrayPath.prototype.cast = function(value, doc, init, prev, options) {
* ignore
*/
-DocumentArrayPath.prototype.clone = function() {
+SchemaDocumentArray.prototype.clone = function() {
const options = Object.assign({}, this.options);
const schematype = new this.constructor(this.path, this.schema, options, this.schemaOptions);
schematype.validators = this.validators.slice();
@@ -525,6 +522,7 @@ DocumentArrayPath.prototype.clone = function() {
}
schematype.Constructor.discriminators = Object.assign({},
this.Constructor.discriminators);
+ schematype._appliedDiscriminators = this._appliedDiscriminators;
return schematype;
};
@@ -532,7 +530,7 @@ DocumentArrayPath.prototype.clone = function() {
* ignore
*/
-DocumentArrayPath.prototype.applyGetters = function(value, scope) {
+SchemaDocumentArray.prototype.applyGetters = function(value, scope) {
return SchemaType.prototype.applyGetters.call(this, value, scope);
};
@@ -581,7 +579,7 @@ function scopePaths(array, fields, init) {
* ignore
*/
-DocumentArrayPath.defaultOptions = {};
+SchemaDocumentArray.defaultOptions = {};
/**
* Sets a default option for all DocumentArray instances.
@@ -599,10 +597,62 @@ DocumentArrayPath.defaultOptions = {};
* @api public
*/
-DocumentArrayPath.set = SchemaType.set;
+SchemaDocumentArray.set = SchemaType.set;
+
+SchemaDocumentArray.setters = [];
+
+/**
+ * Attaches a getter for all DocumentArrayPath instances
+ *
+ * @param {Function} getter
+ * @return {this}
+ * @function get
+ * @static
+ * @api public
+ */
+
+SchemaDocumentArray.get = SchemaType.get;
+
+/*!
+ * Handle casting $elemMatch operators
+ */
+
+SchemaDocumentArray.prototype.$conditionalHandlers.$elemMatch = cast$elemMatch;
+
+function cast$elemMatch(val, context) {
+ const keys = Object.keys(val);
+ const numKeys = keys.length;
+ for (let i = 0; i < numKeys; ++i) {
+ const key = keys[i];
+ const value = val[key];
+ if (isOperator(key) && value != null) {
+ val[key] = this.castForQuery(key, value, context);
+ }
+ }
+
+ // Is this an embedded discriminator and is the discriminator key set?
+ // If so, use the discriminator schema. See gh-7449
+ const discriminatorKey = this &&
+ this.casterConstructor &&
+ this.casterConstructor.schema &&
+ this.casterConstructor.schema.options &&
+ this.casterConstructor.schema.options.discriminatorKey;
+ const discriminators = this &&
+ this.casterConstructor &&
+ this.casterConstructor.schema &&
+ this.casterConstructor.schema.discriminators || {};
+ if (discriminatorKey != null &&
+ val[discriminatorKey] != null &&
+ discriminators[val[discriminatorKey]] != null) {
+ return cast(discriminators[val[discriminatorKey]], val, null, this && this.$$context);
+ }
+
+ const schema = this.casterConstructor.schema ?? context.schema;
+ return cast(schema, val, null, this && this.$$context);
+}
/*!
* Module exports.
*/
-module.exports = DocumentArrayPath;
+module.exports = SchemaDocumentArray;
diff --git a/lib/schema/DocumentArrayElement.js b/lib/schema/documentArrayElement.js
similarity index 69%
rename from lib/schema/DocumentArrayElement.js
rename to lib/schema/documentArrayElement.js
index a2fb9a44a23..5250b74b505 100644
--- a/lib/schema/DocumentArrayElement.js
+++ b/lib/schema/documentArrayElement.js
@@ -5,8 +5,8 @@
'use strict';
const MongooseError = require('../error/mongooseError');
-const SchemaType = require('../schematype');
-const SubdocumentPath = require('./SubdocumentPath');
+const SchemaType = require('../schemaType');
+const SchemaSubdocument = require('./subdocument');
const getConstructor = require('../helpers/discriminator/getConstructor');
/**
@@ -18,7 +18,7 @@ const getConstructor = require('../helpers/discriminator/getConstructor');
* @api public
*/
-function DocumentArrayElement(path, options) {
+function SchemaDocumentArrayElement(path, options) {
this.$parentSchemaType = options && options.$parentSchemaType;
if (!this.$parentSchemaType) {
throw new MongooseError('Cannot create DocumentArrayElement schematype without a parent');
@@ -36,15 +36,15 @@ function DocumentArrayElement(path, options) {
*
* @api public
*/
-DocumentArrayElement.schemaName = 'DocumentArrayElement';
+SchemaDocumentArrayElement.schemaName = 'DocumentArrayElement';
-DocumentArrayElement.defaultOptions = {};
+SchemaDocumentArrayElement.defaultOptions = {};
/*!
* Inherits from SchemaType.
*/
-DocumentArrayElement.prototype = Object.create(SchemaType.prototype);
-DocumentArrayElement.prototype.constructor = DocumentArrayElement;
+SchemaDocumentArrayElement.prototype = Object.create(SchemaType.prototype);
+SchemaDocumentArrayElement.prototype.constructor = SchemaDocumentArrayElement;
/**
* Casts `val` for DocumentArrayElement.
@@ -53,7 +53,7 @@ DocumentArrayElement.prototype.constructor = DocumentArrayElement;
* @api private
*/
-DocumentArrayElement.prototype.cast = function(...args) {
+SchemaDocumentArrayElement.prototype.cast = function(...args) {
return this.$parentSchemaType.cast(...args)[0];
};
@@ -65,14 +65,14 @@ DocumentArrayElement.prototype.cast = function(...args) {
* @api private
*/
-DocumentArrayElement.prototype.doValidate = function(value, fn, scope, options) {
+SchemaDocumentArrayElement.prototype.doValidate = function(value, fn, scope, options) {
const Constructor = getConstructor(this.caster, value);
if (value && !(value instanceof Constructor)) {
value = new Constructor(value, scope, null, null, options && options.index != null ? options.index : null);
}
- return SubdocumentPath.prototype.doValidate.call(this, value, fn, scope, options);
+ return SchemaSubdocument.prototype.doValidate.call(this, value, fn, scope, options);
};
/**
@@ -82,7 +82,7 @@ DocumentArrayElement.prototype.doValidate = function(value, fn, scope, options)
* @api private
*/
-DocumentArrayElement.prototype.clone = function() {
+SchemaDocumentArrayElement.prototype.clone = function() {
this.options.$parentSchemaType = this.$parentSchemaType;
const ret = SchemaType.prototype.clone.apply(this, arguments);
delete this.options.$parentSchemaType;
@@ -97,4 +97,4 @@ DocumentArrayElement.prototype.clone = function() {
* Module exports.
*/
-module.exports = DocumentArrayElement;
+module.exports = SchemaDocumentArrayElement;
diff --git a/lib/schema/double.js b/lib/schema/double.js
new file mode 100644
index 00000000000..79c94752184
--- /dev/null
+++ b/lib/schema/double.js
@@ -0,0 +1,212 @@
+'use strict';
+
+/*!
+ * Module dependencies.
+ */
+
+const CastError = require('../error/cast');
+const SchemaType = require('../schemaType');
+const castDouble = require('../cast/double');
+
+/**
+ * Double SchemaType constructor.
+ *
+ * @param {String} path
+ * @param {Object} options
+ * @inherits SchemaType
+ * @api public
+ */
+
+function SchemaDouble(path, options) {
+ SchemaType.call(this, path, options, 'Double');
+}
+
+/**
+ * This schema type's name, to defend against minifiers that mangle
+ * function names.
+ *
+ * @api public
+ */
+SchemaDouble.schemaName = 'Double';
+
+SchemaDouble.defaultOptions = {};
+
+/*!
+ * Inherits from SchemaType.
+ */
+SchemaDouble.prototype = Object.create(SchemaType.prototype);
+SchemaDouble.prototype.constructor = SchemaDouble;
+
+/*!
+ * ignore
+ */
+
+SchemaDouble._cast = castDouble;
+
+/**
+ * Sets a default option for all Double instances.
+ *
+ * #### Example:
+ *
+ * // Make all Double fields required by default
+ * mongoose.Schema.Double.set('required', true);
+ *
+ * @param {String} option The option you'd like to set the value for
+ * @param {Any} value value for option
+ * @return {undefined}
+ * @function set
+ * @static
+ * @api public
+ */
+
+SchemaDouble.set = SchemaType.set;
+
+SchemaDouble.setters = [];
+
+/**
+ * Attaches a getter for all Double instances
+ *
+ * #### Example:
+ *
+ * // Converts Double to be a represent milliseconds upon access
+ * mongoose.Schema.Double.get(v => v == null ? '0.000 ms' : v.toString() + ' ms');
+ *
+ * @param {Function} getter
+ * @return {this}
+ * @function get
+ * @static
+ * @api public
+ */
+
+SchemaDouble.get = SchemaType.get;
+
+/*!
+ * ignore
+ */
+
+SchemaDouble._defaultCaster = v => {
+ if (v != null) {
+ if (v._bsontype !== 'Double') {
+ throw new Error();
+ }
+ }
+
+ return v;
+};
+
+/**
+ * Get/set the function used to cast arbitrary values to IEEE 754-2008 floating points
+ *
+ * #### Example:
+ *
+ * // Make Mongoose cast any NaNs to 0
+ * const defaultCast = mongoose.Schema.Types.Double.cast();
+ * mongoose.Schema.Types.Double.cast(v => {
+ * if (isNaN(v)) {
+ * return 0;
+ * }
+ * return defaultCast(v);
+ * });
+ *
+ * // Or disable casting for Doubles entirely (only JS numbers are permitted)
+ * mongoose.Schema.Double.cast(false);
+ *
+ *
+ * @param {Function} caster
+ * @return {Function}
+ * @function get
+ * @static
+ * @api public
+ */
+
+SchemaDouble.cast = function cast(caster) {
+ if (arguments.length === 0) {
+ return this._cast;
+ }
+ if (caster === false) {
+ caster = this._defaultCaster;
+ }
+
+ this._cast = caster;
+
+ return this._cast;
+};
+
+
+/*!
+ * ignore
+ */
+
+SchemaDouble._checkRequired = v => v != null;
+/**
+ * Override the function the required validator uses to check whether a value
+ * passes the `required` check.
+ *
+ * @param {Function} fn
+ * @return {Function}
+ * @function checkRequired
+ * @static
+ * @api public
+ */
+
+SchemaDouble.checkRequired = SchemaType.checkRequired;
+
+/**
+ * Check if the given value satisfies a required validator.
+ *
+ * @param {Any} value
+ * @return {Boolean}
+ * @api public
+ */
+
+SchemaDouble.prototype.checkRequired = function(value) {
+ return this.constructor._checkRequired(value);
+};
+
+/**
+ * Casts to Double
+ *
+ * @param {Object} value
+ * @param {Object} model this value is optional
+ * @api private
+ */
+
+SchemaDouble.prototype.cast = function(value) {
+ let castDouble;
+ if (typeof this._castFunction === 'function') {
+ castDouble = this._castFunction;
+ } else if (typeof this.constructor.cast === 'function') {
+ castDouble = this.constructor.cast();
+ } else {
+ castDouble = SchemaDouble.cast();
+ }
+
+ try {
+ return castDouble(value);
+ } catch (error) {
+ throw new CastError('Double', value, this.path, error, this);
+ }
+};
+
+/*!
+ * ignore
+ */
+
+function handleSingle(val) {
+ return this.cast(val);
+}
+
+SchemaDouble.prototype.$conditionalHandlers = {
+ ...SchemaType.prototype.$conditionalHandlers,
+ $gt: handleSingle,
+ $gte: handleSingle,
+ $lt: handleSingle,
+ $lte: handleSingle
+};
+
+
+/*!
+ * Module exports.
+ */
+
+module.exports = SchemaDouble;
diff --git a/lib/schema/index.js b/lib/schema/index.js
index f3eb9851ea6..2d2e99211d1 100644
--- a/lib/schema/index.js
+++ b/lib/schema/index.js
@@ -5,31 +5,22 @@
'use strict';
-exports.String = require('./string');
-
-exports.Number = require('./number');
-
-exports.Boolean = require('./boolean');
-
-exports.DocumentArray = require('./documentarray');
-
-exports.Subdocument = require('./SubdocumentPath');
-
exports.Array = require('./array');
-
+exports.BigInt = require('./bigint');
+exports.Boolean = require('./boolean');
exports.Buffer = require('./buffer');
-
exports.Date = require('./date');
-
-exports.ObjectId = require('./objectid');
-
-exports.Mixed = require('./mixed');
-
exports.Decimal128 = exports.Decimal = require('./decimal128');
-
+exports.DocumentArray = require('./documentArray');
exports.Map = require('./map');
-
+exports.Mixed = require('./mixed');
+exports.Number = require('./number');
+exports.ObjectId = require('./objectId');
+exports.String = require('./string');
+exports.Subdocument = require('./subdocument');
exports.UUID = require('./uuid');
+exports.Double = require('./double');
+exports.Int32 = require('./int32');
// alias
diff --git a/lib/schema/int32.js b/lib/schema/int32.js
new file mode 100644
index 00000000000..6838d22f2bb
--- /dev/null
+++ b/lib/schema/int32.js
@@ -0,0 +1,254 @@
+'use strict';
+
+/*!
+ * Module dependencies.
+ */
+
+const CastError = require('../error/cast');
+const SchemaType = require('../schemaType');
+const castInt32 = require('../cast/int32');
+const handleBitwiseOperator = require('./operators/bitwise');
+
+/**
+ * Int32 SchemaType constructor.
+ *
+ * @param {String} path
+ * @param {Object} options
+ * @inherits SchemaType
+ * @api public
+ */
+
+function SchemaInt32(path, options) {
+ SchemaType.call(this, path, options, 'Int32');
+}
+
+/**
+ * This schema type's name, to defend against minifiers that mangle
+ * function names.
+ *
+ * @api public
+ */
+SchemaInt32.schemaName = 'Int32';
+
+SchemaInt32.defaultOptions = {};
+
+/*!
+ * Inherits from SchemaType.
+ */
+SchemaInt32.prototype = Object.create(SchemaType.prototype);
+SchemaInt32.prototype.constructor = SchemaInt32;
+
+/*!
+ * ignore
+ */
+
+SchemaInt32._cast = castInt32;
+
+/**
+ * Sets a default option for all Int32 instances.
+ *
+ * #### Example:
+ *
+ * // Make all Int32 fields required by default
+ * mongoose.Schema.Int32.set('required', true);
+ *
+ * @param {String} option The option you'd like to set the value for
+ * @param {Any} value value for option
+ * @return {undefined}
+ * @function set
+ * @static
+ * @api public
+ */
+
+SchemaInt32.set = SchemaType.set;
+
+SchemaInt32.setters = [];
+
+/**
+ * Attaches a getter for all Int32 instances
+ *
+ * #### Example:
+ *
+ * // Converts int32 to be a represent milliseconds upon access
+ * mongoose.Schema.Int32.get(v => v == null ? '0 ms' : v.toString() + ' ms');
+ *
+ * @param {Function} getter
+ * @return {this}
+ * @function get
+ * @static
+ * @api public
+ */
+
+SchemaInt32.get = SchemaType.get;
+
+/*!
+ * ignore
+ */
+
+SchemaInt32._defaultCaster = v => {
+ const INT32_MAX = 0x7FFFFFFF;
+ const INT32_MIN = -0x80000000;
+
+ if (v != null) {
+ if (typeof v !== 'number' || v !== (v | 0) || v < INT32_MIN || v > INT32_MAX) {
+ throw new Error();
+ }
+ }
+
+ return v;
+};
+
+/**
+ * Get/set the function used to cast arbitrary values to 32-bit integers
+ *
+ * #### Example:
+ *
+ * // Make Mongoose cast NaN to 0
+ * const defaultCast = mongoose.Schema.Types.Int32.cast();
+ * mongoose.Schema.Types.Int32.cast(v => {
+ * if (isNaN(v)) {
+ * return 0;
+ * }
+ * return defaultCast(v);
+ * });
+ *
+ * // Or disable casting for Int32s entirely (only JS numbers within 32-bit integer bounds and null-ish values are permitted)
+ * mongoose.Schema.Int32.cast(false);
+ *
+ *
+ * @param {Function} caster
+ * @return {Function}
+ * @function get
+ * @static
+ * @api public
+ */
+
+SchemaInt32.cast = function cast(caster) {
+ if (arguments.length === 0) {
+ return this._cast;
+ }
+ if (caster === false) {
+ caster = this._defaultCaster;
+ }
+
+ this._cast = caster;
+
+ return this._cast;
+};
+
+
+/*!
+ * ignore
+ */
+
+SchemaInt32._checkRequired = v => v != null;
+/**
+ * Override the function the required validator uses to check whether a value
+ * passes the `required` check.
+ *
+ * @param {Function} fn
+ * @return {Function}
+ * @function checkRequired
+ * @static
+ * @api public
+ */
+
+SchemaInt32.checkRequired = SchemaType.checkRequired;
+
+/**
+ * Check if the given value satisfies a required validator.
+ *
+ * @param {Any} value
+ * @return {Boolean}
+ * @api public
+ */
+
+SchemaInt32.prototype.checkRequired = function(value) {
+ return this.constructor._checkRequired(value);
+};
+
+/**
+ * Casts to Int32
+ *
+ * @param {Object} value
+ * @param {Object} model this value is optional
+ * @api private
+ */
+
+SchemaInt32.prototype.cast = function(value) {
+ let castInt32;
+ if (typeof this._castFunction === 'function') {
+ castInt32 = this._castFunction;
+ } else if (typeof this.constructor.cast === 'function') {
+ castInt32 = this.constructor.cast();
+ } else {
+ castInt32 = SchemaInt32.cast();
+ }
+
+ try {
+ return castInt32(value);
+ } catch (error) {
+ throw new CastError('Int32', value, this.path, error, this);
+ }
+};
+
+/*!
+ * ignore
+ */
+
+SchemaInt32.$conditionalHandlers = {
+ ...SchemaType.prototype.$conditionalHandlers,
+ $gt: handleSingle,
+ $gte: handleSingle,
+ $lt: handleSingle,
+ $lte: handleSingle,
+ $bitsAllClear: handleBitwiseOperator,
+ $bitsAnyClear: handleBitwiseOperator,
+ $bitsAllSet: handleBitwiseOperator,
+ $bitsAnySet: handleBitwiseOperator
+};
+
+/*!
+ * ignore
+ */
+
+function handleSingle(val, context) {
+ return this.castForQuery(null, val, context);
+}
+
+/**
+ * Casts contents for queries.
+ *
+ * @param {String} $conditional
+ * @param {any} val
+ * @api private
+ */
+
+SchemaInt32.prototype.castForQuery = function($conditional, val, context) {
+ let handler;
+ if ($conditional != null) {
+ handler = SchemaInt32.$conditionalHandlers[$conditional];
+
+ if (handler) {
+ return handler.call(this, val);
+ }
+
+ return this.applySetters(val, context);
+ }
+
+ try {
+ return this.applySetters(val, context);
+ } catch (err) {
+ if (err instanceof CastError && err.path === this.path && this.$fullPath != null) {
+ err.path = this.$fullPath;
+ }
+ throw err;
+ }
+};
+
+
+/*!
+ * Module exports.
+ */
+
+module.exports = SchemaInt32;
diff --git a/lib/schema/map.js b/lib/schema/map.js
index ce25a11f568..1c7c41ae900 100644
--- a/lib/schema/map.js
+++ b/lib/schema/map.js
@@ -5,13 +5,13 @@
*/
const MongooseMap = require('../types/map');
-const SchemaMapOptions = require('../options/SchemaMapOptions');
-const SchemaType = require('../schematype');
+const SchemaMapOptions = require('../options/schemaMapOptions');
+const SchemaType = require('../schemaType');
/*!
* ignore
*/
-class Map extends SchemaType {
+class SchemaMap extends SchemaType {
constructor(key, options) {
super(key, options, 'Map');
this.$isSchemaMap = true;
@@ -75,10 +75,10 @@ class Map extends SchemaType {
*
* @api public
*/
-Map.schemaName = 'Map';
+SchemaMap.schemaName = 'Map';
-Map.prototype.OptionsConstructor = SchemaMapOptions;
+SchemaMap.prototype.OptionsConstructor = SchemaMapOptions;
-Map.defaultOptions = {};
+SchemaMap.defaultOptions = {};
-module.exports = Map;
+module.exports = SchemaMap;
diff --git a/lib/schema/mixed.js b/lib/schema/mixed.js
index 6fbaba094b6..a645a981f7b 100644
--- a/lib/schema/mixed.js
+++ b/lib/schema/mixed.js
@@ -4,7 +4,7 @@
'use strict';
-const SchemaType = require('../schematype');
+const SchemaType = require('../schemaType');
const symbols = require('./symbols');
const isObject = require('../helpers/isObject');
const utils = require('../utils');
@@ -18,7 +18,7 @@ const utils = require('../utils');
* @api public
*/
-function Mixed(path, options) {
+function SchemaMixed(path, options) {
if (options && options.default) {
const def = options.default;
if (Array.isArray(def) && def.length === 0) {
@@ -43,15 +43,15 @@ function Mixed(path, options) {
*
* @api public
*/
-Mixed.schemaName = 'Mixed';
+SchemaMixed.schemaName = 'Mixed';
-Mixed.defaultOptions = {};
+SchemaMixed.defaultOptions = {};
/*!
* Inherits from SchemaType.
*/
-Mixed.prototype = Object.create(SchemaType.prototype);
-Mixed.prototype.constructor = Mixed;
+SchemaMixed.prototype = Object.create(SchemaType.prototype);
+SchemaMixed.prototype.constructor = SchemaMixed;
/**
* Attaches a getter for all Mixed paths.
@@ -71,7 +71,7 @@ Mixed.prototype.constructor = Mixed;
* @api public
*/
-Mixed.get = SchemaType.get;
+SchemaMixed.get = SchemaType.get;
/**
* Sets a default option for all Mixed instances.
@@ -92,7 +92,9 @@ Mixed.get = SchemaType.get;
* @api public
*/
-Mixed.set = SchemaType.set;
+SchemaMixed.set = SchemaType.set;
+
+SchemaMixed.setters = [];
/**
* Casts `val` for Mixed.
@@ -103,7 +105,7 @@ Mixed.set = SchemaType.set;
* @api private
*/
-Mixed.prototype.cast = function(val) {
+SchemaMixed.prototype.cast = function(val) {
if (val instanceof Error) {
return utils.errorToPOJO(val);
}
@@ -118,15 +120,12 @@ Mixed.prototype.cast = function(val) {
* @api private
*/
-Mixed.prototype.castForQuery = function($cond, val) {
- if (arguments.length === 2) {
- return val;
- }
- return $cond;
+SchemaMixed.prototype.castForQuery = function($cond, val) {
+ return val;
};
/*!
* Module exports.
*/
-module.exports = Mixed;
+module.exports = SchemaMixed;
diff --git a/lib/schema/number.js b/lib/schema/number.js
index bbee66df799..d89ab7d63c0 100644
--- a/lib/schema/number.js
+++ b/lib/schema/number.js
@@ -5,8 +5,8 @@
*/
const MongooseError = require('../error/index');
-const SchemaNumberOptions = require('../options/SchemaNumberOptions');
-const SchemaType = require('../schematype');
+const SchemaNumberOptions = require('../options/schemaNumberOptions');
+const SchemaType = require('../schemaType');
const castNumber = require('../cast/number');
const handleBitwiseOperator = require('./operators/bitwise');
const utils = require('../utils');
@@ -67,6 +67,8 @@ SchemaNumber.get = SchemaType.get;
SchemaNumber.set = SchemaType.set;
+SchemaNumber.setters = [];
+
/*!
* ignore
*/
@@ -204,7 +206,7 @@ SchemaNumber.prototype.checkRequired = function checkRequired(value, doc) {
* @param {Number} value minimum number
* @param {String} [message] optional custom error message
* @return {SchemaType} this
- * @see Customized Error Messages #error_messages_MongooseError-messages
+ * @see Customized Error Messages https://mongoosejs.com/docs/api/error.html#Error.messages
* @api public
*/
@@ -258,7 +260,7 @@ SchemaNumber.prototype.min = function(value, message) {
* @param {Number} maximum number
* @param {String} [message] optional custom error message
* @return {SchemaType} this
- * @see Customized Error Messages #error_messages_MongooseError-messages
+ * @see Customized Error Messages https://mongoosejs.com/docs/api/error.html#Error.messages
* @api public
*/
@@ -302,7 +304,7 @@ SchemaNumber.prototype.max = function(value, message) {
* @param {Array} values allowed values
* @param {String} [message] optional custom error message
* @return {SchemaType} this
- * @see Customized Error Messages #error_messages_MongooseError-messages
+ * @see Customized Error Messages https://mongoosejs.com/docs/api/error.html#Error.messages
* @api public
*/
@@ -397,18 +399,18 @@ function handleArray(val) {
});
}
-SchemaNumber.prototype.$conditionalHandlers =
- utils.options(SchemaType.prototype.$conditionalHandlers, {
- $bitsAllClear: handleBitwiseOperator,
- $bitsAnyClear: handleBitwiseOperator,
- $bitsAllSet: handleBitwiseOperator,
- $bitsAnySet: handleBitwiseOperator,
- $gt: handleSingle,
- $gte: handleSingle,
- $lt: handleSingle,
- $lte: handleSingle,
- $mod: handleArray
- });
+SchemaNumber.prototype.$conditionalHandlers = {
+ ...SchemaType.prototype.$conditionalHandlers,
+ $bitsAllClear: handleBitwiseOperator,
+ $bitsAnyClear: handleBitwiseOperator,
+ $bitsAllSet: handleBitwiseOperator,
+ $bitsAnySet: handleBitwiseOperator,
+ $gt: handleSingle,
+ $gte: handleSingle,
+ $lt: handleSingle,
+ $lte: handleSingle,
+ $mod: handleArray
+};
/**
* Casts contents for queries.
@@ -418,16 +420,25 @@ SchemaNumber.prototype.$conditionalHandlers =
* @api private
*/
-SchemaNumber.prototype.castForQuery = function($conditional, val) {
+SchemaNumber.prototype.castForQuery = function($conditional, val, context) {
let handler;
- if (arguments.length === 2) {
+ if ($conditional != null) {
handler = this.$conditionalHandlers[$conditional];
if (!handler) {
throw new CastError('number', val, this.path, null, this);
}
- return handler.call(this, val);
+ return handler.call(this, val, context);
}
- val = this._castForQuery($conditional);
+
+ try {
+ val = this.applySetters(val, context);
+ } catch (err) {
+ if (err instanceof CastError && err.path === this.path && this.$fullPath != null) {
+ err.path = this.$fullPath;
+ }
+ throw err;
+ }
+
return val;
};
diff --git a/lib/schema/objectid.js b/lib/schema/objectId.js
similarity index 79%
rename from lib/schema/objectid.js
rename to lib/schema/objectId.js
index 56d09ed37d1..cad05198ea8 100644
--- a/lib/schema/objectid.js
+++ b/lib/schema/objectId.js
@@ -4,8 +4,8 @@
'use strict';
-const SchemaObjectIdOptions = require('../options/SchemaObjectIdOptions');
-const SchemaType = require('../schematype');
+const SchemaObjectIdOptions = require('../options/schemaObjectIdOptions');
+const SchemaType = require('../schemaType');
const castObjectId = require('../cast/objectid');
const getConstructorName = require('../helpers/getConstructorName');
const oid = require('../types/objectid');
@@ -24,7 +24,7 @@ let Document;
* @api public
*/
-function ObjectId(key, options) {
+function SchemaObjectId(key, options) {
const isKeyHexStr = typeof key === 'string' && key.length === 24 && /^[a-f0-9]+$/i.test(key);
const suppressWarning = options && options.suppressWarning;
if ((isKeyHexStr || typeof key === 'undefined') && !suppressWarning) {
@@ -33,7 +33,7 @@ function ObjectId(key, options) {
'`Mongoose.Schema.ObjectId`. Set the `suppressWarning` option if ' +
'you\'re trying to create a hex char path in your schema.');
}
- SchemaType.call(this, key, options, 'ObjectID');
+ SchemaType.call(this, key, options, 'ObjectId');
}
/**
@@ -42,16 +42,16 @@ function ObjectId(key, options) {
*
* @api public
*/
-ObjectId.schemaName = 'ObjectId';
+SchemaObjectId.schemaName = 'ObjectId';
-ObjectId.defaultOptions = {};
+SchemaObjectId.defaultOptions = {};
/*!
* Inherits from SchemaType.
*/
-ObjectId.prototype = Object.create(SchemaType.prototype);
-ObjectId.prototype.constructor = ObjectId;
-ObjectId.prototype.OptionsConstructor = SchemaObjectIdOptions;
+SchemaObjectId.prototype = Object.create(SchemaType.prototype);
+SchemaObjectId.prototype.constructor = SchemaObjectId;
+SchemaObjectId.prototype.OptionsConstructor = SchemaObjectIdOptions;
/**
* Attaches a getter for all ObjectId instances
@@ -71,7 +71,7 @@ ObjectId.prototype.OptionsConstructor = SchemaObjectIdOptions;
* @api public
*/
-ObjectId.get = SchemaType.get;
+SchemaObjectId.get = SchemaType.get;
/**
* Sets a default option for all ObjectId instances.
@@ -92,7 +92,9 @@ ObjectId.get = SchemaType.get;
* @api public
*/
-ObjectId.set = SchemaType.set;
+SchemaObjectId.set = SchemaType.set;
+
+SchemaObjectId.setters = [];
/**
* Adds an auto-generated ObjectId default if turnOn is true.
@@ -101,7 +103,7 @@ ObjectId.set = SchemaType.set;
* @return {SchemaType} this
*/
-ObjectId.prototype.auto = function(turnOn) {
+SchemaObjectId.prototype.auto = function(turnOn) {
if (turnOn) {
this.default(defaultId);
this.set(resetId);
@@ -114,13 +116,13 @@ ObjectId.prototype.auto = function(turnOn) {
* ignore
*/
-ObjectId._checkRequired = v => isBsonType(v, 'ObjectID');
+SchemaObjectId._checkRequired = v => isBsonType(v, 'ObjectId');
/*!
* ignore
*/
-ObjectId._cast = castObjectId;
+SchemaObjectId._cast = castObjectId;
/**
* Get/set the function used to cast arbitrary values to objectids.
@@ -145,7 +147,7 @@ ObjectId._cast = castObjectId;
* @api public
*/
-ObjectId.cast = function cast(caster) {
+SchemaObjectId.cast = function cast(caster) {
if (arguments.length === 0) {
return this._cast;
}
@@ -161,8 +163,8 @@ ObjectId.cast = function cast(caster) {
* ignore
*/
-ObjectId._defaultCaster = v => {
- if (!(isBsonType(v, 'ObjectID'))) {
+SchemaObjectId._defaultCaster = v => {
+ if (!(isBsonType(v, 'ObjectId'))) {
throw new Error(v + ' is not an instance of ObjectId');
}
return v;
@@ -187,7 +189,7 @@ ObjectId._defaultCaster = v => {
* @api public
*/
-ObjectId.checkRequired = SchemaType.checkRequired;
+SchemaObjectId.checkRequired = SchemaType.checkRequired;
/**
* Check if the given value satisfies a required validator.
@@ -198,7 +200,7 @@ ObjectId.checkRequired = SchemaType.checkRequired;
* @api public
*/
-ObjectId.prototype.checkRequired = function checkRequired(value, doc) {
+SchemaObjectId.prototype.checkRequired = function checkRequired(value, doc) {
if (SchemaType._isRef(this, value, doc, true)) {
return !!value;
}
@@ -207,7 +209,7 @@ ObjectId.prototype.checkRequired = function checkRequired(value, doc) {
// plugins like mongoose-float use `inherits()` for pre-ES6.
const _checkRequired = typeof this.constructor.checkRequired === 'function' ?
this.constructor.checkRequired() :
- ObjectId.checkRequired();
+ SchemaObjectId.checkRequired();
return _checkRequired(value);
};
@@ -221,8 +223,8 @@ ObjectId.prototype.checkRequired = function checkRequired(value, doc) {
* @api private
*/
-ObjectId.prototype.cast = function(value, doc, init) {
- if (!(isBsonType(value, 'ObjectID')) && SchemaType._isRef(this, value, doc, init)) {
+SchemaObjectId.prototype.cast = function(value, doc, init) {
+ if (!(isBsonType(value, 'ObjectId')) && SchemaType._isRef(this, value, doc, init)) {
// wait! we may need to cast this to a document
if ((getConstructorName(value) || '').toLowerCase() === 'objectid') {
return new oid(value.toHexString());
@@ -239,7 +241,7 @@ ObjectId.prototype.cast = function(value, doc, init) {
} else if (typeof this.constructor.cast === 'function') {
castObjectId = this.constructor.cast();
} else {
- castObjectId = ObjectId.cast();
+ castObjectId = SchemaObjectId.cast();
}
try {
@@ -257,13 +259,13 @@ function handleSingle(val) {
return this.cast(val);
}
-ObjectId.prototype.$conditionalHandlers =
- utils.options(SchemaType.prototype.$conditionalHandlers, {
- $gt: handleSingle,
- $gte: handleSingle,
- $lt: handleSingle,
- $lte: handleSingle
- });
+SchemaObjectId.prototype.$conditionalHandlers = {
+ ...SchemaType.prototype.$conditionalHandlers,
+ $gt: handleSingle,
+ $gte: handleSingle,
+ $lt: handleSingle,
+ $lte: handleSingle
+};
/*!
* ignore
@@ -276,7 +278,7 @@ function defaultId() {
defaultId.$runBeforeSetters = true;
function resetId(v) {
- Document || (Document = require('./../document'));
+ Document || (Document = require('../document'));
if (this instanceof Document) {
if (v === void 0) {
@@ -292,4 +294,4 @@ function resetId(v) {
* Module exports.
*/
-module.exports = ObjectId;
+module.exports = SchemaObjectId;
diff --git a/lib/schema/operators/geospatial.js b/lib/schema/operators/geospatial.js
index 80a60520713..75feda602da 100644
--- a/lib/schema/operators/geospatial.js
+++ b/lib/schema/operators/geospatial.js
@@ -34,7 +34,7 @@ function cast$near(val) {
'with a $geometry property');
}
- return SchemaArray.prototype.castForQuery.call(this, val);
+ return SchemaArray.prototype.castForQuery.call(this, null, val);
}
function cast$geometry(val, self) {
diff --git a/lib/schema/operators/text.js b/lib/schema/operators/text.js
index 81e8fa21bff..79be4ff7cb6 100644
--- a/lib/schema/operators/text.js
+++ b/lib/schema/operators/text.js
@@ -15,7 +15,7 @@ const castString = require('../../cast/string');
* @api private
*/
-module.exports = function(val, path) {
+module.exports = function castTextSearch(val, path) {
if (val == null || typeof val !== 'object') {
throw new CastError('$text', val, path);
}
diff --git a/lib/schema/string.js b/lib/schema/string.js
index d58d4797f5e..d62e233765b 100644
--- a/lib/schema/string.js
+++ b/lib/schema/string.js
@@ -4,9 +4,9 @@
* Module dependencies.
*/
-const SchemaType = require('../schematype');
+const SchemaType = require('../schemaType');
const MongooseError = require('../error/index');
-const SchemaStringOptions = require('../options/SchemaStringOptions');
+const SchemaStringOptions = require('../options/schemaStringOptions');
const castString = require('../cast/string');
const utils = require('../utils');
const isBsonType = require('../helpers/isBsonType');
@@ -143,6 +143,8 @@ SchemaString.get = SchemaType.get;
SchemaString.set = SchemaType.set;
+SchemaString.setters = [];
+
/*!
* ignore
*/
@@ -179,11 +181,11 @@ SchemaString.checkRequired = SchemaType.checkRequired;
* const s = new Schema({ state: { type: String, enum: states }})
* const M = db.model('M', s)
* const m = new M({ state: 'invalid' })
- * m.save(function (err) {
- * console.error(String(err)) // ValidationError: `invalid` is not a valid enum value for path `state`.
- * m.state = 'open'
- * m.save(callback) // success
- * })
+ * await m.save()
+ * .catch((err) => console.error(err)); // ValidationError: `invalid` is not a valid enum value for path `state`.
+ * m.state = 'open';
+ * await m.save();
+ * // success
*
* // or with custom error messages
* const enum = {
@@ -193,15 +195,16 @@ SchemaString.checkRequired = SchemaType.checkRequired;
* const s = new Schema({ state: { type: String, enum: enum })
* const M = db.model('M', s)
* const m = new M({ state: 'invalid' })
- * m.save(function (err) {
- * console.error(String(err)) // ValidationError: enum validator failed for path `state` with value `invalid`
- * m.state = 'open'
- * m.save(callback) // success
- * })
+ * await m.save()
+ * .catch((err) => console.error(err)); // ValidationError: enum validator failed for path `state` with value `invalid`
+ * m.state = 'open';
+ * await m.save();
+ * // success
*
* @param {...String|Object} [args] enumeration values
* @return {SchemaType} this
- * @see Customized Error Messages #error_messages_MongooseError-messages
+ * @see Customized Error Messages https://mongoosejs.com/docs/api/error.html#Error.messages
+ * @see Enums in JavaScript https://masteringjs.io/tutorials/fundamentals/enum
* @api public
*/
@@ -241,7 +244,7 @@ SchemaString.prototype.enum = function() {
const vals = this.enumValues;
this.enumValidator = function(v) {
- return undefined === v || ~vals.indexOf(v);
+ return null == v || ~vals.indexOf(v);
};
this.validators.push({
validator: this.enumValidator,
@@ -254,7 +257,7 @@ SchemaString.prototype.enum = function() {
};
/**
- * Adds a lowercase [setter](https://mongoosejs.com/docs/api/schematype.html#schematype_SchemaType-set).
+ * Adds a lowercase [setter](https://mongoosejs.com/docs/api/schematype.html#SchemaType.prototype.set()).
*
* #### Example:
*
@@ -293,7 +296,7 @@ SchemaString.prototype.lowercase = function(shouldApply) {
};
/**
- * Adds an uppercase [setter](https://mongoosejs.com/docs/api/schematype.html#schematype_SchemaType-set).
+ * Adds an uppercase [setter](https://mongoosejs.com/docs/api/schematype.html#SchemaType.prototype.set()).
*
* #### Example:
*
@@ -330,7 +333,7 @@ SchemaString.prototype.uppercase = function(shouldApply) {
};
/**
- * Adds a trim [setter](https://mongoosejs.com/docs/api/schematype.html#schematype_SchemaType-set).
+ * Adds a trim [setter](https://mongoosejs.com/docs/api/schematype.html#SchemaType.prototype.set()).
*
* The string value will be [trimmed](https://masteringjs.io/tutorials/fundamentals/trim-string) when set.
*
@@ -399,7 +402,7 @@ SchemaString.prototype.trim = function(shouldTrim) {
* @param {Number} value minimum string length
* @param {String} [message] optional custom error message
* @return {SchemaType} this
- * @see Customized Error Messages #error_messages_MongooseError-messages
+ * @see Customized Error Messages https://mongoosejs.com/docs/api/error.html#Error.messages
* @api public
*/
@@ -455,7 +458,7 @@ SchemaString.prototype.minLength = SchemaString.prototype.minlength;
* @param {Number} value maximum string length
* @param {String} [message] optional custom error message
* @return {SchemaType} this
- * @see Customized Error Messages #error_messages_MongooseError-messages
+ * @see Customized Error Messages https://mongoosejs.com/docs/api/error.html#Error.messages
* @api public
*/
@@ -518,7 +521,7 @@ SchemaString.prototype.maxLength = SchemaString.prototype.maxlength;
* @param {RegExp} regExp regular expression to test against
* @param {String} [message] optional custom error message
* @return {SchemaType} this
- * @see Customized Error Messages #error_messages_MongooseError-messages
+ * @see Customized Error Messages https://mongoosejs.com/docs/api/error.html#Error.messages
* @api public
*/
@@ -608,21 +611,21 @@ SchemaString.prototype.cast = function(value, doc, init) {
* ignore
*/
-function handleSingle(val) {
- return this.castForQuery(val);
+function handleSingle(val, context) {
+ return this.castForQuery(null, val, context);
}
/*!
* ignore
*/
-function handleArray(val) {
+function handleArray(val, context) {
const _this = this;
if (!Array.isArray(val)) {
- return [this.castForQuery(val)];
+ return [this.castForQuery(null, val, context)];
}
return val.map(function(m) {
- return _this.castForQuery(m);
+ return _this.castForQuery(null, m, context);
});
}
@@ -638,7 +641,8 @@ function handleSingleNoSetters(val) {
return this.cast(val, this);
}
-const $conditionalHandlers = utils.options(SchemaType.prototype.$conditionalHandlers, {
+const $conditionalHandlers = {
+ ...SchemaType.prototype.$conditionalHandlers,
$all: handleArray,
$gt: handleSingle,
$gte: handleSingle,
@@ -653,7 +657,7 @@ const $conditionalHandlers = utils.options(SchemaType.prototype.$conditionalHand
return handleSingleNoSetters.call(this, val);
},
$not: handleSingle
-});
+};
Object.defineProperty(SchemaString.prototype, '$conditionalHandlers', {
configurable: false,
@@ -670,21 +674,28 @@ Object.defineProperty(SchemaString.prototype, '$conditionalHandlers', {
* @api private
*/
-SchemaString.prototype.castForQuery = function($conditional, val) {
+SchemaString.prototype.castForQuery = function($conditional, val, context) {
let handler;
- if (arguments.length === 2) {
+ if ($conditional != null) {
handler = this.$conditionalHandlers[$conditional];
if (!handler) {
throw new Error('Can\'t use ' + $conditional + ' with String.');
}
- return handler.call(this, val);
+ return handler.call(this, val, context);
}
- val = $conditional;
+
if (Object.prototype.toString.call(val) === '[object RegExp]' || isBsonType(val, 'BSONRegExp')) {
return val;
}
- return this._castForQuery(val);
+ try {
+ return this.applySetters(val, context);
+ } catch (err) {
+ if (err instanceof CastError && err.path === this.path && this.$fullPath != null) {
+ err.path = this.$fullPath;
+ }
+ throw err;
+ }
};
/*!
diff --git a/lib/schema/SubdocumentPath.js b/lib/schema/subdocument.js
similarity index 64%
rename from lib/schema/SubdocumentPath.js
rename to lib/schema/subdocument.js
index 7cc391b894b..9a77d82c879 100644
--- a/lib/schema/SubdocumentPath.js
+++ b/lib/schema/subdocument.js
@@ -7,8 +7,8 @@
const CastError = require('../error/cast');
const EventEmitter = require('events').EventEmitter;
const ObjectExpectedError = require('../error/objectExpected');
-const SchemaSubdocumentOptions = require('../options/SchemaSubdocumentOptions');
-const SchemaType = require('../schematype');
+const SchemaSubdocumentOptions = require('../options/schemaSubdocumentOptions');
+const SchemaType = require('../schemaType');
const applyDefaults = require('../helpers/document/applyDefaults');
const $exists = require('./operators/exists');
const castToNumber = require('./operators/helpers').castToNumber;
@@ -17,11 +17,13 @@ const geospatial = require('./operators/geospatial');
const getConstructor = require('../helpers/discriminator/getConstructor');
const handleIdOption = require('../helpers/schema/handleIdOption');
const internalToObjectOptions = require('../options').internalToObjectOptions;
+const isExclusive = require('../helpers/projection/isExclusive');
const utils = require('../utils');
+const InvalidSchemaOptionError = require('../error/invalidSchemaOption');
-let Subdocument;
+let SubdocumentType;
-module.exports = SubdocumentPath;
+module.exports = SchemaSubdocument;
/**
* Single nested subdocument SchemaType constructor.
@@ -33,9 +35,12 @@ module.exports = SubdocumentPath;
* @api public
*/
-function SubdocumentPath(schema, path, options) {
- const schemaTypeIdOption = SubdocumentPath.defaultOptions &&
- SubdocumentPath.defaultOptions._id;
+function SchemaSubdocument(schema, path, options) {
+ if (schema.options.timeseries) {
+ throw new InvalidSchemaOptionError(path, 'timeseries');
+ }
+ const schemaTypeIdOption = SchemaSubdocument.defaultOptions &&
+ SchemaSubdocument.defaultOptions._id;
if (schemaTypeIdOption != null) {
options = options || {};
options._id = schemaTypeIdOption;
@@ -43,7 +48,7 @@ function SubdocumentPath(schema, path, options) {
schema = handleIdOption(schema, options);
- this.caster = _createConstructor(schema);
+ this.caster = _createConstructor(schema, null, options);
this.caster.path = path;
this.caster.prototype.$basePath = path;
this.schema = schema;
@@ -56,21 +61,21 @@ function SubdocumentPath(schema, path, options) {
* ignore
*/
-SubdocumentPath.prototype = Object.create(SchemaType.prototype);
-SubdocumentPath.prototype.constructor = SubdocumentPath;
-SubdocumentPath.prototype.OptionsConstructor = SchemaSubdocumentOptions;
+SchemaSubdocument.prototype = Object.create(SchemaType.prototype);
+SchemaSubdocument.prototype.constructor = SchemaSubdocument;
+SchemaSubdocument.prototype.OptionsConstructor = SchemaSubdocumentOptions;
/*!
* ignore
*/
-function _createConstructor(schema, baseClass) {
+function _createConstructor(schema, baseClass, options) {
// lazy load
- Subdocument || (Subdocument = require('../types/subdocument'));
+ SubdocumentType || (SubdocumentType = require('../types/subdocument'));
const _embedded = function SingleNested(value, path, parent) {
this.$__parent = parent;
- Subdocument.apply(this, arguments);
+ SubdocumentType.apply(this, arguments);
if (parent == null) {
return;
@@ -80,10 +85,12 @@ function _createConstructor(schema, baseClass) {
schema._preCompile();
- const proto = baseClass != null ? baseClass.prototype : Subdocument.prototype;
+ const proto = baseClass != null ? baseClass.prototype : SubdocumentType.prototype;
_embedded.prototype = Object.create(proto);
_embedded.prototype.$__setSchema(schema);
_embedded.prototype.constructor = _embedded;
+ _embedded.$__required = options?.required;
+ _embedded.base = schema.base;
_embedded.schema = schema;
_embedded.$isSingleNested = true;
_embedded.events = new EventEmitter();
@@ -117,27 +124,27 @@ function _createConstructor(schema, baseClass) {
* @api private
*/
-SubdocumentPath.prototype.$conditionalHandlers.$geoWithin = function handle$geoWithin(val) {
- return { $geometry: this.castForQuery(val.$geometry) };
+SchemaSubdocument.prototype.$conditionalHandlers.$geoWithin = function handle$geoWithin(val, context) {
+ return { $geometry: this.castForQuery(null, val.$geometry, context) };
};
/*!
* ignore
*/
-SubdocumentPath.prototype.$conditionalHandlers.$near =
-SubdocumentPath.prototype.$conditionalHandlers.$nearSphere = geospatial.cast$near;
+SchemaSubdocument.prototype.$conditionalHandlers.$near =
+SchemaSubdocument.prototype.$conditionalHandlers.$nearSphere = geospatial.cast$near;
-SubdocumentPath.prototype.$conditionalHandlers.$within =
-SubdocumentPath.prototype.$conditionalHandlers.$geoWithin = geospatial.cast$within;
+SchemaSubdocument.prototype.$conditionalHandlers.$within =
+SchemaSubdocument.prototype.$conditionalHandlers.$geoWithin = geospatial.cast$within;
-SubdocumentPath.prototype.$conditionalHandlers.$geoIntersects =
+SchemaSubdocument.prototype.$conditionalHandlers.$geoIntersects =
geospatial.cast$geoIntersects;
-SubdocumentPath.prototype.$conditionalHandlers.$minDistance = castToNumber;
-SubdocumentPath.prototype.$conditionalHandlers.$maxDistance = castToNumber;
+SchemaSubdocument.prototype.$conditionalHandlers.$minDistance = castToNumber;
+SchemaSubdocument.prototype.$conditionalHandlers.$maxDistance = castToNumber;
-SubdocumentPath.prototype.$conditionalHandlers.$exists = $exists;
+SchemaSubdocument.prototype.$conditionalHandlers.$exists = $exists;
/**
* Casts contents
@@ -146,7 +153,7 @@ SubdocumentPath.prototype.$conditionalHandlers.$exists = $exists;
* @api private
*/
-SubdocumentPath.prototype.cast = function(val, doc, init, priorVal, options) {
+SchemaSubdocument.prototype.cast = function(val, doc, init, priorVal, options) {
if (val && val.$isSingleNested && val.parent === doc) {
return val;
}
@@ -155,27 +162,30 @@ SubdocumentPath.prototype.cast = function(val, doc, init, priorVal, options) {
throw new ObjectExpectedError(this.path, val);
}
- const Constructor = getConstructor(this.caster, val);
+ const discriminatorKeyPath = this.schema.path(this.schema.options.discriminatorKey);
+ const defaultDiscriminatorValue = discriminatorKeyPath == null ? null : discriminatorKeyPath.getDefault(doc);
+ const Constructor = getConstructor(this.caster, val, defaultDiscriminatorValue);
let subdoc;
// Only pull relevant selected paths and pull out the base path
- const parentSelected = doc && doc.$__ && doc.$__.selected || {};
+ const parentSelected = doc && doc.$__ && doc.$__.selected;
const path = this.path;
- const selected = Object.keys(parentSelected).reduce((obj, key) => {
+ const selected = parentSelected == null ? null : Object.keys(parentSelected).reduce((obj, key) => {
if (key.startsWith(path + '.')) {
obj = obj || {};
obj[key.substring(path.length + 1)] = parentSelected[key];
}
return obj;
}, null);
- options = Object.assign({}, options, { priorDoc: priorVal });
if (init) {
subdoc = new Constructor(void 0, selected, doc, false, { defaults: false });
delete subdoc.$__.defaults;
subdoc.$init(val);
- applyDefaults(subdoc, selected);
+ const exclude = isExclusive(selected);
+ applyDefaults(subdoc, selected, exclude);
} else {
+ options = Object.assign({}, options, { priorDoc: priorVal });
if (Object.keys(val).length === 0) {
return new Constructor({}, selected, doc, undefined, options);
}
@@ -194,25 +204,28 @@ SubdocumentPath.prototype.cast = function(val, doc, init, priorVal, options) {
* @api private
*/
-SubdocumentPath.prototype.castForQuery = function($conditional, val, options) {
+SchemaSubdocument.prototype.castForQuery = function($conditional, val, context, options) {
let handler;
- if (arguments.length === 2) {
+ if ($conditional != null) {
handler = this.$conditionalHandlers[$conditional];
if (!handler) {
throw new Error('Can\'t use ' + $conditional);
}
return handler.call(this, val);
}
- val = $conditional;
if (val == null) {
return val;
}
+ const Constructor = getConstructor(this.caster, val);
+ if (val instanceof Constructor) {
+ return val;
+ }
+
if (this.options.runSetters) {
- val = this._applySetters(val);
+ val = this._applySetters(val, context);
}
- const Constructor = getConstructor(this.caster, val);
const overrideStrict = options != null && options.strict != null ?
options.strict :
void 0;
@@ -235,7 +248,7 @@ SubdocumentPath.prototype.castForQuery = function($conditional, val, options) {
* @api private
*/
-SubdocumentPath.prototype.doValidate = function(value, fn, scope, options) {
+SchemaSubdocument.prototype.doValidate = function(value, fn, scope, options) {
const Constructor = getConstructor(this.caster, value);
if (value && !(value instanceof Constructor)) {
@@ -246,7 +259,7 @@ SubdocumentPath.prototype.doValidate = function(value, fn, scope, options) {
if (!value) {
return fn(null);
}
- return value.validate(fn);
+ return value.validate().then(() => fn(null), err => fn(err));
}
SchemaType.prototype.doValidate.call(this, value, function(error) {
@@ -257,7 +270,7 @@ SubdocumentPath.prototype.doValidate = function(value, fn, scope, options) {
return fn(null);
}
- value.validate(fn);
+ value.validate().then(() => fn(null), err => fn(err));
}, scope, options);
};
@@ -267,7 +280,7 @@ SubdocumentPath.prototype.doValidate = function(value, fn, scope, options) {
* @api private
*/
-SubdocumentPath.prototype.doValidateSync = function(value, scope, options) {
+SchemaSubdocument.prototype.doValidateSync = function(value, scope, options) {
if (!options || !options.skipSchemaValidators) {
const schemaTypeError = SchemaType.prototype.doValidateSync.call(this, value, scope);
if (schemaTypeError) {
@@ -297,11 +310,11 @@ SubdocumentPath.prototype.doValidateSync = function(value, scope, options) {
* @param {String} [options.value] the string stored in the `discriminatorKey` property. If not specified, Mongoose uses the `name` parameter.
* @param {Boolean} [options.clone=true] By default, `discriminator()` clones the given `schema`. Set to `false` to skip cloning.
* @return {Function} the constructor Mongoose will use for creating instances of this discriminator model
- * @see discriminators /docs/discriminators.html
+ * @see discriminators https://mongoosejs.com/docs/discriminators.html
* @api public
*/
-SubdocumentPath.prototype.discriminator = function(name, schema, options) {
+SchemaSubdocument.prototype.discriminator = function(name, schema, options) {
options = options || {};
const value = utils.isPOJO(options) ? options.value : options;
const clone = typeof options.clone === 'boolean'
@@ -312,7 +325,7 @@ SubdocumentPath.prototype.discriminator = function(name, schema, options) {
schema = schema.clone();
}
- schema = discriminator(this.caster, name, schema, value);
+ schema = discriminator(this.caster, name, schema, value, null, null, options.overwriteExisting);
this.caster.discriminators[name] = _createConstructor(schema, this.caster);
@@ -323,15 +336,15 @@ SubdocumentPath.prototype.discriminator = function(name, schema, options) {
* ignore
*/
-SubdocumentPath.defaultOptions = {};
+SchemaSubdocument.defaultOptions = {};
/**
- * Sets a default option for all SubdocumentPath instances.
+ * Sets a default option for all Subdocument instances.
*
* #### Example:
*
* // Make all numbers have option `min` equal to 0.
- * mongoose.Schema.SubdocumentPath.set('required', true);
+ * mongoose.Schema.Subdocument.set('required', true);
*
* @param {String} option The option you'd like to set the value for
* @param {Any} value value for option
@@ -341,13 +354,27 @@ SubdocumentPath.defaultOptions = {};
* @api public
*/
-SubdocumentPath.set = SchemaType.set;
+SchemaSubdocument.set = SchemaType.set;
+
+SchemaSubdocument.setters = [];
+
+/**
+ * Attaches a getter for all Subdocument instances
+ *
+ * @param {Function} getter
+ * @return {this}
+ * @function get
+ * @static
+ * @api public
+ */
+
+SchemaSubdocument.get = SchemaType.get;
/*!
* ignore
*/
-SubdocumentPath.prototype.toJSON = function toJSON() {
+SchemaSubdocument.prototype.toJSON = function toJSON() {
return { path: this.path, options: this.options };
};
@@ -355,13 +382,17 @@ SubdocumentPath.prototype.toJSON = function toJSON() {
* ignore
*/
-SubdocumentPath.prototype.clone = function() {
- const options = Object.assign({}, this.options);
- const schematype = new this.constructor(this.schema, this.path, options);
+SchemaSubdocument.prototype.clone = function() {
+ const schematype = new this.constructor(
+ this.schema,
+ this.path,
+ { ...this.options, _skipApplyDiscriminators: true }
+ );
schematype.validators = this.validators.slice();
if (this.requiredValidator !== undefined) {
schematype.requiredValidator = this.requiredValidator;
}
schematype.caster.discriminators = Object.assign({}, this.caster.discriminators);
+ schematype._appliedDiscriminators = this._appliedDiscriminators;
return schematype;
};
diff --git a/lib/schema/uuid.js b/lib/schema/uuid.js
index 839c27dfaba..1fbfc38654d 100644
--- a/lib/schema/uuid.js
+++ b/lib/schema/uuid.js
@@ -5,7 +5,7 @@
'use strict';
const MongooseBuffer = require('../types/buffer');
-const SchemaType = require('../schematype');
+const SchemaType = require('../schemaType');
const CastError = SchemaType.CastError;
const utils = require('../utils');
const handleBitwiseOperator = require('./operators/bitwise');
@@ -26,19 +26,6 @@ function hex2buffer(hex) {
return buff;
}
-/**
- * Helper function to convert the buffer input to a string
- * @param {Buffer} buf The buffer to convert to a hex-string
- * @returns {String} The buffer as a hex-string
- * @api private
- */
-
-function binary2hex(buf) {
- // use buffer built-in function to convert from buffer to hex-string
- const hex = buf != null && buf.toString('hex');
- return hex;
-}
-
/**
* Convert a String to Binary
* @param {String} uuidStr The value to process
@@ -67,7 +54,7 @@ function binaryToString(uuidBin) {
// i(hasezoey) dont quite know why, but "uuidBin" may sometimes also be the already processed string
let hex;
if (typeof uuidBin !== 'string' && uuidBin != null) {
- hex = binary2hex(uuidBin);
+ hex = uuidBin.toString('hex');
const uuidStr = hex.substring(0, 8) + '-' + hex.substring(8, 8 + 4) + '-' + hex.substring(12, 12 + 4) + '-' + hex.substring(16, 16 + 4) + '-' + hex.substring(20, 20 + 12);
return uuidStr;
}
@@ -90,7 +77,15 @@ function SchemaUUID(key, options) {
if (value != null && value.$__ != null) {
return value;
}
- return binaryToString(value);
+ if (Buffer.isBuffer(value)) {
+ return binaryToString(value);
+ } else if (value instanceof Binary) {
+ return binaryToString(value.buffer);
+ } else if (utils.isPOJO(value) && value.type === 'Buffer' && Array.isArray(value.data)) {
+ // Cloned buffers look like `{ type: 'Buffer', data: [5, 224, ...] }`
+ return binaryToString(Buffer.from(value.data));
+ }
+ return value;
});
}
@@ -153,6 +148,26 @@ SchemaUUID._cast = function(value) {
throw new CastError(SchemaUUID.schemaName, value, this.path);
};
+/**
+ * Attaches a getter for all UUID instances.
+ *
+ * #### Example:
+ *
+ * // Note that `v` is a string by default
+ * mongoose.Schema.UUID.get(v => v.toUpperCase());
+ *
+ * const Model = mongoose.model('Test', new Schema({ test: 'UUID' }));
+ * new Model({ test: uuid.v4() }).test; // UUID with all uppercase
+ *
+ * @param {Function} getter
+ * @return {this}
+ * @function get
+ * @static
+ * @api public
+ */
+
+SchemaUUID.get = SchemaType.get;
+
/**
* Sets a default option for all UUID instances.
*
@@ -174,6 +189,8 @@ SchemaUUID._cast = function(value) {
SchemaUUID.set = SchemaType.set;
+SchemaUUID.setters = [];
+
/**
* Get/set the function used to cast arbitrary values to UUIDs.
*
@@ -291,8 +308,8 @@ function handleArray(val) {
});
}
-SchemaUUID.prototype.$conditionalHandlers =
-utils.options(SchemaType.prototype.$conditionalHandlers, {
+SchemaUUID.prototype.$conditionalHandlers = {
+ ...SchemaType.prototype.$conditionalHandlers,
$bitsAllClear: handleBitwiseOperator,
$bitsAnyClear: handleBitwiseOperator,
$bitsAllSet: handleBitwiseOperator,
@@ -305,7 +322,7 @@ utils.options(SchemaType.prototype.$conditionalHandlers, {
$lte: handleSingle,
$ne: handleSingle,
$nin: handleArray
-});
+};
/**
* Casts contents for queries.
@@ -315,15 +332,22 @@ utils.options(SchemaType.prototype.$conditionalHandlers, {
* @api private
*/
-SchemaUUID.prototype.castForQuery = function($conditional, val) {
+SchemaUUID.prototype.castForQuery = function($conditional, val, context) {
let handler;
- if (arguments.length === 2) {
+ if ($conditional != null) {
handler = this.$conditionalHandlers[$conditional];
if (!handler)
throw new Error('Can\'t use ' + $conditional + ' with UUID.');
- return handler.call(this, val);
- } else {
- return this.cast($conditional);
+ return handler.call(this, val, context);
+ }
+
+ try {
+ return this.applySetters(val, context);
+ } catch (err) {
+ if (err instanceof CastError && err.path === this.path && this.$fullPath != null) {
+ err.path = this.$fullPath;
+ }
+ throw err;
}
};
diff --git a/lib/schematype.js b/lib/schemaType.js
similarity index 87%
rename from lib/schematype.js
rename to lib/schemaType.js
index 2b62c2208f7..ed63c47bbc7 100644
--- a/lib/schematype.js
+++ b/lib/schemaType.js
@@ -5,9 +5,10 @@
*/
const MongooseError = require('./error/index');
-const SchemaTypeOptions = require('./options/SchemaTypeOptions');
+const SchemaTypeOptions = require('./options/schemaTypeOptions');
const $exists = require('./schema/operators/exists');
const $type = require('./schema/operators/type');
+const clone = require('./helpers/clone');
const handleImmutable = require('./helpers/schematype/handleImmutable');
const isAsyncFunction = require('./helpers/isAsyncFunction');
const isSimpleValidator = require('./helpers/isSimpleValidator');
@@ -34,7 +35,7 @@ const setOptionsForDefaults = { _skipMarkModified: true };
* schema.path('name') instanceof SchemaType; // true
*
* @param {String} path
- * @param {SchemaTypeOptions} [options] See [SchemaTypeOptions docs](/docs/api/schematypeoptions.html)
+ * @param {SchemaTypeOptions} [options] See [SchemaTypeOptions docs](https://mongoosejs.com/docs/api/schematypeoptions.html)
* @param {String} [instance]
* @api public
*/
@@ -47,7 +48,9 @@ function SchemaType(path, options, instance) {
this.getters = this.constructor.hasOwnProperty('getters') ?
this.constructor.getters.slice() :
[];
- this.setters = [];
+ this.setters = this.constructor.hasOwnProperty('setters') ?
+ this.constructor.setters.slice() :
+ [];
this.splitPath();
@@ -56,7 +59,9 @@ function SchemaType(path, options, instance) {
const defaultOptionsKeys = Object.keys(defaultOptions);
for (const option of defaultOptionsKeys) {
- if (defaultOptions.hasOwnProperty(option) && !Object.prototype.hasOwnProperty.call(options, option)) {
+ if (option === 'validate') {
+ this.validate(defaultOptions.validate);
+ } else if (defaultOptions.hasOwnProperty(option) && !Object.prototype.hasOwnProperty.call(options, option)) {
options[option] = defaultOptions[option];
}
}
@@ -69,7 +74,6 @@ function SchemaType(path, options, instance) {
this.options = new Options(options);
this._index = null;
-
if (utils.hasUserDefinedProperty(this.options, 'immutable')) {
this.$immutable = this.options.immutable;
@@ -79,7 +83,11 @@ function SchemaType(path, options, instance) {
const keys = Object.keys(this.options);
for (const prop of keys) {
if (prop === 'cast') {
- this.castFunction(this.options[prop]);
+ if (Array.isArray(this.options[prop])) {
+ this.castFunction.apply(this, this.options[prop]);
+ } else {
+ this.castFunction(this.options[prop]);
+ }
continue;
}
if (utils.hasUserDefinedProperty(this.options, prop) && typeof this[prop] === 'function') {
@@ -252,14 +260,24 @@ SchemaType.cast = function cast(caster) {
* @api public
*/
-SchemaType.prototype.castFunction = function castFunction(caster) {
+SchemaType.prototype.castFunction = function castFunction(caster, message) {
if (arguments.length === 0) {
return this._castFunction;
}
+
if (caster === false) {
caster = this.constructor._defaultCaster || (v => v);
}
- this._castFunction = caster;
+ if (typeof caster === 'string') {
+ this._castErrorMessage = caster;
+ return this._castFunction;
+ }
+ if (caster != null) {
+ this._castFunction = caster;
+ }
+ if (message != null) {
+ this._castErrorMessage = message;
+ }
return this._castFunction;
};
@@ -428,21 +446,38 @@ SchemaType.prototype.index = function(options) {
*
* _NOTE: violating the constraint returns an `E11000` error from MongoDB when saving, not a Mongoose validation error._
*
- * @param {Boolean} bool
+ * You can optionally specify an error message to replace MongoDB's default `E11000 duplicate key error` message.
+ * The following will throw a "Email must be unique" error if `save()`, `updateOne()`, `updateMany()`, `replaceOne()`,
+ * `findOneAndUpdate()`, or `findOneAndReplace()` throws a duplicate key error:
+ *
+ * ```javascript
+ * new Schema({
+ * email: {
+ * type: String,
+ * unique: [true, 'Email must be unique']
+ * }
+ * });
+ * ```
+ *
+ * Note that the above syntax does **not** work for `bulkWrite()` or `insertMany()`. `bulkWrite()` and `insertMany()`
+ * will still throw MongoDB's default `E11000 duplicate key error` message.
+ *
+ * @param {Boolean} value
+ * @param {String} [message]
* @return {SchemaType} this
* @api public
*/
-SchemaType.prototype.unique = function(bool) {
+SchemaType.prototype.unique = function unique(value, message) {
if (this._index === false) {
- if (!bool) {
+ if (!value) {
return;
}
throw new Error('Path "' + this.path + '" may not have `index` set to ' +
'false and `unique` set to true');
}
- if (!this.options.hasOwnProperty('index') && bool === false) {
+ if (!this.options.hasOwnProperty('index') && value === false) {
return this;
}
@@ -452,7 +487,10 @@ SchemaType.prototype.unique = function(bool) {
this._index = { type: this._index };
}
- this._index.unique = bool;
+ this._index.unique = !!value;
+ if (typeof message === 'string') {
+ this._duplicateKeyErrorMessage = message;
+ }
return this;
};
@@ -531,7 +569,7 @@ SchemaType.prototype.sparse = function(bool) {
/**
* Defines this path as immutable. Mongoose prevents you from changing
- * immutable paths unless the parent document has [`isNew: true`](/docs/api/document.html#document_Document-isNew).
+ * immutable paths unless the parent document has [`isNew: true`](https://mongoosejs.com/docs/api/document.html#Document.prototype.isNew()).
*
* #### Example:
*
@@ -549,7 +587,7 @@ SchemaType.prototype.sparse = function(bool) {
* doc.name; // 'test', because `name` is immutable
*
* Mongoose also prevents changing immutable properties using `updateOne()`
- * and `updateMany()` based on [strict mode](/docs/guide.html#strict).
+ * and `updateMany()` based on [strict mode](https://mongoosejs.com/docs/guide.html#strict).
*
* #### Example:
*
@@ -568,7 +606,7 @@ SchemaType.prototype.sparse = function(bool) {
*
* @param {Boolean} bool
* @return {SchemaType} this
- * @see isNew /docs/api/document.html#document_Document-isNew
+ * @see isNew https://mongoosejs.com/docs/api/document.html#Document.prototype.isNew()
* @api public
*/
@@ -784,6 +822,21 @@ SchemaType.prototype.get = function(fn) {
return this;
};
+/**
+ * Adds multiple validators for this document path.
+ * Calls `validate()` for every element in validators.
+ *
+ * @param {Array} validators
+ * @returns this
+ */
+
+SchemaType.prototype.validateAll = function(validators) {
+ for (let i = 0; i < validators.length; i++) {
+ this.validate(validators[i]);
+ }
+ return this;
+};
+
/**
* Adds validator(s) for this document path.
*
@@ -791,7 +844,7 @@ SchemaType.prototype.get = function(fn) {
* must return `Boolean`. Returning `false` or throwing an error means
* validation failed.
*
- * The error message argument is optional. If not passed, the [default generic error message template](#error_messages_MongooseError-messages) will be used.
+ * The error message argument is optional. If not passed, the [default generic error message template](https://mongoosejs.com/docs/api/error.html#Error.messages) will be used.
*
* #### Example:
*
@@ -809,8 +862,8 @@ SchemaType.prototype.get = function(fn) {
* // adding many validators at a time
*
* const many = [
- * { validator: validator, msg: 'uh oh' }
- * , { validator: anotherValidator, msg: 'failed' }
+ * { validator: validator, message: 'uh oh' }
+ * , { validator: anotherValidator, message: 'failed' }
* ]
* new Schema({ name: { type: String, validate: many }});
*
@@ -821,10 +874,12 @@ SchemaType.prototype.get = function(fn) {
*
* #### Error message templates:
*
- * From the examples above, you may have noticed that error messages support
- * basic templating. There are a few other template keywords besides `{PATH}`
- * and `{VALUE}` too. To find out more, details are available
- * [here](#error_messages_MongooseError-messages).
+ * Below is a list of supported template keywords:
+ *
+ * - PATH: The schema path where the error is being triggered.
+ * - VALUE: The value assigned to the PATH that is triggering the error.
+ * - KIND: The validation property that triggered the error i.e. required.
+ * - REASON: The error object that caused this error if there was one.
*
* If Mongoose's built-in error message templating isn't enough, Mongoose
* supports setting the `message` property to a function.
@@ -842,7 +897,7 @@ SchemaType.prototype.get = function(fn) {
*
* schema.path('name').validate({
* validator: function() { throw new Error('Oops!'); },
- * // `errors['name']` will be "Oops!"
+ * // `errors['name'].message` will be "Oops!"
* message: function(props) { return props.reason.message; }
* });
*
@@ -862,9 +917,9 @@ SchemaType.prototype.get = function(fn) {
*
* You might use asynchronous validators to retreive other documents from the database to validate against or to meet other I/O bound validation needs.
*
- * Validation occurs `pre('save')` or whenever you manually execute [document#validate](#document_Document-validate).
+ * Validation occurs `pre('save')` or whenever you manually execute [document#validate](https://mongoosejs.com/docs/api/document.html#Document.prototype.validate()).
*
- * If validation fails during `pre('save')` and no callback was passed to receive the error, an `error` event will be emitted on your Models associated db [connection](#connection_Connection), passing the validation error object along.
+ * If validation fails during `pre('save')` and no callback was passed to receive the error, an `error` event will be emitted on your Models associated db [connection](https://mongoosejs.com/docs/api/connection.html#Connection()), passing the validation error object along.
*
* const conn = mongoose.createConnection(..);
* conn.on('error', handleError);
@@ -896,7 +951,7 @@ SchemaType.prototype.validate = function(obj, message, type) {
properties = { validator: obj, message: message };
properties.type = type || 'user defined';
} else if (message instanceof Object && !type) {
- properties = isSimpleValidator(message) ? Object.assign({}, message) : utils.clone(message);
+ properties = isSimpleValidator(message) ? Object.assign({}, message) : clone(message);
if (!properties.message) {
properties.message = properties.msg;
}
@@ -925,7 +980,7 @@ SchemaType.prototype.validate = function(obj, message, type) {
if (!utils.isPOJO(arg)) {
const msg = 'Invalid validator. Received (' + typeof arg + ') '
+ arg
- + '. See https://mongoosejs.com/docs/api/schematype.html#schematype_SchemaType-validate';
+ + '. See https://mongoosejs.com/docs/api/schematype.html#SchemaType.prototype.validate()';
throw new Error(msg);
}
@@ -992,13 +1047,13 @@ SchemaType.prototype.validate = function(obj, message, type) {
* @param {Function} [options.ErrorConstructor] custom error constructor. The constructor receives 1 parameter, an object containing the validator properties.
* @param {String} [message] optional custom error message
* @return {SchemaType} this
- * @see Customized Error Messages #error_messages_MongooseError-messages
- * @see SchemaArray#checkRequired #schema_array_SchemaArray-checkRequired
- * @see SchemaBoolean#checkRequired #schema_boolean_SchemaBoolean-checkRequired
- * @see SchemaBuffer#checkRequired #schema_buffer_SchemaBuffer-schemaName
- * @see SchemaNumber#checkRequired #schema_number_SchemaNumber-min
- * @see SchemaObjectId#checkRequired #schema_objectid_ObjectId-auto
- * @see SchemaString#checkRequired #schema_string_SchemaString-checkRequired
+ * @see Customized Error Messages https://mongoosejs.com/docs/api/error.html#Error.messages
+ * @see SchemaArray#checkRequired https://mongoosejs.com/docs/api/schemaarray.html#SchemaArray.prototype.checkRequired()
+ * @see SchemaBoolean#checkRequired https://mongoosejs.com/docs/api/schemaboolean.html#SchemaBoolean.prototype.checkRequired()
+ * @see SchemaBuffer#checkRequired https://mongoosejs.com/docs/api/schemabuffer.html#SchemaBuffer.prototype.checkRequired()
+ * @see SchemaNumber#checkRequired https://mongoosejs.com/docs/api/schemanumber.html#SchemaNumber.prototype.checkRequired()
+ * @see SchemaObjectId#checkRequired https://mongoosejs.com/docs/api/schemaobjectid.html#ObjectId.prototype.checkRequired()
+ * @see SchemaString#checkRequired https://mongoosejs.com/docs/api/schemastring.html#SchemaString.prototype.checkRequired()
* @api public
*/
@@ -1131,7 +1186,7 @@ SchemaType.prototype.getDefault = function(scope, init, options) {
if (ret !== null && ret !== undefined) {
if (typeof ret === 'object' && (!this.options || !this.options.shared)) {
- ret = utils.clone(ret);
+ ret = clone(ret);
}
if (options && options.skipCast) {
@@ -1196,7 +1251,6 @@ SchemaType.prototype.applySetters = function(value, scope, init, priorVal, optio
if (v == null) {
return this._castNullish(v);
}
-
// do not cast until all setters are applied #665
v = this.cast(v, scope, init, priorVal, options);
@@ -1265,6 +1319,9 @@ SchemaType.prototype.select = function select(val) {
SchemaType.prototype.doValidate = function(value, fn, scope, options) {
let err = false;
const path = this.path;
+ if (typeof fn !== 'function') {
+ throw new TypeError(`Must pass callback function to doValidate(), got ${typeof fn}`);
+ }
// Avoid non-object `validators`
const validators = this.validators.
@@ -1285,8 +1342,9 @@ SchemaType.prototype.doValidate = function(value, fn, scope, options) {
const validator = v.validator;
let ok;
- const validatorProperties = isSimpleValidator(v) ? Object.assign({}, v) : utils.clone(v);
+ const validatorProperties = isSimpleValidator(v) ? Object.assign({}, v) : clone(v);
validatorProperties.path = options && options.path ? options.path : path;
+ validatorProperties.fullPath = this.$fullPath;
validatorProperties.value = value;
if (validator instanceof RegExp) {
@@ -1398,7 +1456,6 @@ SchemaType.prototype.doValidateSync = function(value, scope, options) {
let i = 0;
const len = validators.length;
for (i = 0; i < len; ++i) {
-
const v = validators[i];
if (v === null || typeof v !== 'object') {
@@ -1406,8 +1463,9 @@ SchemaType.prototype.doValidateSync = function(value, scope, options) {
}
const validator = v.validator;
- const validatorProperties = isSimpleValidator(v) ? Object.assign({}, v) : utils.clone(v);
+ const validatorProperties = isSimpleValidator(v) ? Object.assign({}, v) : clone(v);
validatorProperties.path = options && options.path ? options.path : path;
+ validatorProperties.fullPath = this.$fullPath;
validatorProperties.value = value;
let ok = false;
@@ -1503,7 +1561,7 @@ SchemaType.prototype._castRef = function _castRef(value, doc, init) {
}
if (value.$__ != null) {
- value.$__.wasPopulated = value.$__.wasPopulated || true;
+ value.$__.wasPopulated = value.$__.wasPopulated || { value: value._doc._id };
return value;
}
@@ -1528,8 +1586,9 @@ SchemaType.prototype._castRef = function _castRef(value, doc, init) {
!doc.$__.populated[path].options ||
!doc.$__.populated[path].options.options ||
!doc.$__.populated[path].options.options.lean) {
- ret = new pop.options[populateModelSymbol](value);
- ret.$__.wasPopulated = true;
+ const PopulatedModel = pop ? pop.options[populateModelSymbol] : doc.constructor.db.model(this.options.ref);
+ ret = new PopulatedModel(value);
+ ret.$__.wasPopulated = { value: ret._doc._id, options: { [populateModelSymbol]: PopulatedModel } };
}
return ret;
@@ -1539,21 +1598,21 @@ SchemaType.prototype._castRef = function _castRef(value, doc, init) {
* ignore
*/
-function handleSingle(val) {
- return this.castForQuery(val);
+function handleSingle(val, context) {
+ return this.castForQuery(null, val, context);
}
/*!
* ignore
*/
-function handleArray(val) {
+function handleArray(val, context) {
const _this = this;
if (!Array.isArray(val)) {
- return [this.castForQuery(val)];
+ return [this.castForQuery(null, val, context)];
}
return val.map(function(m) {
- return _this.castForQuery(m);
+ return _this.castForQuery(null, m, context);
});
}
@@ -1563,16 +1622,16 @@ function handleArray(val) {
* @api private
*/
-function handle$in(val) {
+function handle$in(val, context) {
const _this = this;
if (!Array.isArray(val)) {
- return [this.castForQuery(val)];
+ return [this.castForQuery(null, val, context)];
}
return val.map(function(m) {
if (Array.isArray(m) && m.length === 0) {
return m;
}
- return _this.castForQuery(m);
+ return _this.castForQuery(null, m, context);
});
}
@@ -1590,63 +1649,34 @@ SchemaType.prototype.$conditionalHandlers = {
$type: $type
};
-/**
- * Wraps `castForQuery` to handle context
- * @param {Object} params
- * @instance
- * @api private
- */
-
-SchemaType.prototype.castForQueryWrapper = function(params) {
- this.$$context = params.context;
- if ('$conditional' in params) {
- const ret = this.castForQuery(params.$conditional, params.val);
- this.$$context = null;
- return ret;
- }
- if (params.$skipQueryCastForUpdate || params.$applySetters) {
- const ret = this._castForQuery(params.val);
- this.$$context = null;
- return ret;
- }
-
- const ret = this.castForQuery(params.val);
- this.$$context = null;
- return ret;
-};
-
/**
* Cast the given value with the given optional query operator.
*
* @param {String} [$conditional] query operator, like `$eq` or `$in`
* @param {Any} val
+ * @param {Query} context
* @return {Any}
* @api private
*/
-SchemaType.prototype.castForQuery = function($conditional, val) {
+SchemaType.prototype.castForQuery = function($conditional, val, context) {
let handler;
- if (arguments.length === 2) {
+ if ($conditional != null) {
handler = this.$conditionalHandlers[$conditional];
if (!handler) {
throw new Error('Can\'t use ' + $conditional);
}
- return handler.call(this, val);
+ return handler.call(this, val, context);
}
- val = $conditional;
- return this._castForQuery(val);
-};
-/**
- * Internal switch for runSetters
- *
- * @param {Any} val
- * @return {Any}
- * @api private
- */
-
-SchemaType.prototype._castForQuery = function(val) {
- return this.applySetters(val, this.$$context);
+ try {
+ return this.applySetters(val, context);
+ } catch (err) {
+ if (err instanceof CastError && err.path === this.path && this.$fullPath != null) {
+ err.path = this.$fullPath;
+ }
+ throw err;
+ }
};
/**
@@ -1714,6 +1744,33 @@ SchemaType.prototype.clone = function() {
return schematype;
};
+/**
+ * Returns the embedded schema type, if any. For arrays, document arrays, and maps, `getEmbeddedSchemaType()`
+ * returns the schema type of the array's elements (or map's elements). For other types, `getEmbeddedSchemaType()`
+ * returns `undefined`.
+ *
+ * #### Example:
+ *
+ * const schema = new Schema({ name: String, tags: [String] });
+ * schema.path('name').getEmbeddedSchemaType(); // undefined
+ * schema.path('tags').getEmbeddedSchemaType(); // SchemaString { path: 'tags', ... }
+ *
+ * @returns {SchemaType} embedded schematype
+ * @api public
+ */
+
+SchemaType.prototype.getEmbeddedSchemaType = function getEmbeddedSchemaType() {
+ return this.$embeddedSchemaType;
+};
+
+/*!
+ * If _duplicateKeyErrorMessage is a string, replace unique index errors "E11000 duplicate key error" with this string.
+ *
+ * @api private
+ */
+
+SchemaType.prototype._duplicateKeyErrorMessage = null;
+
/*!
* Module exports.
*/
diff --git a/lib/statemachine.js b/lib/stateMachine.js
similarity index 88%
rename from lib/statemachine.js
rename to lib/stateMachine.js
index 70b1beca695..511dc54de21 100644
--- a/lib/statemachine.js
+++ b/lib/stateMachine.js
@@ -41,6 +41,7 @@ StateMachine.ctor = function() {
};
ctor.prototype = new StateMachine();
+ ctor.prototype.constructor = ctor;
ctor.prototype.stateNames = states;
@@ -65,7 +66,11 @@ StateMachine.ctor = function() {
*/
StateMachine.prototype._changeState = function _changeState(path, nextState) {
- const prevBucket = this.states[this.paths[path]];
+ const prevState = this.paths[path];
+ if (prevState === nextState) {
+ return;
+ }
+ const prevBucket = this.states[prevState];
if (prevBucket) delete prevBucket[path];
this.paths[path] = nextState;
@@ -205,3 +210,23 @@ StateMachine.prototype.map = function map() {
this.map = this._iter('map');
return this.map.apply(this, arguments);
};
+
+/**
+ * Returns a copy of this state machine
+ *
+ * @param {Function} callback
+ * @return {StateMachine}
+ * @api private
+ */
+
+StateMachine.prototype.clone = function clone() {
+ const result = new this.constructor();
+ result.paths = { ...this.paths };
+ for (const state of this.stateNames) {
+ if (!(state in this.states)) {
+ continue;
+ }
+ result.states[state] = this.states[state] == null ? this.states[state] : { ...this.states[state] };
+ }
+ return result;
+};
diff --git a/lib/types/array/index.js b/lib/types/array/index.js
index 41e8399becf..1f6e6a54d88 100644
--- a/lib/types/array/index.js
+++ b/lib/types/array/index.js
@@ -90,6 +90,12 @@ function MongooseArray(values, path, doc, schematype) {
if (mongooseArrayMethods.hasOwnProperty(prop)) {
return mongooseArrayMethods[prop];
}
+ if (schematype && schematype.virtuals && schematype.virtuals.hasOwnProperty(prop)) {
+ return schematype.virtuals[prop].applyGetters(undefined, target);
+ }
+ if (typeof prop === 'string' && numberRE.test(prop) && schematype?.$embeddedSchemaType != null) {
+ return schematype.$embeddedSchemaType.applyGetters(__array[prop], doc);
+ }
return __array[prop];
},
@@ -98,6 +104,8 @@ function MongooseArray(values, path, doc, schematype) {
mongooseArrayMethods.set.call(proxy, prop, value, false);
} else if (internals.hasOwnProperty(prop)) {
internals[prop] = value;
+ } else if (schematype && schematype.virtuals && schematype.virtuals.hasOwnProperty(prop)) {
+ schematype.virtuals[prop].applySetters(value, target);
} else {
__array[prop] = value;
}
diff --git a/lib/types/array/methods/index.js b/lib/types/array/methods/index.js
index 13b3f493c98..3322bbe56e8 100644
--- a/lib/types/array/methods/index.js
+++ b/lib/types/array/methods/index.js
@@ -1,9 +1,10 @@
'use strict';
const Document = require('../../../document');
-const ArraySubdocument = require('../../ArraySubdocument');
+const ArraySubdocument = require('../../arraySubdocument');
const MongooseError = require('../../../error/mongooseError');
const cleanModifiedSubpaths = require('../../../helpers/document/cleanModifiedSubpaths');
+const clone = require('../../../helpers/clone');
const internalToObjectOptions = require('../../../options').internalToObjectOptions;
const mpath = require('mpath');
const utils = require('../../../utils');
@@ -104,7 +105,7 @@ const methods = {
* #### Note:
*
* _Calling this multiple times on an array before saving sends the same command as calling it once._
- * _This update is implemented using the MongoDB [$pop](https://www.mongodb.org/display/DOCS/Updating/#Updating-%24pop) method which enforces this restriction._
+ * _This update is implemented using the MongoDB [$pop](https://www.mongodb.com/docs/manual/reference/operator/update/pop/) method which enforces this restriction._
*
* doc.array = [1,2,3];
*
@@ -129,7 +130,7 @@ const methods = {
* @memberOf MongooseArray
* @instance
* @method $shift
- * @see mongodb https://www.mongodb.org/display/DOCS/Updating/#Updating-%24pop
+ * @see mongodb https://www.mongodb.com/docs/manual/reference/operator/update/pop/
*/
$shift() {
@@ -152,7 +153,7 @@ const methods = {
* #### NOTE:
*
* _Calling this multiple times on an array before saving sends the same command as calling it once._
- * _This update is implemented using the MongoDB [$pop](https://www.mongodb.org/display/DOCS/Updating/#Updating-%24pop) method which enforces this restriction._
+ * _This update is implemented using the MongoDB [$pop](https://www.mongodb.com/docs/manual/reference/operator/update/pop/) method which enforces this restriction._
*
* doc.array = [1,2,3];
*
@@ -177,7 +178,7 @@ const methods = {
* @method $pop
* @memberOf MongooseArray
* @instance
- * @see mongodb https://www.mongodb.org/display/DOCS/Updating/#Updating-%24pop
+ * @see mongodb https://www.mongodb.com/docs/manual/reference/operator/update/pop/
* @method $pop
* @memberOf MongooseArray
*/
@@ -225,11 +226,14 @@ const methods = {
if (populated && value !== null && value !== undefined) {
// cast to the populated Models schema
Model = populated.options[populateModelSymbol];
+ if (Model == null) {
+ throw new MongooseError('No populated model found for path `' + this[arrayPathSymbol] + '`. This is likely a bug in Mongoose, please report an issue on github.com/Automattic/mongoose.');
+ }
// only objects are permitted so we can safely assume that
// non-objects are to be interpreted as _id
if (Buffer.isBuffer(value) ||
- isBsonType(value, 'ObjectID') || !utils.isObject(value)) {
+ isBsonType(value, 'ObjectId') || !utils.isObject(value)) {
value = { _id: value };
}
@@ -370,7 +374,15 @@ const methods = {
if (val != null && utils.hasUserDefinedProperty(val, '$each')) {
atomics.$push = val;
} else {
- atomics.$push.$each = atomics.$push.$each.concat(val);
+ if (val.length === 1) {
+ atomics.$push.$each.push(val[0]);
+ } else if (val.length < 10000) {
+ atomics.$push.$each.push(...val);
+ } else {
+ for (const v of val) {
+ atomics.$push.$each.push(v);
+ }
+ }
}
} else {
atomics[op] = val;
@@ -398,18 +410,20 @@ const methods = {
addToSet() {
_checkManualPopulation(this, arguments);
+ _depopulateIfNecessary(this, arguments);
- let values = [].map.call(arguments, this._mapCast, this);
- values = this[arraySchemaSymbol].applySetters(values, this[arrayParentSymbol]);
+ const values = [].map.call(arguments, this._mapCast, this);
const added = [];
let type = '';
if (values[0] instanceof ArraySubdocument) {
type = 'doc';
} else if (values[0] instanceof Date) {
type = 'date';
+ } else if (isBsonType(values[0], 'ObjectId')) {
+ type = 'ObjectId';
}
- const rawValues = utils.isMongooseArray(values) ? values.__array : this;
+ const rawValues = utils.isMongooseArray(values) ? values.__array : values;
const rawArray = utils.isMongooseArray(this) ? this.__array : this;
rawValues.forEach(function(v) {
@@ -426,8 +440,12 @@ const methods = {
return +d === val;
});
break;
+ case 'ObjectId':
+ found = this.find(o => o.toString() === v.toString());
+ break;
default:
found = ~this.indexOf(v);
+ break;
}
if (!found) {
@@ -486,7 +504,7 @@ const methods = {
*/
indexOf(obj, fromIndex) {
- if (isBsonType(obj, 'ObjectID')) {
+ if (isBsonType(obj, 'ObjectId')) {
obj = obj.toString();
}
@@ -540,7 +558,7 @@ const methods = {
*
* _marks the entire array as modified which will pass the entire thing to $set potentially overwriting any changes that happen between when you retrieved the object and when you save it._
*
- * @see MongooseArray#$pop #types_array_MongooseArray-%24pop
+ * @see MongooseArray#$pop https://mongoosejs.com/docs/api/array.html#MongooseArray.prototype.$pop()
* @api public
* @method pop
* @memberOf MongooseArray
@@ -556,7 +574,7 @@ const methods = {
/**
* Pulls items from the array atomically. Equality is determined by casting
* the provided value to an embedded document and comparing using
- * [the `Document.equals()` function.](/docs/api/document.html#document_Document-equals)
+ * [the `Document.equals()` function.](https://mongoosejs.com/docs/api/document.html#Document.prototype.equals())
*
* #### Example:
*
@@ -578,15 +596,18 @@ const methods = {
* The first pull call will result in a atomic operation on the database, if pull is called repeatedly without saving the document, a $set operation is used on the complete array instead, overwriting possible changes that happened on the database in the meantime.
*
* @param {...any} [args]
- * @see mongodb https://www.mongodb.org/display/DOCS/Updating/#Updating-%24pull
+ * @see mongodb https://www.mongodb.com/docs/manual/reference/operator/update/pull/
* @api public
* @method pull
* @memberOf MongooseArray
*/
pull() {
- const values = [].map.call(arguments, this._cast, this);
- const cur = this[arrayParentSymbol].get(this[arrayPathSymbol]);
+ const values = [].map.call(arguments, (v, i) => this._cast(v, i, { defaults: false }), this);
+ let cur = this[arrayParentSymbol].get(this[arrayPathSymbol]);
+ if (utils.isMongooseArray(cur)) {
+ cur = cur.__array;
+ }
let i = cur.length;
let mem;
this._markModified();
@@ -598,10 +619,10 @@ const methods = {
return mem.equals(v);
});
if (some) {
- [].splice.call(cur, i, 1);
+ cur.splice(i, 1);
}
- } else if (~cur.indexOf.call(values, mem)) {
- [].splice.call(cur, i, 1);
+ } else if (~this.indexOf.call(values, mem)) {
+ cur.splice(i, 1);
}
}
@@ -671,11 +692,9 @@ const methods = {
}
_checkManualPopulation(this, values);
+ _depopulateIfNecessary(this, values);
- const parent = this[arrayParentSymbol];
values = [].map.call(values, this._mapCast, this);
- values = this[arraySchemaSymbol].applySetters(values, parent, undefined,
- undefined, { skipDocumentArrayCast: true });
let ret;
const atomics = this[arrayAtomicsSymbol];
this._markModified();
@@ -684,35 +703,35 @@ const methods = {
if ((atomics.$push && atomics.$push.$each && atomics.$push.$each.length || 0) !== 0 &&
atomics.$push.$position != atomic.$position) {
- throw new MongooseError('Cannot call `Array#push()` multiple times ' +
- 'with different `$position`');
- }
+ if (atomic.$position != null) {
+ [].splice.apply(arr, [atomic.$position, 0].concat(values));
+ ret = arr.length;
+ } else {
+ ret = [].push.apply(arr, values);
+ }
- if (atomic.$position != null) {
+ this._registerAtomic('$set', this);
+ } else if (atomic.$position != null) {
[].splice.apply(arr, [atomic.$position, 0].concat(values));
ret = this.length;
} else {
ret = [].push.apply(arr, values);
}
} else {
- if ((atomics.$push && atomics.$push.$each && atomics.$push.$each.length || 0) !== 0 &&
- atomics.$push.$position != null) {
- throw new MongooseError('Cannot call `Array#push()` multiple times ' +
- 'with different `$position`');
- }
atomic = values;
- ret = [].push.apply(arr, values);
+ ret = _basePush.apply(arr, values);
}
this._registerAtomic('$push', atomic);
+
return ret;
},
/**
- * Alias of [pull](#mongoosearray_MongooseArray-pull)
+ * Alias of [pull](https://mongoosejs.com/docs/api/array.html#MongooseArray.prototype.pull())
*
- * @see MongooseArray#pull #types_array_MongooseArray-pull
- * @see mongodb https://www.mongodb.org/display/DOCS/Updating/#Updating-%24pull
+ * @see MongooseArray#pull https://mongoosejs.com/docs/api/array.html#MongooseArray.prototype.pull()
+ * @see mongodb https://www.mongodb.com/docs/manual/reference/operator/update/pull/
* @api public
* @memberOf MongooseArray
* @instance
@@ -799,7 +818,7 @@ const methods = {
* @api public
* @method sort
* @memberOf MongooseArray
- * @see https://masteringjs.io/tutorials/fundamentals/array-sort
+ * @see MasteringJS: Array sort https://masteringjs.io/tutorials/fundamentals/array-sort
*/
sort() {
@@ -819,7 +838,7 @@ const methods = {
* @api public
* @method splice
* @memberOf MongooseArray
- * @see https://masteringjs.io/tutorials/fundamentals/array-splice
+ * @see MasteringJS: Array splice https://masteringjs.io/tutorials/fundamentals/array-splice
*/
splice() {
@@ -870,7 +889,7 @@ const methods = {
toObject(options) {
const arr = utils.isMongooseArray(this) ? this.__array : this;
if (options && options.depopulate) {
- options = utils.clone(options);
+ options = clone(options);
options._isNested = true;
// Ensure return value is a vanilla array, because in Node.js 6+ `map()`
// is smart enough to use the inherited array's constructor.
@@ -907,7 +926,6 @@ const methods = {
values = arguments;
} else {
values = [].map.call(arguments, this._cast, this);
- values = this[arraySchemaSymbol].applySetters(values, this[arrayParentSymbol]);
}
const arr = utils.isMongooseArray(this) ? this.__array : this;
@@ -993,6 +1011,30 @@ function _checkManualPopulation(arr, docs) {
}
}
+/*!
+ * If `docs` isn't all instances of the right model, depopulate `arr`
+ */
+
+function _depopulateIfNecessary(arr, docs) {
+ const ref = arr == null ?
+ null :
+ arr[arraySchemaSymbol] && arr[arraySchemaSymbol].caster && arr[arraySchemaSymbol].caster.options && arr[arraySchemaSymbol].caster.options.ref || null;
+ const parentDoc = arr[arrayParentSymbol];
+ const path = arr[arrayPathSymbol];
+ if (!ref || !parentDoc.populated(path)) {
+ return;
+ }
+ for (const doc of docs) {
+ if (doc == null) {
+ continue;
+ }
+ if (typeof doc !== 'object' || doc instanceof String || doc instanceof Number || doc instanceof Buffer || utils.isMongooseType(doc)) {
+ parentDoc.depopulate(path);
+ break;
+ }
+ }
+}
+
const returnVanillaArrayMethods = [
'filter',
'flat',
diff --git a/lib/types/ArraySubdocument.js b/lib/types/arraySubdocument.js
similarity index 94%
rename from lib/types/ArraySubdocument.js
rename to lib/types/arraySubdocument.js
index 55889caa839..920088fae76 100644
--- a/lib/types/ArraySubdocument.js
+++ b/lib/types/arraySubdocument.js
@@ -33,7 +33,15 @@ function ArraySubdocument(obj, parentArr, skipId, fields, index) {
this.$setIndex(index);
this.$__parent = this[documentArrayParent];
- Subdocument.call(this, obj, fields, this[documentArrayParent], skipId, { isNew: true });
+ let options;
+ if (typeof skipId === 'object' && skipId != null) {
+ options = { isNew: true, ...skipId };
+ skipId = undefined;
+ } else {
+ options = { isNew: true };
+ }
+
+ Subdocument.call(this, obj, fields, this[documentArrayParent], skipId, options);
}
/*!
@@ -137,7 +145,7 @@ ArraySubdocument.prototype.$__fullPath = function(path, skipIndex) {
*/
ArraySubdocument.prototype.$__pathRelativeToParent = function(path, skipIndex) {
- if (this.__index == null) {
+ if (this.__index == null || (!this.__parentArray || !this.__parentArray.$path)) {
return null;
}
if (skipIndex) {
diff --git a/lib/types/buffer.js b/lib/types/buffer.js
index 2af81a5c13e..4e18a2ef477 100644
--- a/lib/types/buffer.js
+++ b/lib/types/buffer.js
@@ -4,7 +4,7 @@
'use strict';
-const Binary = require('../driver').get().Binary;
+const Binary = require('bson').Binary;
const utils = require('../utils');
/**
diff --git a/lib/types/decimal128.js b/lib/types/decimal128.js
index 15173385b72..1250b41a179 100644
--- a/lib/types/decimal128.js
+++ b/lib/types/decimal128.js
@@ -10,4 +10,4 @@
'use strict';
-module.exports = require('../driver').get().Decimal128;
+module.exports = require('bson').Decimal128;
diff --git a/lib/types/DocumentArray/index.js b/lib/types/documentArray/index.js
similarity index 89%
rename from lib/types/DocumentArray/index.js
rename to lib/types/documentArray/index.js
index 4877f1a30ef..863d40ae62b 100644
--- a/lib/types/DocumentArray/index.js
+++ b/lib/types/documentArray/index.js
@@ -28,7 +28,7 @@ const numberRE = /^\d+$/;
* @see https://bit.ly/f6CnZU
*/
-function MongooseDocumentArray(values, path, doc) {
+function MongooseDocumentArray(values, path, doc, schematype) {
const __array = [];
const internals = {
@@ -84,6 +84,9 @@ function MongooseDocumentArray(values, path, doc) {
if (DocumentArrayMethods.hasOwnProperty(prop)) {
return DocumentArrayMethods[prop];
}
+ if (schematype && schematype.virtuals && schematype.virtuals.hasOwnProperty(prop)) {
+ return schematype.virtuals[prop].applyGetters(undefined, target);
+ }
if (ArrayMethods.hasOwnProperty(prop)) {
return ArrayMethods[prop];
}
@@ -95,6 +98,8 @@ function MongooseDocumentArray(values, path, doc) {
DocumentArrayMethods.set.call(proxy, prop, value, false);
} else if (internals.hasOwnProperty(prop)) {
internals[prop] = value;
+ } else if (schematype && schematype.virtuals && schematype.virtuals.hasOwnProperty(prop)) {
+ schematype.virtuals[prop].applySetters(value, target);
} else {
__array[prop] = value;
}
diff --git a/lib/types/DocumentArray/isMongooseDocumentArray.js b/lib/types/documentArray/isMongooseDocumentArray.js
similarity index 100%
rename from lib/types/DocumentArray/isMongooseDocumentArray.js
rename to lib/types/documentArray/isMongooseDocumentArray.js
diff --git a/lib/types/DocumentArray/methods/index.js b/lib/types/documentArray/methods/index.js
similarity index 95%
rename from lib/types/DocumentArray/methods/index.js
rename to lib/types/documentArray/methods/index.js
index 4175901d37b..00b47c434ba 100644
--- a/lib/types/DocumentArray/methods/index.js
+++ b/lib/types/documentArray/methods/index.js
@@ -13,6 +13,8 @@ const arrayPathSymbol = require('../../../helpers/symbols').arrayPathSymbol;
const arraySchemaSymbol = require('../../../helpers/symbols').arraySchemaSymbol;
const documentArrayParent = require('../../../helpers/symbols').documentArrayParent;
+const _baseToString = Array.prototype.toString;
+
const methods = {
/*!
* ignore
@@ -22,6 +24,15 @@ const methods = {
return this.toObject(internalToObjectOptions);
},
+ toString() {
+ return _baseToString.call(this.__array.map(subdoc => {
+ if (subdoc != null && subdoc.$__ != null) {
+ return subdoc.toString();
+ }
+ return subdoc;
+ }));
+ },
+
/*!
* ignore
*/
@@ -38,7 +49,7 @@ const methods = {
* @memberOf MongooseDocumentArray
*/
- _cast(value, index) {
+ _cast(value, index, options) {
if (this[arraySchemaSymbol] == null) {
return value;
}
@@ -66,7 +77,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) ||
- isBsonType(value, 'ObjectID') || !utils.isObject(value)) {
+ isBsonType(value, 'ObjectId') || !utils.isObject(value)) {
value = { _id: value };
}
@@ -89,7 +100,7 @@ const methods = {
if (Constructor.$isMongooseDocumentArray) {
return Constructor.cast(value, this, undefined, undefined, index);
}
- const ret = new Constructor(value, this, undefined, undefined, index);
+ const ret = new Constructor(value, this, options, undefined, index);
ret.isNew = true;
return ret;
},
@@ -134,7 +145,7 @@ const methods = {
if (sid == _id._id) {
return val;
}
- } else if (!isBsonType(id, 'ObjectID') && !isBsonType(_id, 'ObjectID')) {
+ } else if (!isBsonType(id, 'ObjectId') && !isBsonType(_id, 'ObjectId')) {
if (id == _id || utils.deepEqual(id, _id)) {
return val;
}
diff --git a/lib/types/index.js b/lib/types/index.js
index fbfb89a5582..d234f6bb62a 100644
--- a/lib/types/index.js
+++ b/lib/types/index.js
@@ -9,12 +9,14 @@ exports.Array = require('./array');
exports.Buffer = require('./buffer');
exports.Document = // @deprecate
-exports.Embedded = require('./ArraySubdocument');
+exports.Embedded = require('./arraySubdocument');
-exports.DocumentArray = require('./DocumentArray');
+exports.DocumentArray = require('./documentArray');
exports.Decimal128 = require('./decimal128');
exports.ObjectId = require('./objectid');
exports.Map = require('./map');
exports.Subdocument = require('./subdocument');
+
+exports.UUID = require('./uuid');
diff --git a/lib/types/map.js b/lib/types/map.js
index c6510d49bc1..d3043be0581 100644
--- a/lib/types/map.js
+++ b/lib/types/map.js
@@ -9,6 +9,7 @@ const handleSpreadDoc = require('../helpers/document/handleSpreadDoc');
const util = require('util');
const specialProperties = require('../helpers/specialProperties');
const isBsonType = require('../helpers/isBsonType');
+const cleanModifiedSubpaths = require('../helpers/document/cleanModifiedSubpaths');
const populateModelSymbol = require('../helpers/symbols').populateModelSymbol;
@@ -52,7 +53,7 @@ class MongooseMap extends Map {
*/
get(key, options) {
- if (isBsonType(key, 'ObjectID')) {
+ if (isBsonType(key, 'ObjectId')) {
key = key.toString();
}
@@ -68,10 +69,17 @@ class MongooseMap extends Map {
* and change tracking. Note that Mongoose maps _only_ support strings and
* ObjectIds as keys.
*
+ * Keys also cannot:
+ * - be named after special properties `prototype`, `constructor`, and `__proto__`
+ * - start with a dollar sign (`$`)
+ * - contain any dots (`.`)
+ *
* #### Example:
*
* doc.myMap.set('test', 42); // works
* doc.myMap.set({ obj: 42 }, 42); // Throws "Mongoose maps only support string keys"
+ * doc.myMap.set(10, 42); // Throws "Mongoose maps only support string keys"
+ * doc.myMap.set("$test", 42); // Throws "Mongoose maps do not support keys that start with "$", got "$test""
*
* @api public
* @method set
@@ -79,7 +87,7 @@ class MongooseMap extends Map {
*/
set(key, value) {
- if (isBsonType(key, 'ObjectID')) {
+ if (isBsonType(key, 'ObjectId')) {
key = key.toString();
}
@@ -116,15 +124,15 @@ class MongooseMap extends Map {
v = new populated.options[populateModelSymbol](v);
}
// Doesn't support single nested "in-place" populate
- v.$__.wasPopulated = { value: v._id };
+ v.$__.wasPopulated = { value: v._doc._id };
return v;
});
- } else {
+ } else if (value != null) {
if (value.$__ == null) {
value = new populated.options[populateModelSymbol](value);
}
// Doesn't support single nested "in-place" populate
- value.$__.wasPopulated = { value: value._id };
+ value.$__.wasPopulated = { value: value._doc._id };
}
} else {
try {
@@ -150,7 +158,13 @@ class MongooseMap extends Map {
super.set(key, value);
if (parent != null && parent.$__ != null && !deepEqual(value, priorVal)) {
- parent.markModified(fullPath.call(this));
+ const path = fullPath.call(this);
+ parent.markModified(path);
+ // If overwriting the full document array or subdoc, make sure to clean up any paths that were modified
+ // before re: #15108
+ if (this.$__schemaType.$isMongooseDocumentArray || this.$__schemaType.$isSingleNested) {
+ cleanModifiedSubpaths(parent, path);
+ }
}
// Delay calculating full path unless absolutely necessary, because string
@@ -189,7 +203,7 @@ class MongooseMap extends Map {
*/
delete(key) {
- if (isBsonType(key, 'ObjectID')) {
+ if (isBsonType(key, 'ObjectId')) {
key = key.toString();
}
diff --git a/lib/types/objectid.js b/lib/types/objectid.js
index 63c3bd05c81..d38c223659b 100644
--- a/lib/types/objectid.js
+++ b/lib/types/objectid.js
@@ -10,7 +10,7 @@
'use strict';
-const ObjectId = require('../driver').get().ObjectId;
+const ObjectId = require('bson').ObjectId;
const objectIdSymbol = require('../helpers/symbols').objectIdSymbol;
/**
diff --git a/lib/types/subdocument.js b/lib/types/subdocument.js
index d282f892400..b1984d08ebf 100644
--- a/lib/types/subdocument.js
+++ b/lib/types/subdocument.js
@@ -3,7 +3,6 @@
const Document = require('../document');
const immediate = require('../helpers/immediate');
const internalToObjectOptions = require('../options').internalToObjectOptions;
-const promiseOrCallback = require('../helpers/promiseOrCallback');
const util = require('util');
const utils = require('../utils');
@@ -17,6 +16,10 @@ module.exports = Subdocument;
*/
function Subdocument(value, fields, parent, skipId, options) {
+ if (typeof skipId === 'object' && skipId != null && options == null) {
+ options = skipId;
+ skipId = undefined;
+ }
if (parent != null) {
// If setting a nested path, should copy isNew from parent re: gh-7048
const parentOptions = { isNew: parent.isNew };
@@ -67,11 +70,7 @@ Subdocument.prototype.toBSON = function() {
* @api private
*/
-Subdocument.prototype.save = function(options, fn) {
- if (typeof options === 'function') {
- fn = options;
- options = {};
- }
+Subdocument.prototype.save = async function save(options) {
options = options || {};
if (!options.suppressWarning) {
@@ -81,8 +80,13 @@ Subdocument.prototype.save = function(options, fn) {
'if you\'re sure this behavior is right for your app.');
}
- return promiseOrCallback(fn, cb => {
- this.$__save(cb);
+ return new Promise((resolve, reject) => {
+ this.$__save((err) => {
+ if (err != null) {
+ return reject(err);
+ }
+ resolve(this);
+ });
});
};
@@ -109,7 +113,7 @@ Subdocument.prototype.$__fullPath = function(path) {
/**
* Given a path relative to this document, return the path relative
- * to the top-level document.
+ * to the parent document.
* @param {String} p
* @returns {String}
* @method $__pathRelativeToParent
@@ -178,7 +182,7 @@ Subdocument.prototype.markModified = function(path) {
* ignore
*/
-Subdocument.prototype.isModified = function(paths, modifiedPaths) {
+Subdocument.prototype.isModified = function(paths, options, modifiedPaths) {
const parent = this.$parent();
if (parent != null) {
if (Array.isArray(paths) || typeof paths === 'string') {
@@ -188,10 +192,10 @@ Subdocument.prototype.isModified = function(paths, modifiedPaths) {
paths = this.$__pathRelativeToParent();
}
- return parent.$isModified(paths, modifiedPaths);
+ return parent.$isModified(paths, options, modifiedPaths);
}
- return Document.prototype.isModified.call(this, paths, modifiedPaths);
+ return Document.prototype.isModified.call(this, paths, options, modifiedPaths);
};
/**
@@ -332,13 +336,13 @@ Subdocument.prototype.$parent = Subdocument.prototype.parent;
/**
* no-op for hooks
* @param {Function} cb
- * @method $__remove
+ * @method $__deleteOne
* @memberOf Subdocument
* @instance
* @api private
*/
-Subdocument.prototype.$__remove = function(cb) {
+Subdocument.prototype.$__deleteOne = function(cb) {
if (cb == null) {
return;
}
@@ -364,7 +368,7 @@ Subdocument.prototype.$__removeFromParent = function() {
* @param {Function} [callback] optional callback for compatibility with Document.prototype.remove
*/
-Subdocument.prototype.remove = function(options, callback) {
+Subdocument.prototype.deleteOne = function(options, callback) {
if (typeof options === 'function') {
callback = options;
options = null;
@@ -374,9 +378,13 @@ Subdocument.prototype.remove = function(options, callback) {
// If removing entire doc, no need to remove subdoc
if (!options || !options.noop) {
this.$__removeFromParent();
+
+ const owner = this.ownerDocument();
+ owner.$__.removedSubdocs = owner.$__.removedSubdocs || [];
+ owner.$__.removedSubdocs.push(this);
}
- return this.$__remove(callback);
+ return this.$__deleteOne(callback);
};
/*!
@@ -396,11 +404,7 @@ Subdocument.prototype.populate = function() {
*/
Subdocument.prototype.inspect = function() {
- return this.toObject({
- transform: false,
- virtuals: false,
- flattenDecimals: false
- });
+ return this.toObject();
};
if (util.inspect.custom) {
@@ -417,16 +421,15 @@ if (util.inspect.custom) {
*/
function registerRemoveListener(sub) {
- let owner = sub.ownerDocument();
+ const owner = sub.ownerDocument();
function emitRemove() {
owner.$removeListener('save', emitRemove);
- owner.$removeListener('remove', emitRemove);
- sub.emit('remove', sub);
- sub.constructor.emit('remove', sub);
- owner = sub = null;
+ owner.$removeListener('deleteOne', emitRemove);
+ sub.emit('deleteOne', sub);
+ sub.constructor.emit('deleteOne', sub);
}
owner.$on('save', emitRemove);
- owner.$on('remove', emitRemove);
+ owner.$on('deleteOne', emitRemove);
}
diff --git a/lib/types/uuid.js b/lib/types/uuid.js
new file mode 100644
index 00000000000..fc9db855f7d
--- /dev/null
+++ b/lib/types/uuid.js
@@ -0,0 +1,13 @@
+/**
+ * UUID type constructor
+ *
+ * #### Example:
+ *
+ * const id = new mongoose.Types.UUID();
+ *
+ * @constructor UUID
+ */
+
+'use strict';
+
+module.exports = require('bson').UUID;
diff --git a/lib/utils.js b/lib/utils.js
index 923dc700222..6fc5c335ef0 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -8,13 +8,14 @@ const UUID = require('bson').UUID;
const ms = require('ms');
const mpath = require('mpath');
const ObjectId = require('./types/objectid');
-const PopulateOptions = require('./options/PopulateOptions');
+const PopulateOptions = require('./options/populateOptions');
const clone = require('./helpers/clone');
const immediate = require('./helpers/immediate');
const isObject = require('./helpers/isObject');
const isMongooseArray = require('./types/array/isMongooseArray');
-const isMongooseDocumentArray = require('./types/DocumentArray/isMongooseDocumentArray');
+const isMongooseDocumentArray = require('./types/documentArray/isMongooseDocumentArray');
const isBsonType = require('./helpers/isBsonType');
+const isPOJO = require('./helpers/isPOJO');
const getFunctionName = require('./helpers/getFunctionName');
const isMongooseObject = require('./helpers/isMongooseObject');
const promiseOrCallback = require('./helpers/promiseOrCallback');
@@ -31,6 +32,9 @@ exports.isMongooseDocumentArray = isMongooseDocumentArray.isMongooseDocumentArra
exports.registerMongooseArray = isMongooseArray.registerMongooseArray;
exports.registerMongooseDocumentArray = isMongooseDocumentArray.registerMongooseDocumentArray;
+const oneSpaceRE = /\s/;
+const manySpaceRE = /\s+/;
+
/**
* Produces a collection name from model `name`. By default, just returns
* the model name
@@ -49,6 +53,12 @@ exports.toCollectionName = function(name, pluralize) {
return name;
}
if (typeof pluralize === 'function') {
+ if (typeof name !== 'string') {
+ throw new TypeError('Collection name must be a string');
+ }
+ if (name.length === 0) {
+ throw new TypeError('Collection name cannot be empty');
+ }
return pluralize(name);
}
return name;
@@ -78,7 +88,7 @@ exports.deepEqual = function deepEqual(a, b) {
return a.getTime() === b.getTime();
}
- if ((isBsonType(a, 'ObjectID') && isBsonType(b, 'ObjectID')) ||
+ if ((isBsonType(a, 'ObjectId') && isBsonType(b, 'ObjectId')) ||
(isBsonType(a, 'Decimal128') && isBsonType(b, 'Decimal128'))) {
return a.toString() === b.toString();
}
@@ -187,8 +197,6 @@ exports.last = function(arr) {
return void 0;
};
-exports.clone = clone;
-
/*!
* ignore
*/
@@ -226,33 +234,6 @@ exports.omit = function omit(obj, keys) {
return ret;
};
-
-/**
- * Shallow copies defaults into options.
- *
- * @param {Object} defaults
- * @param {Object} [options]
- * @return {Object} the merged object
- * @api private
- */
-
-exports.options = function(defaults, options) {
- const keys = Object.keys(defaults);
- let i = keys.length;
- let k;
-
- options = options || {};
-
- while (i--) {
- k = keys[i];
- if (!(k in options)) {
- options[k] = defaults[k];
- }
- }
-
- return options;
-};
-
/**
* Merges `from` into `to` without overwriting existing properties.
*
@@ -290,7 +271,13 @@ exports.merge = function merge(to, from, options, path) {
continue;
}
if (to[key] == null) {
- to[key] = from[key];
+ if (isPOJO(from[key])) {
+ to[key] = { ...from[key] };
+ } else if (Array.isArray(from[key])) {
+ to[key] = [...from[key]];
+ } else {
+ to[key] = from[key];
+ }
} else if (exports.isObject(from[key])) {
if (!exports.isObject(to[key])) {
to[key] = {};
@@ -310,7 +297,7 @@ exports.merge = function merge(to, from, options, path) {
to[key] = from[key].clone();
}
continue;
- } else if (isBsonType(from[key], 'ObjectID')) {
+ } else if (isBsonType(from[key], 'ObjectId')) {
to[key] = new ObjectId(from[key]);
continue;
}
@@ -320,6 +307,8 @@ exports.merge = function merge(to, from, options, path) {
to[key] = from[key];
}
}
+
+ return to;
};
/**
@@ -386,16 +375,7 @@ exports.isObject = isObject;
* @return {Boolean}
*/
-exports.isPOJO = function isPOJO(arg) {
- if (arg == null || typeof arg !== 'object') {
- return false;
- }
- const proto = Object.getPrototypeOf(arg);
- // Prototype may be null if you used `Object.create(null)`
- // Checking `proto`'s constructor is safe because `getPrototypeOf()`
- // explicitly crosses the boundary from object data to object metadata
- return !proto || proto.constructor.name === 'Object';
-};
+exports.isPOJO = require('./helpers/isPOJO');
/**
* Determines if `arg` is an object that isn't an instance of a built-in value
@@ -491,7 +471,7 @@ exports.tick = function tick(callback) {
*/
exports.isMongooseType = function(v) {
- return isBsonType(v, 'ObjectID') || isBsonType(v, 'Decimal128') || v instanceof Buffer;
+ return isBsonType(v, 'ObjectId') || isBsonType(v, 'Decimal128') || v instanceof Buffer;
};
exports.isMongooseObject = isMongooseObject;
@@ -536,7 +516,7 @@ exports.populate = function populate(path, select, model, match, options, subPop
if (path instanceof PopulateOptions) {
// If reusing old populate docs, avoid reusing `_docs` because that may
// lead to bugs and memory leaks. See gh-11641
- path._docs = [];
+ path._docs = {};
path._childDocs = [];
return [path];
}
@@ -583,8 +563,8 @@ exports.populate = function populate(path, select, model, match, options, subPop
function makeSingles(arr) {
const ret = [];
arr.forEach(function(obj) {
- if (/[\s]/.test(obj.path)) {
- const paths = obj.path.split(' ');
+ if (oneSpaceRE.test(obj.path)) {
+ const paths = obj.path.split(manySpaceRE);
paths.forEach(function(p) {
const copy = Object.assign({}, obj);
copy.path = p;
@@ -603,9 +583,9 @@ function _populateObj(obj) {
if (Array.isArray(obj.populate)) {
const ret = [];
obj.populate.forEach(function(obj) {
- if (/[\s]/.test(obj.path)) {
+ if (oneSpaceRE.test(obj.path)) {
const copy = Object.assign({}, obj);
- const paths = copy.path.split(' ');
+ const paths = copy.path.split(manySpaceRE);
paths.forEach(function(p) {
copy.path = p;
ret.push(exports.populate(copy)[0]);
@@ -620,9 +600,9 @@ function _populateObj(obj) {
}
const ret = [];
- const paths = obj.path.split(' ');
+ const paths = oneSpaceRE.test(obj.path) ? obj.path.split(manySpaceRE) : [obj.path];
if (obj.options != null) {
- obj.options = exports.clone(obj.options);
+ obj.options = clone(obj.options);
}
for (const path of paths) {
@@ -641,9 +621,28 @@ function _populateObj(obj) {
*/
exports.getValue = function(path, obj, map) {
- return mpath.get(path, obj, '_doc', map);
+ return mpath.get(path, obj, getValueLookup, map);
};
+/*!
+ * ignore
+ */
+
+const mapGetterOptions = Object.freeze({ getters: false });
+
+function getValueLookup(obj, part) {
+ if (part === '$*' && obj instanceof Map) {
+ return obj;
+ }
+ let _from = obj?._doc || obj;
+ if (_from != null && _from.isMongooseArrayProxy) {
+ _from = _from.__array;
+ }
+ return _from instanceof Map ?
+ _from.get(part, mapGetterOptions) :
+ _from[part];
+}
+
/**
* Sets the value of `obj` at the given `path`.
*
@@ -679,12 +678,6 @@ exports.object.vals = function vals(o) {
return ret;
};
-/**
- * @see exports.options
- */
-
-exports.object.shallowCopy = exports.options;
-
const hop = Object.prototype.hasOwnProperty;
/**
@@ -819,7 +812,7 @@ exports.array.unique = function(arr) {
}
ret.push(item);
primitives.add(item);
- } else if (isBsonType(item, 'ObjectID')) {
+ } else if (isBsonType(item, 'ObjectId')) {
if (ids.has(item.toString())) {
continue;
}
@@ -906,7 +899,7 @@ exports.mergeClone = function(to, fromObj) {
continue;
}
if (typeof to[key] === 'undefined') {
- to[key] = exports.clone(fromObj[key], {
+ to[key] = clone(fromObj[key], {
transform: false,
virtuals: false,
depopulate: true,
@@ -934,7 +927,7 @@ exports.mergeClone = function(to, fromObj) {
}
exports.mergeClone(to[key], obj);
} else {
- to[key] = exports.clone(val, {
+ to[key] = clone(val, {
flattenDecimals: false
});
}
@@ -956,6 +949,29 @@ exports.each = function(arr, fn) {
}
};
+/**
+ * Rename an object key, while preserving its position in the object
+ *
+ * @param {Object} oldObj
+ * @param {String|Number} oldKey
+ * @param {String|Number} newKey
+ * @api private
+ */
+exports.renameObjKey = function(oldObj, oldKey, newKey) {
+ const keys = Object.keys(oldObj);
+ return keys.reduce(
+ (acc, val) => {
+ if (val === oldKey) {
+ acc[newKey] = oldObj[oldKey];
+ } else {
+ acc[val] = oldObj[val];
+ }
+ return acc;
+ },
+ {}
+ );
+};
+
/*!
* ignore
*/
diff --git a/lib/validoptions.js b/lib/validOptions.js
similarity index 81%
rename from lib/validoptions.js
rename to lib/validOptions.js
index a42e552c7af..6c09480def1 100644
--- a/lib/validoptions.js
+++ b/lib/validOptions.js
@@ -11,10 +11,13 @@ const VALID_OPTIONS = Object.freeze([
'applyPluginsToDiscriminators',
'autoCreate',
'autoIndex',
+ 'autoSearchIndex',
'bufferCommands',
'bufferTimeoutMS',
'cloneSchemas',
+ 'createInitialConnection',
'debug',
+ 'forceRepopulate',
'id',
'timestamps.createdAt.immutable',
'maxTimeMS',
@@ -30,7 +33,9 @@ const VALID_OPTIONS = Object.freeze([
'strictPopulate',
'strictQuery',
'toJSON',
- 'toObject'
+ 'toObject',
+ 'transactionAsyncLocalStorage',
+ 'translateAliases'
]);
module.exports = VALID_OPTIONS;
diff --git a/lib/virtualtype.js b/lib/virtualType.js
similarity index 84%
rename from lib/virtualtype.js
rename to lib/virtualType.js
index 6efa01c096e..2008ebf8bb4 100644
--- a/lib/virtualtype.js
+++ b/lib/virtualType.js
@@ -1,7 +1,10 @@
'use strict';
+const modelNamesFromRefPath = require('./helpers/populate/modelNamesFromRefPath');
const utils = require('./utils');
+const modelSymbol = require('./helpers/symbols').modelSymbol;
+
/**
* VirtualType constructor
*
@@ -13,12 +16,12 @@ const utils = require('./utils');
* fullname instanceof mongoose.VirtualType // true
*
* @param {Object} options
- * @param {String|Function} [options.ref] if `ref` is not nullish, this becomes a [populated virtual](/docs/populate.html#populate-virtuals)
+ * @param {String|Function} [options.ref] if `ref` is not nullish, this becomes a [populated virtual](https://mongoosejs.com/docs/populate.html#populate-virtuals)
* @param {String|Function} [options.localField] the local field to populate on if this is a populated virtual.
* @param {String|Function} [options.foreignField] the foreign field to populate on if this is a populated virtual.
* @param {Boolean} [options.justOne=false] by default, a populated virtual is an array. If you set `justOne`, the populated virtual will be a single doc or `null`.
* @param {Boolean} [options.getters=false] if you set this to `true`, Mongoose will call any custom getters you defined on this virtual
- * @param {Boolean} [options.count=false] if you set this to `true`, `populate()` will set this virtual to the number of populated documents, as opposed to the documents themselves, using [`Query#countDocuments()`](/docs/api/query.html#query_Query-countDocuments)
+ * @param {Boolean} [options.count=false] if you set this to `true`, `populate()` will set this virtual to the number of populated documents, as opposed to the documents themselves, using [`Query#countDocuments()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.countDocuments())
* @param {Object|Function} [options.match=null] add an extra match condition to `populate()`
* @param {Number} [options.limit=null] add a default `limit` to the `populate()` query
* @param {Number} [options.skip=null] add a default `skip` to the `populate()` query
@@ -168,6 +171,32 @@ VirtualType.prototype.applySetters = function(value, doc) {
return v;
};
+/**
+ * Get the names of models used to populate this model given a doc
+ *
+ * @param {Document} doc
+ * @return {Array | null}
+ * @api private
+ */
+
+VirtualType.prototype._getModelNamesForPopulate = function _getModelNamesForPopulate(doc) {
+ if (this.options.refPath) {
+ return modelNamesFromRefPath(this.options.refPath, doc, this.path);
+ }
+
+ let normalizedRef = null;
+ if (typeof this.options.ref === 'function' && !this.options.ref[modelSymbol]) {
+ normalizedRef = this.options.ref.call(doc, doc);
+ } else {
+ normalizedRef = this.options.ref;
+ }
+ if (normalizedRef != null && !Array.isArray(normalizedRef)) {
+ return [normalizedRef];
+ }
+
+ return normalizedRef;
+};
+
/*!
* exports
*/
diff --git a/migrating_to_5.md b/migrating_to_5.md
index 86e85e51006..cb41f30b4ee 100644
--- a/migrating_to_5.md
+++ b/migrating_to_5.md
@@ -1 +1 @@
-This guide has moved to the [Mongoose docs site](https://mongoosejs.com/docs/migrating_to_5.html).
\ No newline at end of file
+This guide has moved to the [Mongoose docs site](https://mongoosejs.com/docs/migrating_to_5.html).
diff --git a/package.json b/package.json
index 6fa02483ba4..663a81830ea 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "mongoose",
"description": "Mongoose MongoDB ODM",
- "version": "6.11.3",
+ "version": "8.9.5",
"author": "Guillermo Rauch ",
"keywords": [
"mongodb",
@@ -19,56 +19,53 @@
],
"license": "MIT",
"dependencies": {
- "bson": "^4.7.2",
- "kareem": "2.5.1",
- "mongodb": "4.16.0",
+ "bson": "^6.10.1",
+ "kareem": "2.6.3",
+ "mongodb": "~6.12.0",
"mpath": "0.9.0",
- "mquery": "4.0.3",
+ "mquery": "5.0.0",
"ms": "2.1.3",
- "sift": "16.0.1"
+ "sift": "17.1.3"
},
"devDependencies": {
- "@babel/core": "7.20.12",
- "@babel/preset-env": "7.20.2",
- "@typescript-eslint/eslint-plugin": "5.50.0",
- "@typescript-eslint/parser": "5.50.0",
+ "@babel/core": "7.26.0",
+ "@babel/preset-env": "7.26.0",
+ "@typescript-eslint/eslint-plugin": "^8.19.1",
+ "@typescript-eslint/parser": "^8.19.1",
"acquit": "1.3.0",
"acquit-ignore": "0.2.1",
"acquit-require": "0.1.1",
"assert-browserify": "2.0.0",
- "axios": "1.1.3",
"babel-loader": "8.2.5",
- "benchmark": "2.1.4",
- "bluebird": "3.7.2",
"broken-link-checker": "^0.7.8",
"buffer": "^5.6.0",
- "cheerio": "1.0.0-rc.12",
- "crypto-browserify": "3.12.0",
- "dotenv": "16.0.3",
+ "cheerio": "1.0.0",
+ "crypto-browserify": "3.12.1",
"dox": "1.0.0",
- "eslint": "8.33.0",
- "eslint-plugin-markdown": "^3.0.0",
- "eslint-plugin-mocha-no-only": "1.1.1",
- "express": "^4.18.1",
- "highlight.js": "11.7.0",
+ "eslint": "8.57.1",
+ "eslint-plugin-markdown": "^5.1.0",
+ "eslint-plugin-mocha-no-only": "1.2.0",
+ "express": "^4.19.2",
+ "fs-extra": "~11.2.0",
+ "highlight.js": "11.11.1",
"lodash.isequal": "4.5.0",
"lodash.isequalwith": "4.4.0",
- "marked": "4.2.12",
- "mkdirp": "^2.1.3",
- "mocha": "10.2.0",
- "moment": "2.x",
- "mongodb-memory-server": "8.11.4",
+ "markdownlint-cli2": "^0.17.1",
+ "marked": "15.0.4",
+ "mkdirp": "^3.0.1",
+ "mocha": "11.0.1",
+ "moment": "2.30.1",
+ "mongodb-memory-server": "10.1.3",
"ncp": "^2.0.0",
"nyc": "15.1.0",
- "pug": "3.0.2",
+ "pug": "3.0.3",
"q": "1.5.1",
- "sinon": "15.0.1",
+ "sinon": "19.0.2",
"stream-browserify": "3.0.0",
- "tsd": "0.25.0",
- "typescript": "4.9.5",
- "uuid": "9.0.0",
- "webpack": "5.75.0",
- "fs-extra": "~11.1.1"
+ "tsd": "0.31.2",
+ "typescript": "5.7.2",
+ "uuid": "11.0.3",
+ "webpack": "5.97.1"
},
"directories": {
"lib": "./lib/mongoose"
@@ -80,33 +77,27 @@
"docs:clean:6x": "rimraf index.html && rimraf -rf ./docs/6.x && rimraf -rf ./docs/source/_docs && rimraf -rf ./tmp",
"docs:copy:tmp": "mkdirp ./tmp/docs/css && mkdirp ./tmp/docs/js && mkdirp ./tmp/docs/images && mkdirp ./tmp/docs/tutorials && mkdirp ./tmp/docs/typescript && mkdirp ./tmp/docs/api && ncp ./docs/css ./tmp/docs/css --filter=.css$ && ncp ./docs/js ./tmp/docs/js --filter=.js$ && ncp ./docs/images ./tmp/docs/images && ncp ./docs/tutorials ./tmp/docs/tutorials && ncp ./docs/typescript ./tmp/docs/typescript && ncp ./docs/api ./tmp/docs/api && cp index.html ./tmp && cp docs/*.html ./tmp/docs/",
"docs:copy:tmp:5x": "rimraf ./docs/5.x && ncp ./tmp ./docs/5.x",
- "docs:move:6x:tmp": "mv ./docs/6.x ./tmp",
"docs:copy:tmp:6x": "rimraf ./docs/6.x && ncp ./tmp ./docs/6.x",
- "docs:checkout:gh-pages": "git checkout gh-pages",
- "docs:checkout:5x": "git checkout 5.x",
- "docs:checkout:6x": "git checkout 6.x",
"docs:generate": "node ./scripts/website.js",
- "docs:generate:search": "node ./scripts/generateSearch.js",
"docs:generate:sponsorData": "node ./scripts/loadSponsorData.js",
- "docs:merge:stable": "git merge master",
- "docs:merge:5x": "git merge 5.x",
- "docs:merge:6x": "git merge 6.x",
- "docs:test": "npm run docs:generate && npm run docs:generate:search",
+ "docs:test": "npm run docs:generate",
"docs:view": "node ./scripts/static.js",
- "docs:prepare:publish:stable": "npm run docs:checkout:gh-pages && npm run docs:merge:stable && npm run docs:clean:stable && npm run docs:generate && npm run docs:generate:search",
- "docs:prepare:publish:5x": "npm run docs:checkout:5x && npm run docs:merge:5x && npm run docs:clean:stable && npm run docs:generate && npm run docs:copy:tmp && npm run docs:checkout:gh-pages && npm run docs:copy:tmp:5x",
- "docs:prepare:publish:6x": "npm run docs:checkout:6x && npm run docs:merge:6x && npm run docs:clean:stable && env DOCS_DEPLOY=true npm run docs:generate && npm run docs:move:6x:tmp && npm run docs:checkout:gh-pages && npm run docs:copy:tmp:6x",
+ "docs:prepare:publish:stable": "git checkout gh-pages && git merge master && npm run docs:generate",
+ "docs:prepare:publish:5x": "git checkout 5.x && git merge 5.x && npm run docs:clean:stable && npm run docs:generate && npm run docs:copy:tmp && git checkout gh-pages && npm run docs:copy:tmp:5x",
+ "docs:prepare:publish:6x": "git checkout 6.x && git merge 6.x && npm run docs:clean:stable && env DOCS_DEPLOY=true npm run docs:generate && mv ./docs/6.x ./tmp && git checkout gh-pages && npm run docs:copy:tmp:6x",
+ "docs:prepare:publish:7x": "env DOCS_DEPLOY=true npm run docs:generate && git checkout gh-pages && rimraf ./docs/7.x && mv ./tmp ./docs/7.x",
"docs:check-links": "blc http://127.0.0.1:8089 -ro",
"lint": "eslint .",
- "lint-js": "eslint . --ext .js",
+ "lint-js": "eslint . --ext .js --ext .cjs",
"lint-ts": "eslint . --ext .ts",
- "lint-md": "eslint . --ext .md",
+ "lint-md": "markdownlint-cli2 \"**/*.md\" \"#node_modules\" \"#benchmarks\"",
"build-browser": "(rm ./dist/* || true) && node ./scripts/build-browser.js",
"prepublishOnly": "npm run build-browser",
"release": "git pull && git push origin master --tags && npm publish",
"release-5x": "git pull origin 5.x && git push origin 5.x && git push origin 5.x --tags && npm publish --tag 5x",
- "release-6x": "git pull origin 6.x && git push origin 6.x && git push origin 6.x --tags && npm publish --tag legacy",
+ "release-6x": "git pull origin 6.x && git push origin 6.x && git push origin 6.x --tags && npm publish --tag 6x",
"mongo": "node ./tools/repl.js",
+ "publish-7x": "npm publish --tag 7x",
"test": "mocha --exit ./test/*.test.js",
"test-deno": "deno run --allow-env --allow-read --allow-net --allow-run --allow-sys --allow-write ./test/deno.js",
"test-rs": "START_REPLICA_SET=1 mocha --timeout 30000 --exit ./test/*.test.js",
@@ -118,7 +109,7 @@
"main": "./index.js",
"types": "./types/index.d.ts",
"engines": {
- "node": ">=12.0.0"
+ "node": ">=16.20.1"
},
"bugs": {
"url": "https://github.com/Automattic/mongoose/issues/new"
@@ -129,14 +120,6 @@
},
"homepage": "https://mongoosejs.com",
"browser": "./dist/browser.umd.js",
- "mocha": {
- "extension": [
- "test.js"
- ],
- "watch-files": [
- "test/**/*.js"
- ]
- },
"config": {
"mongodbMemoryServer": {
"disablePostinstall": true
diff --git a/release-items.md b/release-items.md
index 9ceb5922cab..afef471f61b 100644
--- a/release-items.md
+++ b/release-items.md
@@ -1,14 +1,16 @@
+# Release prodecure
+
## mongoose release procedure
-1. tests must pass
-2. update `package.json` and `package-lock.json` version
-3. update `CHANGELOG.md`. Add # as well as a link to the github user who fixed it if applicable.
-4. git commit -a -m 'release x.x.x'
-5. git tag x.x.x
-6. `npm run release`, or `npm run release-legacy` for 4.x
-7. update mongoosejs.com (see "updating the website" below)
-8. tweet changelog link from [@mongoosejs](https://twitter.com/mongoosejs)
-9. Announce on mongoosejsteam slack channel
+1. tests must pass
+2. update `package.json` and `package-lock.json` version
+3. update `CHANGELOG.md`. Add # as well as a link to the github user who fixed it if applicable.
+4. git commit -a -m 'release x.x.x'
+5. git tag x.x.x
+6. `npm run release`, or `npm run release-legacy` for 4.x
+7. update mongoosejs.com (see "updating the website" below)
+8. tweet changelog link from [@mongoosejs](https://twitter.com/mongoosejs)
+9. Announce on mongoosejsteam slack channel
10. if this is a legacy release, `git merge` changes into master.
## updating the website
diff --git a/scripts/generateSearch.js b/scripts/generateSearch.js
index 56103488f8b..5497044668e 100644
--- a/scripts/generateSearch.js
+++ b/scripts/generateSearch.js
@@ -1,6 +1,14 @@
'use strict';
-const config = require('../.config');
+let config;
+try {
+ config = require('../.config.js');
+} finally {
+ if (!config || !config.uri) {
+ console.error('No Config or config.URI given, please create a .config.js file with those values in the root of the repository');
+ process.exit(-1);
+ }
+}
const cheerio = require('cheerio');
const docsFilemap = require('../docs/source');
const fs = require('fs');
@@ -37,7 +45,7 @@ for (const [filename, file] of Object.entries(docsFilemap.fileMap)) {
const content = new Content({
title: `API: ${prop.name}`,
body: prop.description,
- url: `${filename.replace(/^docs/, '')}#${prop.anchorId}`
+ url: `${filename}#${prop.anchorId}`
});
const err = content.validateSync();
if (err != null) {
@@ -118,28 +126,28 @@ run().catch(async error => {
});
async function run() {
- if (!config || !config.uri) {
- console.error('No Config or config.URI given, please create a .config.js file with those values');
- process.exit(-1);
- }
-
- await mongoose.connect(config.uri, { dbName: 'mongoose', serverSelectionTimeoutMS: 5000 });
+ await mongoose.connect(config.uri, { dbName: 'mongoose' });
// wait for the index to be created
await Content.init();
await Content.deleteMany({ version });
+ let count = 0;
for (const content of contents) {
- if (version === '7.x') {
+ if (version === '8.x') {
let url = content.url.startsWith('/') ? content.url : `/${content.url}`;
if (!url.startsWith('/docs')) {
url = '/docs' + url;
}
content.url = url;
} else {
- const url = content.url.startsWith('/') ? content.url : `/${content.url}`;
- content.url = `/docs/${version}/docs${url}`;
+ let url = content.url.startsWith('/') ? content.url : `/${content.url}`;
+ if (!url.startsWith('/docs')) {
+ url = '/docs' + url;
+ }
+ content.url = `/docs/${version}${url}`;
}
+ console.log(`${++count} / ${contents.length}`);
await content.save();
}
@@ -150,5 +158,7 @@ async function run() {
console.log(results.map(res => res.url));
+ console.log(`Added ${contents.length} Content`);
+
process.exit(0);
}
diff --git a/docs/loadSponsorData.js b/scripts/loadSponsorData.js
similarity index 77%
rename from docs/loadSponsorData.js
rename to scripts/loadSponsorData.js
index 20c8ecb27de..0a6b4d6baff 100644
--- a/docs/loadSponsorData.js
+++ b/scripts/loadSponsorData.js
@@ -9,7 +9,7 @@ try {
process.exit(-1);
}
}
-const axios = require('axios');
+
const fs = require('fs');
const mongoose = require('../');
@@ -18,6 +18,9 @@ run().catch(err => {
process.exit(-1);
});
+// only "." because fs resolves relative to the CWD instead of relative to __dirname
+const docsDir = './docs';
+
async function run() {
await mongoose.connect(config.uri);
@@ -48,8 +51,7 @@ async function run() {
const OpenCollectiveSponsor = mongoose.model('OpenCollectiveSponsor', mongoose.Schema({
openCollectiveId: {
- type: Number,
- required: true
+ type: Number
},
website: {
type: String,
@@ -65,24 +67,21 @@ async function run() {
}), 'OpenCollectiveSponsor');
try {
- fs.mkdirSync(`${__dirname}/data`);
+ fs.mkdirSync(`${docsDir}/data`);
} catch (err) {}
const subscribers = await Subscriber.
find({ companyName: { $exists: true }, description: { $exists: true }, logo: { $exists: true } }).
sort({ createdAt: 1 }).
select({ companyName: 1, description: 1, url: 1, logo: 1 });
- fs.writeFileSync(`${__dirname}/data/sponsors.json`, JSON.stringify(subscribers, null, ' '));
+ fs.writeFileSync(`${docsDir}/data/sponsors.json`, JSON.stringify(subscribers, null, ' '));
const jobs = await Job.find().select({ logo: 1, company: 1, title: 1, location: 1, description: 1, url: 1 });
- fs.writeFileSync(`${__dirname}/data/jobs.json`, JSON.stringify(jobs, null, ' '));
+ fs.writeFileSync(`${docsDir}/data/jobs.json`, JSON.stringify(jobs, null, ' '));
- const opencollectiveSponsors = await axios.get('https://opencollective.com/mongoose/members.json').
- then(res => res.data).
- then(sponsors => {
- return sponsors.filter(result => result.tier == 'sponsor' && result.isActive);
- }).
- catch(() => null);
+ const opencollectiveSponsors = await fetch('https://opencollective.com/mongoose/members.json')
+ .then(res => res.json())
+ .then(res => res.filter(result => result.tier === 'sponsor' && result.isActive));
for (const sponsor of opencollectiveSponsors) {
const override = await OpenCollectiveSponsor.findOne({ openCollectiveId: sponsor['MemberId'] });
@@ -104,7 +103,7 @@ async function run() {
}
if (opencollectiveSponsors != null) {
- fs.writeFileSync(`${__dirname}/data/opencollective.json`, JSON.stringify(opencollectiveSponsors, null, ' '));
+ fs.writeFileSync(`${docsDir}/data/opencollective.json`, JSON.stringify(opencollectiveSponsors, null, ' '));
}
console.log('Done');
diff --git a/scripts/tsc-diagnostics-check.js b/scripts/tsc-diagnostics-check.js
index d68e64431ec..a13c884ee02 100644
--- a/scripts/tsc-diagnostics-check.js
+++ b/scripts/tsc-diagnostics-check.js
@@ -3,7 +3,7 @@
const fs = require('fs');
const stdin = fs.readFileSync(0).toString('utf8');
-const maxInstantiations = isNaN(process.argv[2]) ? 100000 : parseInt(process.argv[2], 10);
+const maxInstantiations = isNaN(process.argv[2]) ? 275000 : parseInt(process.argv[2], 10);
console.log(stdin);
diff --git a/scripts/website.js b/scripts/website.js
index 709a0eb2df2..6c01c3b4ad9 100644
--- a/scripts/website.js
+++ b/scripts/website.js
@@ -4,6 +4,7 @@ Error.stackTraceLimit = Infinity;
const acquit = require('acquit');
const fs = require('fs');
+const fsextra = require('fs-extra');
const path = require('path');
const pug = require('pug');
const pkg = require('../package.json');
@@ -14,6 +15,12 @@ const childProcess = require("child_process");
// also a consistent root path so that it is easy to change later when the script should be moved
const cwd = path.resolve(__dirname, '..');
+// support custom heading ids
+// see https://www.markdownguide.org/extended-syntax/#heading-ids
+// Example:
+// # Some Header {#custom-id}
+const CustomIdRegex = /{#([a-zA-Z0-9_-]+)}(?: *)$/;
+
const isMain = require.main === module;
let jobs = [];
@@ -28,59 +35,140 @@ try {
require('acquit-ignore')();
-const { marked: markdown } = require('marked');
+const markdown = require('marked');
const highlight = require('highlight.js');
-const { promisify } = require("util");
-const renderer = {
- heading: function(text, level, raw, slugger) {
- const slug = slugger.slug(raw);
- return `
-
- ${text}
-
- \n`;
- }
-};
-markdown.setOptions({
- highlight: function(code, language) {
- if (!language) {
- language = 'javascript';
- }
- if (language === 'no-highlight') {
- return code;
+const { promisify } = require('util');
+
+markdown.use({
+ renderer: {
+ heading: function({ tokens, depth }) {
+ let raw = this.parser.parseInline(tokens);
+ let slug;
+ const idMatch = CustomIdRegex.exec(raw);
+
+ // use custom header id if available, otherwise fallback to default slugger
+ if (idMatch) {
+ slug = idMatch[1];
+ raw = raw.replace(CustomIdRegex, '');
+ } else {
+ slug = createSlug(raw.trim());
+ }
+ return `
+
+ ${raw}
+
+ \n`;
+ },
+ code: function({ text, lang }) {
+ if (!lang || lang === 'acquit') {
+ lang = 'javascript';
+ }
+ if (lang === 'no-highlight') {
+ return text;
+ }
+ return `${highlight.highlight(text, { language: lang }).value} `;
}
- return highlight.highlight(code, { language }).value;
}
});
-markdown.use({ renderer });
-
-const testPath = path.resolve(cwd, 'test')
-
-const tests = [
- ...acquit.parse(fs.readFileSync(path.join(testPath, 'geojson.test.js')).toString()),
- ...acquit.parse(fs.readFileSync(path.join(testPath, 'docs/transactions.test.js')).toString()),
- ...acquit.parse(fs.readFileSync(path.join(testPath, 'schema.alias.test.js')).toString()),
- ...acquit.parse(fs.readFileSync(path.join(testPath, 'model.middleware.test.js')).toString()),
- ...acquit.parse(fs.readFileSync(path.join(testPath, 'docs/date.test.js')).toString()),
- ...acquit.parse(fs.readFileSync(path.join(testPath, 'docs/lean.test.js')).toString()),
- ...acquit.parse(fs.readFileSync(path.join(testPath, 'docs/cast.test.js')).toString()),
- ...acquit.parse(fs.readFileSync(path.join(testPath, 'docs/findoneandupdate.test.js')).toString()),
- ...acquit.parse(fs.readFileSync(path.join(testPath, 'docs/custom-casting.test.js')).toString()),
- ...acquit.parse(fs.readFileSync(path.join(testPath, 'docs/getters-setters.test.js')).toString()),
- ...acquit.parse(fs.readFileSync(path.join(testPath, 'docs/virtuals.test.js')).toString()),
- ...acquit.parse(fs.readFileSync(path.join(testPath, 'docs/defaults.test.js')).toString()),
- ...acquit.parse(fs.readFileSync(path.join(testPath, 'docs/discriminators.test.js')).toString()),
- ...acquit.parse(fs.readFileSync(path.join(testPath, 'docs/promises.test.js')).toString()),
- ...acquit.parse(fs.readFileSync(path.join(testPath, 'docs/schematypes.test.js')).toString()),
- ...acquit.parse(fs.readFileSync(path.join(testPath, 'docs/validation.test.js')).toString()),
- ...acquit.parse(fs.readFileSync(path.join(testPath, 'docs/schemas.test.js')).toString())
+
+const testPath = path.resolve(cwd, 'test');
+
+/** additional test files to scan, relative to `test/` */
+const additionalTestFiles = [
+ 'geojson.test.js',
+ 'schema.alias.test.js'
];
+/** ignored files from `test/docs/` */
+const ignoredTestFiles = [
+ // ignored because acquit does not like "for await"
+ 'asyncIterator.test.js'
+];
+
+/**
+ * Load all test file contents with acquit
+ * @returns {Object[]} acquit ast array
+ */
+function getTests() {
+ const testDocs = path.resolve(testPath, 'docs');
+ const filesToScan = [
+ ...additionalTestFiles.map(v => path.join(testPath, v)),
+ ...fs.readdirSync(testDocs).filter(v => !ignoredTestFiles.includes(v)).map(v => path.join(testDocs, v))
+ ];
+
+ const retArray = [];
+
+ for (const file of filesToScan) {
+ try {
+ retArray.push(acquit.parse(fs.readFileSync(file).toString()));
+ } catch (err) {
+ // add a file path to a acquit error, for better debugging
+ err.filePath = file;
+ throw err;
+ }
+ }
+
+ return retArray.flat();
+}
-/**
+function deleteAllHtmlFiles() {
+ try {
+ console.log('Delete', path.join(versionObj.versionedPath, 'index.html'));
+ fs.unlinkSync(path.join(versionObj.versionedPath, 'index.html'));
+ } catch (err) {
+ if (err.code !== 'ENOENT') {
+ throw err;
+ }
+ }
+ const foldersToClean = [
+ path.join('.', versionObj.versionedPath, 'docs'),
+ path.join('.', versionObj.versionedPath, 'docs', 'tutorials'),
+ path.join('.', versionObj.versionedPath, 'docs', 'typescript'),
+ path.join('.', versionObj.versionedPath, 'docs', 'api'),
+ path.join('.', versionObj.versionedPath, 'docs', 'source', '_docs'),
+ './tmp'
+ ];
+ for (const folder of foldersToClean) {
+ let files = [];
+
+ try {
+ files = fs.readdirSync(folder);
+ } catch (err) {
+ if (err.code === 'ENOENT') {
+ continue;
+ }
+ }
+ for (const file of files) {
+ if (file.endsWith('.html')) {
+ console.log('Delete', path.join(folder, file));
+ fs.unlinkSync(path.join(folder, file));
+ }
+ }
+ }
+}
+
+function moveDocsToTemp() {
+ if (!versionObj.versionedPath) {
+ throw new Error('Cannot move unversioned deploy to /tmp');
+ }
+ try {
+ fs.rmSync('./tmp', { recursive: true });
+ } catch (err) {
+ if (err.code !== 'ENOENT') {
+ throw err;
+ }
+ }
+ const folder = versionObj.versionedPath.replace(/^\//, '');
+ const directory = fs.readdirSync(folder);
+ for (const file of directory) {
+ fsextra.moveSync(`${folder}/${file}`, `./tmp/${file}`);
+ }
+}
+
+/**
* Array of array of semver numbers, sorted with highest number first
* @example
* [[1,2,3], [0,1,2]]
- * @type {number[][]}
+ * @type {number[][]}
*/
let filteredTags = [];
@@ -90,7 +178,8 @@ let filteredTags = [];
* @returns number array or undefined
*/
function parseVersion(str) {
- const versionReg = /^v?(\d+)\.(\d+)\.(\d+)$/i;
+ // there is no ending "$", because of "rc"-like versions
+ const versionReg = /^v?(\d+)\.(\d+)\.(\d+)/i;
const match = versionReg.exec(str);
@@ -105,12 +194,23 @@ function parseVersion(str) {
return parsed;
}
+ // special case, to not log a warning
+ if (str === "test") {
+ return undefined;
+ }
+
+ console.log(`Failed to parse version! got: ${str}`);
+
return undefined;
}
+/**
+ * Get versions from git tags and put them into {@link filteredTags}
+ */
function getVersions() {
// get all tags from git
- const res = childProcess.execSync("git tag").toString();
+ // "trim" is used to remove the ending new-line
+ const res = childProcess.execSync("git tag").toString().trim();
filteredTags = res.split('\n')
// map all gotten tags if they match the regular expression
@@ -138,16 +238,16 @@ function getVersions() {
* Stringify a semver number array
* @param {number[]} arr The array to stringify
* @param {boolean} dotX If "true", return "5.X" instead of "5.5.5"
- * @returns
+ * @returns
*/
function stringifySemverNumber(arr, dotX) {
if (dotX) {
- return `${arr[0]}.x`;
+ return `${arr[0]}.x`;
}
return `${arr[0]}.${arr[1]}.${arr[2]}`;
}
-/**
+/**
* Get the latest version available
* @returns {Version}
*/
@@ -210,11 +310,11 @@ const versionObj = (() => {
currentVersion: getCurrentVersion(),
latestVersion: getLatestVersion(),
pastVersions: [
- getLatestVersionOf(6),
- getLatestVersionOf(5),
+ getLatestVersionOf(7),
+ getLatestVersionOf(6)
]
};
- const versionedDeploy = process.env.DOCS_DEPLOY === "true" ? !(base.currentVersion.listed === base.latestVersion.listed) : false;
+ const versionedDeploy = !!process.env.DOCS_DEPLOY ? !(base.currentVersion.listed === base.latestVersion.listed) : false;
const versionedPath = versionedDeploy ? `/docs/${base.currentVersion.path}` : '';
@@ -232,6 +332,8 @@ try {
const docsFilemap = require('../docs/source/index');
const files = Object.keys(docsFilemap.fileMap);
+// api explicitly imported for specific file loading
+const apiReq = require('../docs/source/api');
const wrapMarkdown = (md, baseLayout, versionedPath) => `
extends ${baseLayout}
@@ -239,33 +341,91 @@ extends ${baseLayout}
append style
link(rel="stylesheet", href="${versionedPath}/docs/css/inlinecpc.css")
script(type="text/javascript" src="${versionedPath}/docs/js/native.js")
- style.
- p { line-height: 1.5em }
block content
-
+
:markdown
${md.split('\n').map(line => ' ' + line).join('\n')}
`;
const cpc = `
-
-
-
-
#native_company# — #native_desc#
+
`;
/** Alias to not execute "promisify" often */
const pugRender = promisify(pug.render);
-async function pugify(filename, options) {
+/** Find all urls that are href's and start with "https://mongoosejs.com" */
+const mongooseComRegex = /(?:href=")(https:\/\/mongoosejs\.com\/?)/g;
+/** Regex to detect a versioned path */
+const versionedDocs = /docs\/\d/;
+
+/**
+ * Map urls (https://mongoosejs.com/) to local paths
+ * @param {String} block The String block to look for urls
+ * @param {String} currentUrl The URL the block is for (non-versioned)
+ */
+function mapURLs(block, currentUrl) {
+ let match;
+
+ let out = '';
+ let lastIndex = 0;
+
+ while ((match = mongooseComRegex.exec(block)) !== null) {
+ // console.log("match", match);
+ // cant just use "match.index" byitself, because of the extra "href=\"" condition, which is not factored in in "match.index"
+ let startIndex = match.index + match[0].length - match[1].length;
+ out += block.slice(lastIndex, startIndex);
+ lastIndex = startIndex + match[1].length;
+
+ // somewhat primitive gathering of the url, but should be enough for now
+ let fullUrl = /^\/[^"]+/.exec(block.slice(lastIndex-1));
+
+ let noPrefix = false;
+
+ if (fullUrl) {
+ // extra processing to only use "#otherId" instead of using full url for the same page
+ // at least firefox does not make a difference between a full path and just "#", but it makes debugging paths easier
+ if (fullUrl[0].startsWith(currentUrl)) {
+ let indexMatch = /#/.exec(fullUrl);
+
+ if (indexMatch) {
+ lastIndex += indexMatch.index - 1;
+ noPrefix = true;
+ }
+ }
+ }
+
+ if (!noPrefix) {
+ // map all to the versioned-path, unless a explicit version is given
+ if (!versionedDocs.test(block.slice(lastIndex, lastIndex+10))) {
+ out += versionObj.versionedPath + "/";
+ } else {
+ out += "/";
+ }
+ }
+ }
+
+ out += block.slice(lastIndex);
+
+ return out;
+}
+
+/**
+ * Render a given file with the given options
+ * @param {String} filename The documentation file path to render
+ * @param {import("../docs/source/index").DocsOptions} options The options to use to render the file (api may be overwritten at reload)
+ * @param {Boolean} isReload Indicate this is a reload of the file
+ * @returns
+ */
+async function pugify(filename, options, isReload = false) {
+ /** Path for the output file */
let newfile = undefined;
options = options || {};
options.package = pkg;
@@ -278,14 +438,20 @@ async function pugify(filename, options) {
/** Set which path to read, also pug uses this to resolve relative includes from */
let inputFile = filename;
- if (isAPI) {
+ if (options.api) {
+ // only re-parse the api file when in a reload, because it is done once at file load
+ if (isReload) {
+ apiReq.parseFile(options.file);
+ // overwrite original options because of reload
+ options = {...options, ...apiReq.docs.get(options.file)};
+ }
inputFile = path.resolve(cwd, 'docs/api_split.pug');
}
let contents = fs.readFileSync(path.resolve(cwd, inputFile)).toString();
if (options.acquit) {
- contents = transform(contents, tests);
+ contents = transform(contents, getTests());
contents = contents.replaceAll(/^```acquit$/gmi, "```javascript");
}
@@ -308,13 +474,16 @@ async function pugify(filename, options) {
}
};
- if (isAPI) {
+ if (options.api) {
newfile = path.resolve(cwd, filename);
- options.docs = docsFilemap.apiDocs;
+ options.docs = Array.from(docsFilemap.apiDocs.values());
}
newfile = newfile || filename.replace('.pug', '.html');
+ /** Unversioned final documentation path */
+ const docsPath = newfile;
+
if (versionObj.versionedDeploy) {
newfile = path.resolve(cwd, path.join('.', versionObj.versionedPath), path.relative(cwd, newfile));
await fs.promises.mkdir(path.dirname(newfile), {recursive:true});
@@ -326,12 +495,14 @@ async function pugify(filename, options) {
options.opencollectiveSponsors = opencollectiveSponsors;
- const str = await pugRender(contents, options).catch(console.error);
+ let str = await pugRender(contents, options).catch(console.error);
if (typeof str !== "string") {
return;
}
-
+
+ str = mapURLs(str, '/' + path.relative(cwd, docsPath))
+
await fs.promises.writeFile(newfile, str).catch((err) => {
console.error('could not write', err.stack);
}).then(() => {
@@ -339,20 +510,19 @@ async function pugify(filename, options) {
});
}
-// extra function to start watching for file-changes, without having to call this file directly with "watch"
+/** extra function to start watching for file-changes, without having to call this file directly with "watch" */
function startWatch() {
Object.entries(docsFilemap.fileMap).forEach(([file, fileValue]) => {
let watchPath = path.resolve(cwd, file);
const notifyPath = path.resolve(cwd, file);
- // exclude "api.pug" from changing the watchpath
- if (fileValue.api && !file.endsWith('docs/api.pug')) {
+ if (fileValue.api) {
watchPath = path.resolve(cwd, fileValue.file);
}
fs.watchFile(watchPath, { interval: 1000 }, (cur, prev) => {
if (cur.mtime > prev.mtime) {
- pugify(notifyPath, docsFilemap.fileMap[file]);
+ pugify(notifyPath, docsFilemap.fileMap[file], true);
}
});
});
@@ -360,7 +530,17 @@ function startWatch() {
fs.watchFile(path.join(cwd, 'docs/layout.pug'), { interval: 1000 }, (cur, prev) => {
if (cur.mtime > prev.mtime) {
console.log('docs/layout.pug modified, reloading all files');
- pugifyAllFiles(true);
+ pugifyAllFiles(true, true);
+ }
+ });
+
+ fs.watchFile(path.join(cwd, 'docs/api_split.pug'), {interval: 1000}, (cur, prev) => {
+ if (cur.mtime > prev.mtime) {
+ console.log('docs/api_split.pug modified, reloading all api files');
+ Promise.all(files.filter(v=> v.startsWith('docs/api')).map(async (file) => {
+ const filename = path.join(cwd, file);
+ await pugify(filename, docsFilemap.fileMap[file], true);
+ }));
}
});
@@ -375,10 +555,15 @@ function startWatch() {
});
}
-async function pugifyAllFiles(noWatch) {
+/**
+ * Render all files at once
+ * @param {Boolean} noWatch Set whether to start file watchers for reload
+ * @param {Boolean} isReload Indicate this is a reload of all files
+ */
+async function pugifyAllFiles(noWatch, isReload = false) {
await Promise.all(files.map(async (file) => {
const filename = path.join(cwd, file);
- await pugify(filename, docsFilemap.fileMap[file]);
+ await pugify(filename, docsFilemap.fileMap[file], isReload);
}));
// enable watch after all files have been done once, and not in the loop to use less-code
@@ -393,7 +578,7 @@ const pathsToCopy = [
'docs/js',
'docs/css',
'docs/images'
-]
+];
/** Copy all static files when versionedDeploy is used */
async function copyAllRequiredFiles() {
@@ -402,7 +587,6 @@ async function copyAllRequiredFiles() {
return;
}
- const fsextra = require('fs-extra');
await Promise.all(pathsToCopy.map(async v => {
const resultPath = path.resolve(cwd, path.join('.', versionObj.versionedPath, v));
await fsextra.copy(v, resultPath);
@@ -419,8 +603,26 @@ exports.cwd = cwd;
// only run the following code if this file is the main module / entry file
if (isMain) {
- console.log(`Processing ~${files.length} files`);
- Promise.all([pugifyAllFiles(), copyAllRequiredFiles()]).then(() => {
- console.log("Done Processing");
- })
+ (async function main() {
+ console.log(`Processing ~${files.length} files`);
+
+ require('./generateSearch');
+ await deleteAllHtmlFiles();
+ await pugifyAllFiles();
+ await copyAllRequiredFiles();
+ if (!!process.env.DOCS_DEPLOY && !!versionObj.versionedPath) {
+ await moveDocsToTemp();
+ }
+
+ console.log('Done Processing');
+ })();
+}
+
+// Modified from github-slugger
+function createSlug(value) {
+ if (typeof value !== 'string') {
+ return '';
+ }
+ value = value.toLowerCase();
+ return value.replace(/<\/?code>/g, '').replace(/[^a-z0-9-_\s]/g, '').replace(/ /g, '-');
}
diff --git a/test/aggregate.test.js b/test/aggregate.test.js
index 2e052a8e564..a746e143e8d 100644
--- a/test/aggregate.test.js
+++ b/test/aggregate.test.js
@@ -7,6 +7,7 @@
const start = require('./common');
const assert = require('assert');
+const stream = require('stream');
const Aggregate = require('../lib/aggregate');
@@ -834,40 +835,26 @@ describe('aggregate: ', function() {
const agg = new Aggregate([], db.model('Employee'));
const promise = agg.exec();
- assert.ok(promise instanceof mongoose.Promise);
+ assert.ok(promise instanceof Promise);
return promise.catch(error => {
assert.ok(error);
assert.ok(error.message.indexOf('empty pipeline') !== -1, error.message);
});
});
-
- it('with a callback', function(done) {
- const aggregate = new Aggregate([], db.model('Employee'));
-
- const callback = function(err) {
- assert.ok(err);
- assert.equal(err.message, 'Aggregate has empty pipeline');
- done();
- };
-
- aggregate.exec(callback);
- });
});
describe('error when not bound to a model', function() {
- it('with callback', function() {
+ it('with callback', async function() {
const aggregate = new Aggregate();
aggregate.skip(0);
- let threw = false;
try {
- aggregate.exec();
+ await aggregate.exec();
+ assert.ok(false);
} catch (error) {
- threw = true;
assert.equal(error.message, 'Aggregate not bound to any Model');
}
- assert.ok(threw);
});
});
@@ -1073,7 +1060,7 @@ describe('aggregate: ', function() {
const schema = new Schema({ name: String }, { read: 'secondary' });
const M = db.model('Test', schema);
const a = M.aggregate();
- assert.equal(a.options.readPreference.mode, 'secondary');
+ assert.equal(a.options.readPreference, 'secondary');
a.read('secondaryPreferred');
@@ -1229,6 +1216,32 @@ describe('aggregate: ', function() {
assert.equal(res[1].test, 'a test');
});
+ it('cursor supports transform option (gh-14331)', async function() {
+ const mySchema = new Schema({ name: String });
+ const Test = db.model('Test', mySchema);
+
+ await Test.deleteMany({});
+ await Test.create([{ name: 'Apple' }, { name: 'Apple' }]);
+
+ let resolve;
+ const waitForStream = new Promise(innerResolve => {
+ resolve = innerResolve;
+ });
+ const otherStream = new stream.Writable({
+ write(chunk, encoding, callback) {
+ resolve(chunk.toString());
+ callback();
+ }
+ });
+
+ await Test.
+ aggregate([{ $match: { name: 'Apple' } }]).
+ cursor({ transform: JSON.stringify }).
+ pipe(otherStream);
+ const streamValue = await waitForStream;
+ assert.ok(streamValue.includes('"name":"Apple"'), streamValue);
+ });
+
describe('Mongo 3.6 options', function() {
before(async function() {
await onlyTestAtOrAbove('3.6', this);
@@ -1256,4 +1269,19 @@ describe('aggregate: ', function() {
});
});
+ it('should not throw error if database connection has not been established (gh-13125)', async function() {
+ const m = new mongoose.Mongoose();
+ const mySchema = new Schema({ test: String });
+ const M = m.model('Test', mySchema);
+
+ const aggregate = M.aggregate();
+ aggregate.match({ foo: 'bar' });
+
+ const p = aggregate.exec();
+
+ await m.connect(start.uri);
+
+ await p;
+ await m.disconnect();
+ });
});
diff --git a/test/bigint.test.js b/test/bigint.test.js
new file mode 100644
index 00000000000..e3d00418e2c
--- /dev/null
+++ b/test/bigint.test.js
@@ -0,0 +1,167 @@
+'use strict';
+
+const assert = require('assert');
+const start = require('./common');
+
+const mongoose = start.mongoose;
+const Schema = mongoose.Schema;
+
+describe('BigInt', function() {
+ beforeEach(() => mongoose.deleteModel(/Test/));
+
+ it('is a valid schema type', function() {
+ const schema = new Schema({
+ myBigInt: BigInt
+ });
+ const Test = mongoose.model('Test', schema);
+
+ const doc = new Test({
+ myBigInt: 42n
+ });
+ assert.strictEqual(doc.myBigInt, 42n);
+ assert.equal(typeof doc.myBigInt, 'bigint');
+ });
+
+ it('casting from strings and numbers', function() {
+ const schema = new Schema({
+ bigint1: {
+ type: BigInt
+ },
+ bigint2: 'BigInt'
+ });
+ const Test = mongoose.model('Test', schema);
+
+ const doc = new Test({
+ bigint1: 42,
+ bigint2: '997'
+ });
+ assert.strictEqual(doc.bigint1, 42n);
+ assert.strictEqual(doc.bigint2, 997n);
+ });
+
+ it('handles cast errors', async function() {
+ const schema = new Schema({
+ bigint: 'BigInt'
+ });
+ const Test = mongoose.model('Test', schema);
+
+ const doc = new Test({
+ bigint: 'foo bar'
+ });
+ assert.strictEqual(doc.bigint, undefined);
+
+ const err = await doc.validate().then(() => null, err => err);
+ assert.ok(err);
+ assert.ok(err.errors['bigint']);
+ assert.equal(err.errors['bigint'].name, 'CastError');
+ assert.equal(
+ err.errors['bigint'].message,
+ 'Cast to BigInt failed for value "foo bar" (type string) at path "bigint" because of "SyntaxError"'
+ );
+ });
+
+ it('supports required', async function() {
+ const schema = new Schema({
+ bigint: {
+ type: BigInt,
+ required: true
+ }
+ });
+ const Test = mongoose.model('Test', schema);
+
+ const doc = new Test({
+ bigint: null
+ });
+
+ const err = await doc.validate().then(() => null, err => err);
+ assert.ok(err);
+ assert.ok(err.errors['bigint']);
+ assert.equal(err.errors['bigint'].name, 'ValidatorError');
+ assert.equal(
+ err.errors['bigint'].message,
+ 'Path `bigint` is required.'
+ );
+ });
+
+ describe('MongoDB integration', function() {
+ let db;
+ let Test;
+
+ before(async function() {
+ db = await start();
+
+ const schema = new Schema({
+ myBigInt: BigInt
+ });
+ db.deleteModel(/Test/);
+ Test = db.model('Test', schema);
+ });
+
+ after(async function() {
+ await db.close();
+ });
+
+ beforeEach(async() => {
+ await Test.deleteMany({});
+ });
+
+ it('is stored as a long in MongoDB', async function() {
+ await Test.create({ myBigInt: 42n });
+
+ const doc = await Test.findOne({ myBigInt: { $type: 'long' } });
+ assert.ok(doc);
+ assert.strictEqual(doc.myBigInt, 42n);
+ });
+
+ it('becomes a bigint with lean using useBigInt64', async function() {
+ await Test.create({ myBigInt: 7n });
+
+ const doc = await Test.
+ findOne({ myBigInt: 7n }).
+ setOptions({ useBigInt64: true }).
+ lean();
+ assert.ok(doc);
+ assert.strictEqual(doc.myBigInt, 7n);
+ });
+
+ it('can query with comparison operators', async function() {
+ await Test.create([
+ { myBigInt: 1n },
+ { myBigInt: 2n },
+ { myBigInt: 3n },
+ { myBigInt: 4n }
+ ]);
+
+ let docs = await Test.find({ myBigInt: { $gte: 3n } }).sort({ myBigInt: 1 });
+ assert.equal(docs.length, 2);
+ assert.deepStrictEqual(docs.map(doc => doc.myBigInt), [3n, 4n]);
+
+ docs = await Test.find({ myBigInt: { $lt: 3n } }).sort({ myBigInt: -1 });
+ assert.equal(docs.length, 2);
+ assert.deepStrictEqual(docs.map(doc => doc.myBigInt), [2n, 1n]);
+ });
+
+ it('supports populate()', async function() {
+ const parentSchema = new Schema({
+ child: {
+ type: BigInt,
+ ref: 'Child'
+ }
+ });
+ const childSchema = new Schema({
+ _id: BigInt,
+ name: String
+ });
+ const Parent = db.model('Parent', parentSchema);
+ const Child = db.model('Child', childSchema);
+
+ const { _id } = await Parent.create({ child: 42n });
+ await Child.create({ _id: 42n, name: 'test-bigint-populate' });
+
+ const doc = await Parent.findById(_id).populate('child');
+ assert.ok(doc);
+ assert.equal(doc.child.name, 'test-bigint-populate');
+ assert.equal(doc.child._id, 42n);
+ });
+ });
+});
diff --git a/test/cast.test.js b/test/cast.test.js
index 51b97518ff3..0ed0c8df9f7 100644
--- a/test/cast.test.js
+++ b/test/cast.test.js
@@ -123,6 +123,12 @@ describe('cast: ', function() {
{ x: { $bitsAnyClear: Buffer.from([3]) } });
});
+ it('with int32 (gh-15170)', function() {
+ const schema = new Schema({ x: 'Int32' });
+ assert.deepEqual(cast(schema, { x: { $bitsAnySet: 3 } }),
+ { x: { $bitsAnySet: 3 } });
+ });
+
it('throws when invalid', function() {
const schema = new Schema({ x: Number });
assert.throws(function() {
@@ -160,6 +166,33 @@ describe('cast: ', function() {
});
});
+ it('casts $comment (gh-14576)', function() {
+ const schema = new Schema({ name: String });
+
+ let res = cast(schema, {
+ $comment: 'test'
+ });
+ assert.deepStrictEqual(res, { $comment: 'test' });
+
+ res = cast(schema, {
+ $comment: 42
+ });
+ assert.deepStrictEqual(res, { $comment: '42' });
+
+ assert.throws(
+ () => cast(schema, {
+ $comment: { name: 'taco' }
+ }),
+ /\$comment/
+ );
+
+ const schema2 = new Schema({ $comment: Number });
+ res = cast(schema2, {
+ $comment: 42
+ });
+ assert.deepStrictEqual(res, { $comment: 42 });
+ });
+
it('avoids setting stripped out nested schema values to undefined (gh-11291)', function() {
const nested = new Schema({}, {
id: false,
@@ -193,4 +226,40 @@ describe('cast: ', function() {
name: 'foo'
});
});
+
+ it('handles $in with discriminators if $in has exactly 1 element (gh-13492)', function() {
+ const itemStateFooSchema = new Schema({
+ fieldFoo: String
+ });
+
+ const itemSchema = new Schema({
+ state: new Schema(
+ {
+ field: String
+ },
+ {
+ discriminatorKey: 'type'
+ }
+ )
+ });
+
+ itemSchema.path('state').discriminator('FOO', itemStateFooSchema);
+
+ const res = cast(itemSchema, {
+ 'state.type': {
+ $in: ['FOO']
+ },
+ 'state.fieldFoo': 44
+ });
+ assert.deepStrictEqual(res, {
+ 'state.type': { $in: ['FOO'] },
+ 'state.fieldFoo': '44'
+ });
+ });
+
+ it('treats unknown operators as passthrough (gh-15170)', function() {
+ const schema = new Schema({ x: Boolean });
+ assert.deepEqual(cast(schema, { x: { $someConditional: 'true' } }),
+ { x: { $someConditional: true } });
+ });
});
diff --git a/test/collection.test.js b/test/collection.test.js
index 8b4fa71ea4c..755eccbe55a 100644
--- a/test/collection.test.js
+++ b/test/collection.test.js
@@ -18,41 +18,24 @@ describe('collections:', function() {
db = null;
});
- it('should buffer commands until connection is established', function(done) {
+ it('should buffer commands until connection is established', async function() {
db = mongoose.createConnection();
const collection = db.collection('test-buffering-collection');
- let connected = false;
- let insertedId = undefined;
- let pending = 2;
-
- function finish() {
- if (--pending) {
- return;
- }
- assert.ok(connected);
- assert.ok(insertedId !== undefined);
- collection.findOne({ _id: insertedId }).then(doc => {
- assert.strictEqual(doc.foo, 'bar');
- db.close();
- done();
- });
- }
- collection.insertOne({ foo: 'bar' }, {}, function(err, result) {
- assert.ok(connected);
- insertedId = result.insertedId;
- finish();
- });
+ const op = collection.insertOne({ foo: 'bar' }, {});
const uri = start.uri;
- db.openUri(process.env.MONGOOSE_TEST_URI || uri, function(err) {
- connected = !err;
- finish();
- });
+ await db.openUri(process.env.MONGOOSE_TEST_URI || uri);
+
+ const res = await op;
+ assert.ok(res.insertedId);
+ const doc = await collection.findOne({ _id: res.insertedId });
+ assert.strictEqual(doc.foo, 'bar');
+ await db.close();
});
- it('returns a promise if buffering and no callback (gh-7676)', function(done) {
- db = mongoose.createConnection();
+ it('returns a promise if buffering and no callback (gh-7676)', async function() {
+ const db = mongoose.createConnection();
const collection = db.collection('gh7676');
const promise = collection.insertOne({ foo: 'bar' }, {})
@@ -62,9 +45,22 @@ describe('collections:', function() {
assert.strictEqual(doc.foo, 'bar');
});
- db.openUri(start.uri, function(err) {
- assert.ifError(err);
- promise.then(() => done(), done);
+ await db.openUri(start.uri);
+
+ await promise;
+ await db.close();
+ });
+
+ it('returns a promise if buffering and callback with find() (gh-14184)', function(done) {
+ db = mongoose.createConnection();
+ const collection = db.collection('gh14184');
+ collection.opts.bufferTimeoutMS = 100;
+
+ collection.find({ foo: 'bar' }, {}, (err, docs) => {
+ assert.ok(err);
+ assert.ok(err.message.includes('buffering timed out after 100ms'));
+ assert.equal(docs, undefined);
+ done();
});
});
@@ -83,7 +79,7 @@ describe('collections:', function() {
thrown = false;
try {
- collection.update();
+ collection.updateOne();
} catch (e) {
assert.ok(/unimplemented/.test(e.message));
thrown = true;
@@ -153,17 +149,16 @@ describe('collections:', function() {
thrown = false;
});
- it('buffers for sync methods (gh-10610)', function(done) {
+ it('buffers for sync methods (gh-10610)', async function() {
db = mongoose.createConnection();
const collection = db.collection('gh10610');
- collection.find({}, {}, function(err, res) {
- assert.ifError(err);
- assert.equal(typeof res.toArray, 'function');
- done();
- });
+ const promise = collection.find({}, {});
const uri = start.uri;
- db.openUri(process.env.MONGOOSE_TEST_URI || uri);
+ await db.openUri(process.env.MONGOOSE_TEST_URI || uri);
+
+ const res = await promise;
+ assert.equal(typeof res.toArray, 'function');
});
});
diff --git a/test/common.js b/test/common.js
index 6e036983c91..0831d0eb3a9 100644
--- a/test/common.js
+++ b/test/common.js
@@ -19,31 +19,6 @@ if (process.env.PRINT_COLLECTIONS) {
});
}
-/**
- * Override all Collection related queries to keep count
- */
-
-[
- 'createIndex',
- 'findAndModify',
- 'findOne',
- 'find',
- 'insert',
- 'save',
- 'update',
- 'remove',
- 'count',
- 'distinct',
- 'isCapped',
- 'options'
-].forEach(function(method) {
- const oldMethod = Collection.prototype[method];
-
- Collection.prototype[method] = function() {
- return oldMethod.apply(this, arguments);
- };
-});
-
/**
* Override Collection#onOpen to keep track of connections
*/
@@ -162,32 +137,39 @@ module.exports.mongoose = mongoose;
*/
module.exports.mongodVersion = async function() {
- return new Promise((resolve, reject) => {
- const db = module.exports();
-
-
- db.on('error', reject);
-
- db.on('open', function() {
- const admin = db.db.admin();
- admin.serverStatus(function(err, info) {
- if (err) {
- return reject(err);
- }
- const version = info.version.split('.').map(function(n) {
- return parseInt(n, 10);
- });
- db.close(function() {
- resolve(version);
- });
- });
- });
+ const db = await module.exports();
+
+ const admin = db.client.db().admin();
+
+ const info = await admin.serverStatus();
+ const version = info.version.split('.').map(function(n) {
+ return parseInt(n, 10);
});
+ await db.close();
+ return version;
};
async function dropDBs() {
this.timeout(60000);
+ // retry the "dropDBs" actions if the error is "operation was interrupted", which can often happen in replset CI tests
+ let retries = 5;
+ while (retries > 0) {
+ retries -= 1;
+ try {
+ await _dropDBs();
+ } catch (err) {
+ if (err instanceof mongoose.mongo.MongoWriteConcernError && /operation was interrupted/.test(err.message)) {
+ console.log('DropDB operation interrupted, retrying'); // log that a error was thrown to know that it is going to re-try
+ continue;
+ }
+
+ throw err;
+ }
+ }
+}
+
+async function _dropDBs() {
const db = await module.exports({ noErrorListener: true }).asPromise();
await db.dropDatabase();
await db.close();
diff --git a/test/connection.test.js b/test/connection.test.js
index dcf4cf621c7..2243886f6be 100644
--- a/test/connection.test.js
+++ b/test/connection.test.js
@@ -6,12 +6,12 @@
const start = require('./common');
-const Promise = require('bluebird');
+const STATES = require('../lib/connectionState');
const Q = require('q');
const assert = require('assert');
-const sinon = require('sinon');
const mongodb = require('mongodb');
const MongooseError = require('../lib/error/index');
+const CastError = require('../lib/error/cast');
const mongoose = start.mongoose;
const Schema = mongoose.Schema;
@@ -95,7 +95,7 @@ describe('connections:', function() {
}));
await Model.init();
- const res = await conn.db.listCollections().toArray();
+ const res = await conn.listCollections();
assert.ok(!res.map(c => c.name).includes('gh8814_Conn'));
await conn.close();
});
@@ -114,15 +114,9 @@ describe('connections:', function() {
await conn.close();
});
- it('throws helpful error with legacy syntax (gh-6756)', function() {
- assert.throws(function() {
- mongoose.createConnection('127.0.0.1', 'dbname', 27017);
- }, /mongoosejs\.com.*connections\.html/);
- });
-
- it('throws helpful error with undefined uri (gh-6763)', function() {
- assert.throws(function() {
- mongoose.createConnection(void 0);
+ it('throws helpful error with undefined uri (gh-6763)', async function() {
+ await assert.rejects(async function() {
+ await mongoose.createConnection(void 0).asPromise();
}, /string.*createConnection/);
});
@@ -154,6 +148,7 @@ describe('connections:', function() {
conn1.model('Test', schema);
assert.equal(called.length, 1);
assert.equal(called[0], schema);
+
await conn1.close();
await conn2.close();
});
@@ -162,8 +157,9 @@ describe('connections:', function() {
describe('helpers', function() {
let conn;
- before(function() {
+ before(async function() {
conn = mongoose.createConnection(start.uri2);
+ await conn.asPromise();
return conn;
});
@@ -190,16 +186,97 @@ describe('connections:', function() {
size: 1024
});
- const collections = await conn.db.listCollections().toArray();
+ const collections = await conn.listCollections();
const names = collections.map(function(c) { return c.name; });
assert.ok(names.indexOf('gh5712') !== -1);
assert.ok(collections[names.indexOf('gh5712')].options.capped);
await conn.createCollection('gh5712_0');
- const collectionsAfterCreation = await conn.db.listCollections().toArray();
+ const collectionsAfterCreation = await conn.listCollections();
const newCollectionsNames = collectionsAfterCreation.map(function(c) { return c.name; });
assert.ok(newCollectionsNames.indexOf('gh5712') !== -1);
});
+
+ it('listCollections()', async function() {
+ await conn.dropDatabase();
+ await conn.createCollection('test1176');
+ await conn.createCollection('test94112');
+
+ const collections = await conn.listCollections();
+ assert.deepStrictEqual(collections.map(coll => coll.name).sort(), ['test1176', 'test94112']);
+ });
+ });
+
+ describe('events', function() {
+ let conn;
+
+ before(async function() {
+ conn = mongoose.createConnection(start.uri2, { monitorCommands: true });
+ await conn.asPromise();
+ await conn.collection('test').deleteMany({});
+ return conn;
+ });
+
+ after(function() {
+ return conn.close();
+ });
+
+ it('operation-start', async function() {
+ const events = [];
+ conn.on('operation-start', ev => events.push(ev));
+
+ await conn.collection('test').findOne({ answer: 42 });
+ assert.equal(events.length, 1);
+ assert.equal(events[0].collectionName, 'test');
+ assert.equal(events[0].method, 'findOne');
+ assert.deepStrictEqual(events[0].params, [{ answer: 42 }]);
+
+ await conn.collection('test').insertOne({ _id: 12, answer: 99 });
+ assert.equal(events.length, 2);
+ assert.equal(events[1].collectionName, 'test');
+ assert.equal(events[1].method, 'insertOne');
+ assert.deepStrictEqual(events[1].params, [{ _id: 12, answer: 99 }]);
+ });
+
+ it('operation-end', async function() {
+ const events = [];
+ conn.on('operation-end', ev => {
+ events.push(ev);
+ });
+
+ await conn.collection('test').insertOne({ _id: 17, answer: 42 });
+ assert.equal(events.length, 1);
+ assert.equal(events[0].collectionName, 'test');
+ assert.equal(events[0].method, 'insertOne');
+
+ await conn.collection('test').findOne({ answer: 42 });
+ assert.equal(events.length, 2);
+ assert.equal(events[1].collectionName, 'test');
+ assert.equal(events[1].method, 'findOne');
+ assert.deepStrictEqual(events[1].result, { _id: 17, answer: 42 });
+ });
+
+ it('commandStarted, commandFailed, commandSucceeded (gh-14611)', async function() {
+ let events = [];
+ conn.on('commandStarted', event => events.push(event));
+ conn.on('commandFailed', event => events.push(event));
+ conn.on('commandSucceeded', event => events.push(event));
+
+ await conn.collection('test').insertOne({ _id: 14611, answer: 42 });
+ assert.equal(events.length, 2);
+ assert.equal(events[0].constructor.name, 'CommandStartedEvent');
+ assert.equal(events[0].commandName, 'insert');
+ assert.equal(events[1].constructor.name, 'CommandSucceededEvent');
+ assert.equal(events[1].requestId, events[0].requestId);
+
+ events = [];
+ await conn.createCollection('tests', { capped: 1024 }).catch(() => {});
+ assert.equal(events.length, 2);
+ assert.equal(events[0].constructor.name, 'CommandStartedEvent');
+ assert.equal(events[0].commandName, 'create');
+ assert.equal(events[1].constructor.name, 'CommandFailedEvent');
+ assert.equal(events[1].requestId, events[0].requestId);
+ });
});
it('should allow closing a closed connection', async function() {
@@ -251,15 +328,10 @@ describe('connections:', function() {
});
});
- describe('connect callbacks', function() {
- it('should return an error if malformed uri passed', function(done) {
- const db = mongoose.createConnection('mongodb:///fake', {}, function(err) {
- assert.equal(err.name, 'MongoParseError');
- done();
- });
- db.close();
- assert.ok(!db.options);
- });
+ it('should return an error if malformed uri passed', async function() {
+ const err = await mongoose.createConnection('mongodb:///fake').asPromise().then(() => null, err => err);
+ assert.ok(err);
+ assert.equal(err.name, 'MongoParseError');
});
describe('.model()', function() {
@@ -338,18 +410,13 @@ describe('connections:', function() {
});
describe('passing object literal schemas', function() {
- it('works', function(done) {
+ it('works', async function() {
const A = db.model('A', { n: [{ age: 'number' }] });
const a = new A({ n: [{ age: '47' }] });
assert.strictEqual(47, a.n[0].age);
- a.save(function(err) {
- assert.ifError(err);
- A.findById(a, function(err) {
- assert.ifError(err);
- assert.strictEqual(47, a.n[0].age);
- done();
- });
- });
+ await a.save();
+ await A.findById(a);
+ assert.strictEqual(47, a.n[0].age);
});
});
});
@@ -371,64 +438,41 @@ describe('connections:', function() {
});
});
- it('destroy connection and remove it permanantly', (done) => {
+ it('destroy connection and remove it permanently', async function() {
const opts = {};
- const conn = mongoose.createConnection(start.uri, opts);
- const MongoClient = mongodb.MongoClient;
- const stub = sinon.stub(MongoClient.prototype, 'close').callsFake((force, callback) => {
- callback();
- });
-
+ const conn = await mongoose.createConnection(start.uri, opts).asPromise();
conn.useDb('test-db');
const totalConn = mongoose.connections.length;
- conn.destroy(() => {
- assert.equal(mongoose.connections.length, totalConn - 1);
- stub.restore();
- done();
- });
+ await conn.destroy();
+ assert.equal(mongoose.connections.length, totalConn - 1);
});
- it('verify that attempt to re-open destroyed connection throws error, via promise', (done) => {
+ it('verify that attempt to re-open destroyed connection throws error, via promise', async function() {
const opts = {};
- const conn = mongoose.createConnection(start.uri, opts);
- const MongoClient = mongodb.MongoClient;
- const stub = sinon.stub(MongoClient.prototype, 'close').callsFake((force, callback) => {
- callback();
- });
+ const conn = await mongoose.createConnection(start.uri, opts).asPromise();
conn.useDb('test-db');
- conn.destroy(async() => {
- try {
- await conn.openUri(start.uri);
- } catch (error) {
- assert.equal(error.message, 'Connection has been closed and destroyed, and cannot be used for re-opening the connection. Please create a new connection with `mongoose.createConnection()` or `mongoose.connect()`.');
- stub.restore();
- done();
- }
- });
+ await conn.destroy();
+ try {
+ await conn.openUri(start.uri);
+ } catch (error) {
+ assert.equal(error.message, 'Connection has been closed and destroyed, and cannot be used for re-opening the connection. Please create a new connection with `mongoose.createConnection()` or `mongoose.connect()`.');
+ }
});
- it('verify that attempt to re-open destroyed connection throws error, via callback', (done) => {
+ it('verify that attempt to re-open destroyed connection throws error, via callback', async function() {
const opts = {};
- const conn = mongoose.createConnection(start.uri, opts);
- const MongoClient = mongodb.MongoClient;
- const stub = sinon.stub(MongoClient.prototype, 'close').callsFake((force, callback) => {
- callback();
- });
+ const conn = await mongoose.createConnection(start.uri, opts).asPromise();
conn.useDb('test-db');
-
- conn.destroy(() => {
- conn.openUri(start.uri, function(error, result) {
- assert.equal(result, undefined);
- assert.equal(error, 'Connection has been closed and destroyed, and cannot be used for re-opening the connection. Please create a new connection with `mongoose.createConnection()` or `mongoose.connect()`.');
- stub.restore();
- done();
- });
- });
+ await conn.destroy();
+ await assert.rejects(
+ () => conn.openUri(start.uri),
+ /Connection has been closed and destroyed/
+ );
});
it('force close with connection created after close (gh-5664)', function(done) {
@@ -478,7 +522,11 @@ describe('connections:', function() {
db.openUri(start.uri, opts);
assert.ok(!M.collection._shouldBufferCommands());
- return M.findOne().then(() => assert.ok(false), err => assert.ok(err.message.includes('initial connection'))).
+ return M.findOne().
+ then(
+ () => assert.ok(false),
+ err => assert.ok(err.message.includes('initial connection'))
+ ).
then(() => db.close());
});
@@ -781,6 +829,35 @@ describe('connections:', function() {
assert.strictEqual(db2, db3);
return db.close();
});
+
+ it('supports removing db (gh-11821)', async function() {
+ const db = await mongoose.createConnection(start.uri).asPromise();
+
+ const schema = mongoose.Schema({ name: String }, { autoCreate: false, autoIndex: false });
+ const Test = db.model('Test', schema);
+ await Test.deleteMany({});
+ await Test.create({ name: 'gh-11821' });
+
+ const db2 = db.useDb(start.databases[1]);
+ const Test2 = db2.model('Test', schema);
+
+ await Test2.deleteMany({});
+ let doc = await Test2.findOne();
+ assert.equal(doc, null);
+
+ db.removeDb(start.databases[1]);
+ assert.equal(db2.readyState, STATES.disconnected);
+ assert.equal(db.readyState, STATES.connected);
+ await assert.rejects(
+ () => Test2.findOne(),
+ /Connection was force closed/
+ );
+
+ doc = await Test.findOne();
+ assert.equal(doc.name, 'gh-11821');
+
+ await db.close();
+ });
});
describe('shouldAuthenticate()', function() {
@@ -817,7 +894,7 @@ describe('connections:', function() {
assert.equal(db.shouldAuthenticate(), true);
- db.close(); // does not actually do anything
+ return db.close();
});
});
});
@@ -842,7 +919,7 @@ describe('connections:', function() {
db.asPromise().catch(() => {});
assert.equal(db.shouldAuthenticate(), true);
- db.close(); // does not actually do anything
+ return db.close();
});
});
describe('when both username and password are defined', function() {
@@ -856,20 +933,22 @@ describe('connections:', function() {
assert.equal(db.shouldAuthenticate(), true);
- db.close(); // does not actually do anything
+ return db.close(); // does not actually do anything
});
});
});
});
describe('passing a function into createConnection', function() {
- it('should store the name of the function (gh-6517)', function(done) {
+ it('should store the name of the function (gh-6517)', async function() {
const conn = mongoose.createConnection(start.uri);
const schema = new Schema({ name: String });
class Person extends mongoose.Model {}
- conn.model(Person, schema);
+ const PersonModel = conn.model(Person, schema);
assert.strictEqual(conn.modelNames()[0], 'Person');
- conn.close(done);
+ await conn.asPromise();
+ await PersonModel.init();
+ await conn.close();
});
});
@@ -897,14 +976,31 @@ describe('connections:', function() {
await conn.close();
});
- it('throws a MongooseServerSelectionError on server selection timeout (gh-8451)', function() {
+ it('throws a MongooseServerSelectionError on server selection timeout (gh-8451)', async function() {
+ const opts = {
+ serverSelectionTimeoutMS: 100
+ };
+ const uri = 'mongodb://baddomain:27017/test';
+
+ const err = await mongoose.createConnection(uri, opts).
+ asPromise().
+ then(() => null, err => err);
+ assert.ok(err);
+ assert.equal(err.name, 'MongooseServerSelectionError');
+ });
+
+ it('avoids unhandled error on createConnection() if error handler registered (gh-14377)', async function() {
const opts = {
serverSelectionTimeoutMS: 100
};
const uri = 'mongodb://baddomain:27017/test';
- return mongoose.createConnection(uri, opts).asPromise().then(() => assert.ok(false), err => {
- assert.equal(err.name, 'MongooseServerSelectionError');
+ const conn = mongoose.createConnection(uri, opts);
+ await new Promise(resolve => {
+ conn.on('error', err => {
+ assert.equal(err.name, 'MongoServerSelectionError');
+ resolve();
+ });
});
});
@@ -937,6 +1033,8 @@ describe('connections:', function() {
await nextChange;
assert.equal(changes.length, 1);
assert.equal(changes[0].operationType, 'insert');
+
+ await changeStream.close();
await conn.close();
});
@@ -1097,13 +1195,13 @@ describe('connections:', function() {
'autoCreate, autoIndex'
);
const m = new mongoose.Mongoose();
- return assert.throws(
- () => m.createConnection(start.uri, opts),
+ return assert.rejects(
+ () => m.createConnection(start.uri, opts).asPromise(),
err
);
});
- it('throws if options.config.autoIndex is true, even if options.autoIndex is false', function() {
+ it('throws if options.config.autoIndex is true, even if options.autoIndex is false', async function() {
const opts = {
readPreference: 'secondary',
autoIndex: false,
@@ -1118,8 +1216,8 @@ describe('connections:', function() {
'autoCreate, autoIndex'
);
- assert.throws(
- () => mongoose.createConnection(start.uri, opts),
+ await assert.rejects(
+ () => mongoose.createConnection(start.uri, opts).asPromise(),
err
);
});
@@ -1212,7 +1310,8 @@ describe('connections:', function() {
before(async() => {
mongooseInstance = new mongoose.Mongoose();
- connection = mongooseInstance.createConnection(start.uri);
+ connection = await mongooseInstance.createConnection(start.uri).asPromise();
+ await connection.dropDatabase();
});
beforeEach(() => connection.deleteModel(/.*/));
afterEach(async() => {
@@ -1491,7 +1590,7 @@ describe('connections:', function() {
const [res] = await Promise.all([
Test.findOne().exec(),
- new Promise.resolve(resolve => setTimeout(resolve, 100)).then(() => {
+ Promise.resolve(resolve => setTimeout(resolve, 100)).then(() => {
conn.client.emit('serverDescriptionChanged', { newDescription: { type: 'Single' } });
})
]);
@@ -1512,6 +1611,13 @@ describe('connections:', function() {
assert.deepEqual(connectionIds, [1, 2, 3, 4, 5]);
});
+ it('should not create default connection with createInitialConnection = false (gh-12965)', function() {
+ const m = new mongoose.Mongoose({
+ createInitialConnection: false
+ });
+ assert.deepEqual(m.connections.length, 0);
+ });
+
it('with autoCreate = false after schema create (gh-12940)', async function() {
const m = new mongoose.Mongoose();
@@ -1537,4 +1643,147 @@ describe('connections:', function() {
});
assert.deepEqual(m.connections.length, 0);
});
+ it('should demonstrate the withSession() function (gh-14330)', async function() {
+ if (!process.env.REPLICA_SET && !process.env.START_REPLICA_SET) {
+ this.skip();
+ }
+ const m = new mongoose.Mongoose();
+ m.connect(start.uri);
+ let session = null;
+ await m.connection.withSession(s => {
+ session = s;
+ });
+ assert.ok(session);
+ });
+ it('listDatabases() should return a list of database objects with a name property (gh-9048)', async function() {
+ const connection = await mongoose.createConnection(start.uri).asPromise();
+ // If this test is running in isolation, then the `start.uri` db might not
+ // exist yet, so create this collection (and the associated db) just in case
+ await connection.createCollection('tests').catch(() => {});
+
+ const { databases } = await connection.listDatabases();
+ assert.ok(connection.name);
+ assert.ok(databases.map(database => database.name).includes(connection.name));
+ });
+ describe('createCollections()', function() {
+ it('should create collections for all models on the connection with the createCollections() function (gh-13300)', async function() {
+ const m = new mongoose.Mongoose();
+ const schema = new Schema({ name: String });
+ const A = m.model('gh13300A', schema, 'gh13300A');
+ const B = m.model('gh13300B', schema, 'gh13300B');
+ const C = m.model('gh13300C', schema, 'gh13300C');
+ await m.connect(start.uri);
+ await m.connection.createCollections();
+ const collections = await m.connection.db.listCollections().toArray();
+ assert.equal(collections.length, 3);
+ const collectionNames = collections.map(inner => Object.values(inner)[0]);
+ assert.equal(collectionNames.includes(A.modelName), true);
+ assert.equal(collectionNames.includes(B.modelName), true);
+ assert.equal(collectionNames.includes(C.modelName), true);
+ // currently cannot write test for continueOnError or errors in general.
+ });
+ });
+ describe('processConnectionOptions', function() {
+ let m = null;
+ after(async() => {
+ await m.disconnect();
+ });
+ it('should not throw an error when attempting to mutate unmutable options object gh-13335', async function() {
+ m = new mongoose.Mongoose();
+ const opts = Object.preventExtensions({ readPreference: 'secondaryPreferred' });
+ const conn = await m.connect(start.uri, opts);
+ assert.ok(conn);
+ });
+ });
+
+ it('connection bulkWrite() ordered (gh-15028)', async function() {
+ const db = start();
+
+ const version = await start.mongodVersion();
+ if (version[0] < 8) {
+ this.skip();
+ return;
+ }
+ const Test = db.model('Test', new Schema({ name: { type: String, required: true } }));
+
+ await Test.deleteMany({});
+ await db.bulkWrite([{ model: 'Test', name: 'insertOne', document: { name: 'test1' } }]);
+ assert.ok(await Test.exists({ name: 'test1' }));
+
+ await db.bulkWrite([{ model: Test, name: 'insertOne', document: { name: 'test2' } }]);
+ assert.ok(await Test.exists({ name: 'test2' }));
+
+ await assert.rejects(
+ () => db.bulkWrite([{ name: 'insertOne', document: { name: 'foo' } }]),
+ /Must specify model in Connection.prototype.bulkWrite\(\) operations/
+ );
+
+ await assert.rejects(
+ () => db.bulkWrite([{ model: Test, document: { name: 'foo' } }]),
+ /Must specify operation name in Connection.prototype.bulkWrite\(\)/
+ );
+ await assert.rejects(
+ () => db.bulkWrite([{ model: Test, name: 'upsertAll', document: { name: 'foo' } }]),
+ /Unrecognized bulkWrite\(\) operation name upsertAll/
+ );
+ });
+
+ it('connection bulkWrite() unordered (gh-15028)', async function() {
+ const db = start();
+
+ const version = await start.mongodVersion();
+ if (version[0] < 8) {
+ this.skip();
+ return;
+ }
+
+ const Test = db.model('Test', new Schema({ name: { type: String, required: true }, num: Number }));
+
+ await Test.deleteMany({});
+ await db.bulkWrite([{ model: 'Test', name: 'insertOne', document: { name: 'test1' } }], { ordered: false });
+ assert.ok(await Test.exists({ name: 'test1' }));
+
+ await db.bulkWrite([{ model: Test, name: 'insertOne', document: { name: 'test2' } }], { ordered: false });
+ assert.ok(await Test.exists({ name: 'test2' }));
+
+ await assert.rejects(
+ () => {
+ return db.bulkWrite([
+ { name: 'insertOne', document: { name: 'foo' } },
+ { model: Test, name: 'insertOne', document: { name: 'test3' } }
+ ], { ordered: false, throwOnValidationError: true });
+ },
+ /Must specify model in Connection.prototype.bulkWrite\(\) operations/
+ );
+ assert.ok(await Test.exists({ name: 'test3' }));
+
+ await assert.rejects(
+ () => db.bulkWrite([
+ { model: Test, document: { name: 'foo' } },
+ { model: Test, name: 'insertOne', document: { name: 'test4' } }
+ ], { ordered: false, throwOnValidationError: true }),
+ /Must specify operation name in Connection.prototype.bulkWrite\(\)/
+ );
+ assert.ok(await Test.exists({ name: 'test4' }));
+
+ await assert.rejects(
+ () => db.bulkWrite([
+ { model: Test, name: 'upsertAll', document: { name: 'foo' } },
+ { model: Test, name: 'insertOne', document: { name: 'test5' } }
+ ], { ordered: false, throwOnValidationError: true }),
+ /Unrecognized bulkWrite\(\) operation name upsertAll/
+ );
+ assert.ok(await Test.exists({ name: 'test5' }));
+
+ const res = await db.bulkWrite([
+ { model: 'Test', name: 'updateOne', filter: { name: 'test5' }, update: { $set: { num: 42 } } },
+ { model: 'Test', name: 'updateOne', filter: { name: 'test4' }, update: { $set: { num: 'not a number' } } }
+ ], { ordered: false });
+ assert.equal(res.matchedCount, 1);
+ assert.equal(res.modifiedCount, 1);
+ assert.equal(res.mongoose.results.length, 2);
+ assert.equal(res.mongoose.results[0], null);
+ assert.ok(res.mongoose.results[1] instanceof CastError);
+ assert.ok(res.mongoose.results[1].message.includes('not a number'));
+ });
});
diff --git a/test/docs/cast.test.js b/test/docs/cast.test.js
index b2b9b4aef78..b24a9db657d 100644
--- a/test/docs/cast.test.js
+++ b/test/docs/cast.test.js
@@ -137,6 +137,26 @@ describe('Cast Tutorial', function() {
// acquit:ignore:end
});
+ it('strictQuery removes casted empty objects', async function() {
+ mongoose.deleteModel('Character');
+ const schema = new mongoose.Schema({ name: String, age: Number }, {
+ strictQuery: true
+ });
+ Character = mongoose.model('Character', schema);
+
+ const query = Character.findOne({
+ $or: [{ notInSchema: { $lt: 'not a number' } }],
+ $and: [{ name: 'abc' }, { age: { $gt: 18 } }, { notInSchema: { $lt: 'not a number' } }],
+ $nor: [{}] // should be kept
+ });
+
+ await query.exec();
+ query.getFilter(); // Empty object `{}`, Mongoose removes `notInSchema`
+ // acquit:ignore:start
+ assert.deepEqual(query.getFilter(), { $and: [{ name: 'abc' }, { age: { $gt: 18 } }], $nor: [{}] });
+ // acquit:ignore:end
+ });
+
it('implicit in', async function() {
// Normally wouldn't find anything because `name` is a string, but
// Mongoose automatically inserts `$in`
diff --git a/test/docs/debug.test.js b/test/docs/debug.test.js
index 26311721692..3cb82fab4ec 100644
--- a/test/docs/debug.test.js
+++ b/test/docs/debug.test.js
@@ -105,7 +105,7 @@ describe('debug: shell', function() {
await Test.create({ name: 'foo' });
assert.equal(args.length, 1);
assert.equal(args[0][1], 'insertOne');
- assert.ok(!('session' in args[0][3]));
+ assert.strictEqual(args[0][4], undefined);
await m.disconnect();
});
diff --git a/test/docs/discriminators.test.js b/test/docs/discriminators.test.js
index be09f43e002..f593f814ed7 100644
--- a/test/docs/discriminators.test.js
+++ b/test/docs/discriminators.test.js
@@ -214,7 +214,7 @@ describe('discriminator docs', function() {
// The discriminator schema has a String `time` and an
// implicitly added ObjectId `_id`.
assert.ok(clickedLinkSchema.path('_id'));
- assert.equal(clickedLinkSchema.path('_id').instance, 'ObjectID');
+ assert.equal(clickedLinkSchema.path('_id').instance, 'ObjectId');
const ClickedLinkEvent = Event.discriminator('ChildEventBad',
clickedLinkSchema);
@@ -273,6 +273,7 @@ describe('discriminator docs', function() {
const batchSchema = new Schema({ events: [eventSchema] });
// `batchSchema.path('events')` gets the mongoose `DocumentArray`
+ // For TypeScript, use `schema.path
('events')`
const docArray = batchSchema.path('events');
// The `events` array can contain 2 different types of events, a
@@ -391,6 +392,7 @@ describe('discriminator docs', function() {
const shapeSchema = Schema({ name: String }, { discriminatorKey: 'kind' });
const schema = Schema({ shape: shapeSchema });
+ // For TypeScript, use `schema.path('shape').discriminator(...)`
schema.path('shape').discriminator('Circle', Schema({ radius: String }));
schema.path('shape').discriminator('Square', Schema({ side: Number }));
diff --git a/test/docs/findoneandupdate.test.js b/test/docs/findoneandupdate.test.js
index e6cbaf8fe96..1eb0076980a 100644
--- a/test/docs/findoneandupdate.test.js
+++ b/test/docs/findoneandupdate.test.js
@@ -40,16 +40,18 @@ describe('Tutorial: findOneAndUpdate()', function() {
age: Number
}));
- await Character.create({ name: 'Jean-Luc Picard' });
+ const _id = new mongoose.Types.ObjectId('0'.repeat(24));
+ let doc = await Character.create({ _id, name: 'Jean-Luc Picard' });
+ doc; // { name: 'Jean-Luc Picard', _id: ObjectId('000000000000000000000000') }
const filter = { name: 'Jean-Luc Picard' };
const update = { age: 59 };
- // `doc` is the document _before_ `update` was applied
- let doc = await Character.findOneAndUpdate(filter, update);
- doc.name; // 'Jean-Luc Picard'
- doc.age; // undefined
+ // The result of `findOneAndUpdate()` is the document _before_ `update` was applied
+ doc = await Character.findOneAndUpdate(filter, update);
+ doc; // { name: 'Jean-Luc Picard', _id: ObjectId('000000000000000000000000') }
// acquit:ignore:start
+ assert.equal(doc._id.toHexString(), _id.toHexString());
assert.equal(doc.name, 'Jean-Luc Picard');
assert.equal(doc.age, undefined);
// acquit:ignore:end
@@ -139,7 +141,7 @@ describe('Tutorial: findOneAndUpdate()', function() {
// acquit:ignore:end
});
- it('rawResult', async function() {
+ it('includeResultMetadata', async function() {
const filter = { name: 'Will Riker' };
const update = { age: 29 };
@@ -151,7 +153,8 @@ describe('Tutorial: findOneAndUpdate()', function() {
const res = await Character.findOneAndUpdate(filter, update, {
new: true,
upsert: true,
- rawResult: true // Return the raw result from the MongoDB driver
+ // Return additional properties about the operation, not just the document
+ includeResultMetadata: true
});
res.value instanceof Character; // true
diff --git a/test/docs/lean.test.js b/test/docs/lean.test.js
index 51ca5458cff..e571987864b 100644
--- a/test/docs/lean.test.js
+++ b/test/docs/lean.test.js
@@ -42,7 +42,7 @@ describe('Lean Tutorial', function() {
const leanDoc = await MyModel.findOne().lean();
v8Serialize(normalDoc).length; // approximately 180
- v8Serialize(leanDoc).length; // 32, about 5x smaller!
+ v8Serialize(leanDoc).length; // approximately 55, about 3x smaller!
// In case you were wondering, the JSON form of a Mongoose doc is the same
// as the POJO. This additional memory only affects how much memory your
@@ -50,7 +50,7 @@ describe('Lean Tutorial', function() {
JSON.stringify(normalDoc).length === JSON.stringify(leanDoc).length; // true
// acquit:ignore:start
assert.ok(v8Serialize(normalDoc).length >= 150 && v8Serialize(normalDoc).length <= 200, v8Serialize(normalDoc).length);
- assert.equal(v8Serialize(leanDoc).length, 32);
+ assert.ok(v8Serialize(leanDoc).length === 55 || v8Serialize(leanDoc).length === 32, v8Serialize(leanDoc).length);
assert.equal(JSON.stringify(normalDoc).length, JSON.stringify(leanDoc).length);
// acquit:ignore:end
});
@@ -203,4 +203,35 @@ describe('Lean Tutorial', function() {
assert.equal(group.members[1].name, 'Kira Nerys');
// acquit:ignore:end
});
+
+ it('bigint', async function() {
+ const Person = mongoose.model('Person', new mongoose.Schema({
+ name: String,
+ age: BigInt
+ }));
+ // acquit:ignore:start
+ await Person.deleteMany({});
+ // acquit:ignore:end
+ // Mongoose will convert `age` to a BigInt
+ const { age } = await Person.create({ name: 'Benjamin Sisko', age: 37 });
+ typeof age; // 'bigint'
+
+ // By default, if you store a document with a BigInt property in MongoDB and you
+ // load the document with `lean()`, the BigInt property will be a number
+ let person = await Person.findOne({ name: 'Benjamin Sisko' }).lean();
+ typeof person.age; // 'number'
+ // acquit:ignore:start
+ assert.equal(typeof person.age, 'number');
+ assert.equal(person.age, 37);
+ // acquit:ignore:end
+
+ // Set the `useBigInt64` option to opt in to converting MongoDB longs to BigInts.
+ person = await Person.findOne({ name: 'Benjamin Sisko' }).
+ setOptions({ useBigInt64: true }).
+ lean();
+ typeof person.age; // 'bigint'
+ // acquit:ignore:start
+ assert.equal(typeof person.age, 'bigint');
+ // acquit:ignore:end
+ });
});
diff --git a/test/docs/promises.test.js b/test/docs/promises.test.js
index 24592a671ca..240637a0936 100644
--- a/test/docs/promises.test.js
+++ b/test/docs/promises.test.js
@@ -8,21 +8,17 @@ describe('promises docs', function() {
let Band;
let db;
- before(function(done) {
+ before(function() {
db = mongoose.createConnection(start.uri);
Band = db.model('band-promises', { name: String, members: [String] });
-
- done();
});
- beforeEach(function(done) {
- Band.deleteMany({}, done);
+ beforeEach(function() {
+ return Band.deleteMany({});
});
after(async function() {
- mongoose.Promise = global.Promise;
-
await db.close();
});
@@ -110,12 +106,20 @@ describe('promises docs', function() {
* - `await Band.findOne().exec();`
*
* As far as functionality is concerned, these two are equivalent.
- * However, we recommend using `.exec()` because that gives you
+ * In Mongoose 6, we recommended using `.exec()` because that gives you
* better stack traces.
+ * However, in Mongoose 7+, that is no longer the case.
+ * You may use whichever you prefer: `await Band.findOne()` is more concise,
+ * `await Band.findOne().exec()` is more explicit and ensures that you `await`
+ * on a fully fledged promise.
*/
it('Should You Use `exec()` With `await`?', async function() {
const doc = await Band.findOne({ name: 'Guns N\' Roses' }); // works
// acquit:ignore:start
+ if (typeof Deno !== 'undefined') {
+ // Deno doesn't have V8 async stack traces
+ return this.skip();
+ }
assert.ok(!doc);
// acquit:ignore:end
@@ -133,7 +137,7 @@ describe('promises docs', function() {
// at process._tickCallback (internal/process/next_tick.js:68:7)
err.stack;
// acquit:ignore:start
- assert.ok(!err.stack.includes('promises.test.js'));
+ assert.ok(err.stack.includes('promises.test.js'));
// acquit:ignore:end
}
@@ -154,34 +158,4 @@ describe('promises docs', function() {
// acquit:ignore:end
}
});
-
- /**
- * If you're an advanced user, you may want to plug in your own promise
- * library like [bluebird](https://www.npmjs.com/package/bluebird). Just set
- * `mongoose.Promise` to your favorite
- * ES6-style promise constructor and mongoose will use it.
- */
- it('Plugging in your own Promises Library', function(done) {
- // acquit:ignore:start
- if (!global.Promise) {
- return done();
- }
- // acquit:ignore:end
- // Use bluebird
- mongoose.Promise = require('bluebird');
- const bluebirdPromise = Band.findOne({ name: 'Guns N\' Roses' }).exec();
- assert.equal(bluebirdPromise.constructor, require('bluebird'));
-
- // Use q. Note that you **must** use `require('q').Promise`.
- mongoose.Promise = require('q').Promise;
- const qPromise = Band.findOne({ name: 'Guns N\' Roses' }).exec();
- assert.ok(qPromise instanceof require('q').makePromise);
-
- // acquit:ignore:start
- // Wait for promises
- bluebirdPromise.then(qPromise).then(function() {
- done();
- });
- // acquit:ignore:end
- });
});
diff --git a/test/docs/transactions.test.js b/test/docs/transactions.test.js
index 408858f945e..df16eaba91c 100644
--- a/test/docs/transactions.test.js
+++ b/test/docs/transactions.test.js
@@ -12,11 +12,11 @@ describe('transactions', function() {
this.timeout(10000);
before(async function() {
- if (!process.env.REPLICA_SET) {
+ if (!process.env.REPLICA_SET && !process.env.START_REPLICA_SET) {
_skipped = true;
this.skip();
}
- db = start({ replicaSet: process.env.REPLICA_SET });
+ db = start(process.env.REPLICA_SET ? { replicaSet: process.env.REPLICA_SET } : {});
try {
await db.asPromise();
@@ -327,7 +327,7 @@ describe('transactions', function() {
// Session isn't committed
assert.equal(await Character.countDocuments({ title: /hand/i }), 0);
- await tyrion.remove();
+ await tyrion.deleteOne();
// Undo both update and delete since doc should pull from `$session()`
await session.abortTransaction();
@@ -338,7 +338,27 @@ describe('transactions', function() {
assert.deepEqual(fromDb, { name: 'Tyrion Lannister' });
});
+ it('distinct (gh-8006)', async function() {
+ const Character = db.model('gh8006_Character', new Schema({ name: String, rank: String }, { versionKey: false }));
+
+ const session = await db.startSession();
+
+ session.startTransaction();
+ await Character.create([{ name: 'Will Riker', rank: 'Commander' }, { name: 'Jean-Luc Picard', rank: 'Captain' }], { session });
+
+ let names = await Character.distinct('name', {}, { session });
+ assert.deepStrictEqual(names.sort(), ['Jean-Luc Picard', 'Will Riker']);
+
+ names = await Character.distinct('name', { rank: 'Captain' }, { session });
+ assert.deepStrictEqual(names.sort(), ['Jean-Luc Picard']);
+
+ // Undo both update and delete since doc should pull from `$session()`
+ await session.abortTransaction();
+ session.endSession();
+ });
+
it('save() with no changes (gh-8571)', async function() {
+ db.deleteModel(/Test/);
const Test = db.model('Test', Schema({ name: String }));
await Test.createCollection();
@@ -349,4 +369,299 @@ describe('transactions', function() {
});
await session.endSession();
});
+
+ describe('transactionAsyncLocalStorage option', function() {
+ let m;
+ before(async function() {
+ m = new mongoose.Mongoose();
+ m.set('transactionAsyncLocalStorage', true);
+
+ await m.connect(start.uri);
+ });
+
+ after(async function() {
+ await m.disconnect();
+ });
+
+ it('transaction() sets `session` by default if transactionAsyncLocalStorage option is set', async function() {
+ const Test = m.model('Test', m.Schema({ name: String }));
+
+ await Test.createCollection();
+ await Test.deleteMany({});
+
+ let doc = new Test({ name: 'test_transactionAsyncLocalStorage' });
+ await assert.rejects(
+ () => m.connection.transaction(async() => {
+ await doc.save();
+
+ await Test.updateOne({ name: 'foo' }, { name: 'foo' }, { upsert: true });
+
+ let docs = await Test.aggregate([{ $match: { _id: doc._id } }]);
+ assert.equal(docs.length, 1);
+
+ const aggCursor = Test.aggregate([{ $match: { _id: doc._id } }]).cursor();
+ docs = [await aggCursor.next()];
+ assert.equal(docs[0].name, 'test_transactionAsyncLocalStorage');
+
+ docs = await Test.find({ _id: doc._id });
+ assert.equal(docs.length, 1);
+
+ docs = await async function test() {
+ return await Test.findOne({ _id: doc._id });
+ }();
+ assert.equal(doc.name, 'test_transactionAsyncLocalStorage');
+
+ await Test.insertMany([{ name: 'bar' }]);
+
+ throw new Error('Oops!');
+ }),
+ /Oops!/
+ );
+ let exists = await Test.exists({ _id: doc._id });
+ assert.ok(!exists);
+
+ exists = await Test.exists({ name: 'foo' });
+ assert.ok(!exists);
+
+ exists = await Test.exists({ name: 'bar' });
+ assert.ok(!exists);
+
+ doc = new Test({ name: 'test_transactionAsyncLocalStorage' });
+ await assert.rejects(
+ () => m.connection.transaction(async() => {
+ await doc.save({ session: null });
+ throw new Error('Oops!');
+ }),
+ /Oops!/
+ );
+ exists = await Test.exists({ _id: doc._id });
+ assert.ok(exists);
+ });
+ });
+
+ it('transaction() resets $isNew on error', async function() {
+ db.deleteModel(/Test/);
+ const Test = db.model('Test', Schema({ name: String }));
+
+ await Test.createCollection();
+ await Test.deleteMany({});
+
+ const doc = new Test({ name: 'test' });
+ assert.ok(doc.$isNew);
+ await assert.rejects(
+ db.transaction(async(session) => {
+ await doc.save({ session });
+ throw new Error('Oops!');
+ }),
+ /Oops!/
+ );
+ assert.ok(doc.$isNew);
+ const exists = await Test.exists({ _id: doc._id });
+ assert.ok(!exists);
+ });
+
+ it('transaction() resets $isNew between retries (gh-13698)', async function() {
+ db.deleteModel(/Test/);
+ const Test = db.model('Test', Schema({ name: String }));
+
+ await Test.createCollection();
+ await Test.deleteMany({});
+
+ const doc = new Test({ name: 'test' });
+ assert.ok(doc.$isNew);
+ let retryCount = 0;
+ await db.transaction(async(session) => {
+ assert.ok(doc.$isNew);
+ await doc.save({ session });
+ if (++retryCount < 3) {
+ throw new mongoose.mongo.MongoServerError({
+ errorLabels: ['TransientTransactionError']
+ });
+ }
+ });
+
+ const docs = await Test.find();
+ assert.equal(docs.length, 1);
+ assert.equal(docs[0].name, 'test');
+ });
+
+ it('handles resetting array state with $set atomic (gh-13698)', async function() {
+ db.deleteModel(/Test/);
+ const subItemSchema = new mongoose.Schema(
+ {
+ name: { type: String, required: true }
+ },
+ { _id: false }
+ );
+
+ const itemSchema = new mongoose.Schema(
+ {
+ name: { type: String, required: true },
+ subItems: { type: [subItemSchema], required: true }
+ },
+ { _id: false }
+ );
+
+ const schema = new mongoose.Schema({
+ items: { type: [itemSchema], required: true }
+ });
+
+ const Test = db.model('Test', schema);
+
+ const { _id } = await Test.create({
+ items: [
+ { name: 'test1', subItems: [{ name: 'x1' }] },
+ { name: 'test2', subItems: [{ name: 'x2' }] }
+ ]
+ });
+
+ const doc = await Test.findById(_id).orFail();
+ let attempt = 0;
+
+ const res = await db.transaction(async(session) => {
+ await doc.save({ session });
+
+ if (attempt === 0) {
+ attempt += 1;
+ throw new mongoose.mongo.MongoServerError({
+ message: 'Test transient transaction failures & retries',
+ errorLabels: [mongoose.mongo.MongoErrorLabel.TransientTransactionError]
+ });
+ }
+
+ return { answer: 42 };
+ });
+ assert.deepStrictEqual(res, { answer: 42 });
+
+ const { items } = await Test.findById(_id).orFail();
+ assert.ok(Array.isArray(items));
+ assert.equal(items.length, 2);
+ assert.equal(items[0].name, 'test1');
+ assert.equal(items[0].subItems.length, 1);
+ assert.equal(items[0].subItems[0].name, 'x1');
+ assert.equal(items[1].name, 'test2');
+ assert.equal(items[1].subItems.length, 1);
+ assert.equal(items[1].subItems[0].name, 'x2');
+ });
+
+ it('transaction() retains modified status for documents created outside of the transaction then modified inside the transaction (gh-13973)', async function() {
+ db.deleteModel(/Test/);
+ const Test = db.model('Test', Schema({ status: String }));
+
+ await Test.createCollection();
+ await Test.deleteMany({});
+
+ const { _id } = await Test.create({ status: 'test' });
+ const doc = await Test.findById(_id);
+
+ let i = 0;
+ await db.transaction(async(session) => {
+ doc.status = 'test2';
+ assert.ok(doc.$isModified('status'));
+ await doc.save({ session });
+ if (++i < 3) {
+ throw new mongoose.mongo.MongoServerError({
+ errorLabels: ['TransientTransactionError']
+ });
+ }
+ });
+
+ assert.equal(i, 3);
+ });
+
+ it('transaction() avoids duplicating atomic operations (gh-14848)', async function() {
+ db.deleteModel(/Test/);
+ const subItemSchema = new mongoose.Schema(
+ {
+ name: { type: String, required: true }
+ },
+ { _id: false }
+ );
+ const itemSchema = new mongoose.Schema(
+ {
+ name: { type: String, required: true },
+ subItems: { type: [subItemSchema], required: true }
+ },
+ { _id: false }
+ );
+ const schema = new mongoose.Schema({
+ items: { type: [itemSchema], required: true }
+ });
+ const Test = db.model('Test', schema);
+
+
+ await Test.createCollection();
+ await Test.deleteMany({});
+
+ const { _id } = await Test.create({
+ items: [
+ { name: 'test1', subItems: [{ name: 'x1' }] },
+ { name: 'test2', subItems: [{ name: 'x2' }] }
+ ]
+ });
+
+ let doc = await Test.findById(_id);
+
+ doc.items.push({ name: 'test3', subItems: [{ name: 'x3' }] });
+
+ let i = 0;
+ await db.transaction(async(session) => {
+ await doc.save({ session });
+ if (++i < 3) {
+ throw new mongoose.mongo.MongoServerError({
+ errorLabels: ['TransientTransactionError']
+ });
+ }
+ });
+
+ assert.equal(i, 3);
+
+ doc = await Test.findById(_id);
+ assert.equal(doc.items.length, 3);
+ });
+
+ it('doesnt apply schema write concern to transaction operations (gh-11382)', async function() {
+ db.deleteModel(/Test/);
+ const Test = db.model('Test', Schema({ status: String }, { writeConcern: { w: 'majority' } }));
+
+ await Test.createCollection();
+ await Test.deleteMany({});
+
+ const session = await db.startSession();
+
+ await session.withTransaction(async function() {
+ await Test.findOneAndUpdate({}, { name: 'test' }, { session });
+ });
+
+ await session.endSession();
+ });
+
+ it('allows custom transaction wrappers to store and reset document state with $createModifiedPathsSnapshot (gh-14268)', async function() {
+ db.deleteModel(/Test/);
+ const Test = db.model('Test', Schema({ name: String }, { writeConcern: { w: 'majority' } }));
+
+ await Test.createCollection();
+ await Test.deleteMany({});
+
+ const { _id } = await Test.create({ name: 'foo' });
+ const doc = await Test.findById(_id);
+ doc.name = 'bar';
+ for (let i = 0; i < 2; ++i) {
+ const session = await db.startSession();
+ const snapshot = doc.$createModifiedPathsSnapshot();
+ session.startTransaction();
+
+ await doc.save({ session });
+ if (i === 0) {
+ await session.abortTransaction();
+ doc.$restoreModifiedPathsSnapshot(snapshot);
+ } else {
+ await session.commitTransaction();
+ }
+ await session.endSession();
+ }
+
+ const { name } = await Test.findById(_id);
+ assert.strictEqual(name, 'bar');
+ });
});
diff --git a/test/docs/validation.test.js b/test/docs/validation.test.js
index 5357d1f9640..20e654a4f34 100644
--- a/test/docs/validation.test.js
+++ b/test/docs/validation.test.js
@@ -3,8 +3,6 @@ const assert = require('assert');
const mongoose = require('../../');
const start = require('../common');
-const Promise = global.Promise || require('bluebird');
-
describe('validation docs', function() {
let db;
const Schema = mongoose.Schema;
@@ -16,6 +14,8 @@ describe('validation docs', function() {
});
});
+ beforeEach(() => db.deleteModel(/Vehicle/));
+
after(async function() {
await db.close();
});
@@ -176,14 +176,20 @@ describe('validation docs', function() {
// acquit:ignore:end
const dup = [{ username: 'Val' }, { username: 'Val' }];
- U1.create(dup, err => {
- // Race condition! This may save successfully, depending on whether
- // MongoDB built the index before writing the 2 docs.
- // acquit:ignore:start
- err;
- --remaining || done();
- // acquit:ignore:end
- });
+ // Race condition! This may save successfully, depending on whether
+ // MongoDB built the index before writing the 2 docs.
+ U1.create(dup).
+ then(() => {
+ // acquit:ignore:start
+ --remaining || done();
+ // acquit:ignore:end
+ }).
+ catch(err => {
+ // acquit:ignore:start
+ err;
+ --remaining || done();
+ // acquit:ignore:end
+ });
// You need to wait for Mongoose to finish building the `unique`
// index before writing. You only need to build indexes once for
@@ -193,7 +199,7 @@ describe('validation docs', function() {
U2.init().
then(() => U2.create(dup)).
catch(error => {
- // Will error, but will *not* be a mongoose validation error, it will be
+ // `U2.create()` will error, but will *not* be a mongoose validation error, it will be
// a duplicate key error.
// See: https://masteringjs.io/tutorials/mongoose/e11000-duplicate-key
assert.ok(error);
@@ -380,6 +386,53 @@ describe('validation docs', function() {
// acquit:ignore:end
});
+ it('Cast Error Message Overwrite', function() {
+ const vehicleSchema = new mongoose.Schema({
+ numWheels: {
+ type: Number,
+ cast: '{VALUE} is not a number'
+ }
+ });
+ const Vehicle = db.model('Vehicle', vehicleSchema);
+
+ const doc = new Vehicle({ numWheels: 'pie' });
+ const err = doc.validateSync();
+
+ err.errors['numWheels'].name; // 'CastError'
+ // "pie" is not a number
+ err.errors['numWheels'].message;
+ // acquit:ignore:start
+ assert.equal(err.errors['numWheels'].name, 'CastError');
+ assert.equal(err.errors['numWheels'].message,
+ '"pie" is not a number');
+ db.deleteModel(/Vehicle/);
+ // acquit:ignore:end
+ });
+
+ /* eslint-disable no-unused-vars */
+ it('Cast Error Message Function Overwrite', function() {
+ const vehicleSchema = new mongoose.Schema({
+ numWheels: {
+ type: Number,
+ cast: [null, (value, path, model, kind) => `"${value}" is not a number`]
+ }
+ });
+ const Vehicle = db.model('Vehicle', vehicleSchema);
+
+ const doc = new Vehicle({ numWheels: 'pie' });
+ const err = doc.validateSync();
+
+ err.errors['numWheels'].name; // 'CastError'
+ // "pie" is not a number
+ err.errors['numWheels'].message;
+ // acquit:ignore:start
+ assert.equal(err.errors['numWheels'].name, 'CastError');
+ assert.equal(err.errors['numWheels'].message,
+ '"pie" is not a number');
+ db.deleteModel(/Vehicle/);
+ // acquit:ignore:end
+ });
+
it('Global SchemaType Validation', async function() {
// Add a custom validator to all strings
mongoose.Schema.Types.String.set('validate', v => v == null || v > 0);
@@ -541,10 +594,7 @@ describe('validation docs', function() {
* you try to explicitly `$unset` the key.
*/
- it('Update Validators Only Run On Updated Paths', function(done) {
- // acquit:ignore:start
- let outstanding = 2;
- // acquit:ignore:end
+ it('Update Validators Only Run On Updated Paths', async function() {
const kittenSchema = new Schema({
name: { type: String, required: true },
age: Number
@@ -554,22 +604,14 @@ describe('validation docs', function() {
const update = { color: 'blue' };
const opts = { runValidators: true };
- Kitten.updateOne({}, update, opts, function() {
- // Operation succeeds despite the fact that 'name' is not specified
- // acquit:ignore:start
- --outstanding || done();
- // acquit:ignore:end
- });
+ // Operation succeeds despite the fact that 'name' is not specified
+ await Kitten.updateOne({}, update, opts);
const unset = { $unset: { name: 1 } };
- Kitten.updateOne({}, unset, opts, function(err) {
- // Operation fails because 'name' is required
- assert.ok(err);
- assert.ok(err.errors['name']);
- // acquit:ignore:start
- --outstanding || done();
- // acquit:ignore:end
- });
+ // Operation fails because 'name' is required
+ const err = await Kitten.updateOne({}, unset, opts).then(() => null, err => err);
+ assert.ok(err);
+ assert.ok(err.errors['name']);
});
/**
diff --git a/test/docs/virtuals.test.js b/test/docs/virtuals.test.js
index 5517d75e447..2918049936e 100644
--- a/test/docs/virtuals.test.js
+++ b/test/docs/virtuals.test.js
@@ -174,4 +174,76 @@ describe('Virtuals', function() {
assert.equal(doc.author.email, 'test@gmail.com');
// acquit:ignore:end
});
+
+ it('schema-options fullName', function() {
+ const userSchema = mongoose.Schema({
+ firstName: String,
+ lastName: String
+ }, {
+ virtuals: {
+ // Create a virtual property `fullName` with a getter and setter
+ fullName: {
+ get() { return `${this.firstName} ${this.lastName}`; },
+ set(v) {
+ // `v` is the value being set, so use the value to set
+ // `firstName` and `lastName`.
+ const firstName = v.substring(0, v.indexOf(' '));
+ const lastName = v.substring(v.indexOf(' ') + 1);
+ this.set({ firstName, lastName });
+ }
+ }
+ }
+ });
+ const User = mongoose.model('User', userSchema);
+
+ const doc = new User();
+ // Vanilla JavaScript assignment triggers the setter
+ doc.fullName = 'Jean-Luc Picard';
+
+ doc.fullName; // 'Jean-Luc Picard'
+ doc.firstName; // 'Jean-Luc'
+ doc.lastName; // 'Picard'
+ // acquit:ignore:start
+ assert.equal(doc.fullName, 'Jean-Luc Picard');
+ assert.equal(doc.firstName, 'Jean-Luc');
+ assert.equal(doc.lastName, 'Picard');
+ // acquit:ignore:end
+ });
+
+ it('schema-options populate', async function() {
+ const userSchema = mongoose.Schema({ _id: Number, email: String });
+ const blogPostSchema = mongoose.Schema({
+ title: String,
+ authorId: Number
+ }, {
+ virtuals: {
+ // When you `populate()` the `author` virtual, Mongoose will find the
+ // first document in the User model whose `_id` matches this document's
+ // `authorId` property.
+ author: {
+ options: {
+ ref: 'User',
+ localField: 'authorId',
+ foreignField: '_id',
+ justOne: true
+ }
+ }
+ }
+ });
+ const User = mongoose.model('User', userSchema);
+ const BlogPost = mongoose.model('BlogPost', blogPostSchema);
+
+ // acquit:ignore:start
+ await BlogPost.deleteMany({});
+ await User.deleteMany({});
+ // acquit:ignore:end
+ await BlogPost.create({ title: 'Introduction to Mongoose', authorId: 1 });
+ await User.create({ _id: 1, email: 'test@gmail.com' });
+
+ const doc = await BlogPost.findOne().populate('author');
+ doc.author.email; // 'test@gmail.com'
+ // acquit:ignore:start
+ assert.equal(doc.author.email, 'test@gmail.com');
+ // acquit:ignore:end
+ });
});
diff --git a/test/document.modified.test.js b/test/document.modified.test.js
index 3a0ce0bfb35..4cacfafc9eb 100644
--- a/test/document.modified.test.js
+++ b/test/document.modified.test.js
@@ -87,38 +87,27 @@ describe('document modified', function() {
});
describe('modified states', function() {
- it('reset after save', function(done) {
+ it('reset after save', async function() {
const B = BlogPost;
const b = new B();
b.numbers.push(3);
- b.save(function(err) {
- assert.strictEqual(null, err);
+ await b.save();
- b.numbers.push(3);
- b.save(function(err1) {
- assert.strictEqual(null, err1);
-
- B.findById(b, function(err2, b) {
- assert.strictEqual(null, err2);
- assert.equal(b.numbers.length, 2);
+ b.numbers.push(3);
+ await b.save();
- done();
- });
- });
- });
+ const blogPost = await B.findById(b);
+ assert.equal(blogPost.numbers.length, 2);
});
- it('of embedded docs reset after save', function(done) {
+ it('of embedded docs reset after save', async() => {
const post = new BlogPost({ title: 'hocus pocus' });
post.comments.push({ title: 'Humpty Dumpty', comments: [{ title: 'nested' }] });
- post.save(function(err) {
- assert.strictEqual(null, err);
- const mFlag = post.comments[0].isModified('title');
- assert.equal(mFlag, false);
- assert.equal(post.isModified('title'), false);
- done();
- });
+ await post.save();
+ const mFlag = post.comments[0].isModified('title');
+ assert.equal(mFlag, false);
+ assert.equal(post.isModified('title'), false);
});
});
@@ -176,6 +165,22 @@ describe('document modified', function() {
assert.equal(post.isModified('title'), false);
});
+ it('should support passing a string of keys separated by a blank space as the first argument', function() {
+ const post = new BlogPost();
+ post.init({
+ title: 'Test',
+ slug: 'test',
+ date: new Date()
+ });
+
+ assert.equal(post.isModified('title'), false);
+ post.set('title', 'modified title');
+ assert.equal(post.isModified('title'), true);
+ assert.equal(post.isModified('slug'), false);
+ assert.equal(post.isModified('title slug'), true);
+ });
+
+
describe('on DocumentArray', function() {
it('work', function() {
const post = new BlogPost();
@@ -192,6 +197,43 @@ describe('document modified', function() {
assert.equal(post.isModified('comments.0.title'), true);
assert.equal(post.isDirectModified('comments.0.title'), true);
});
+ it('with push (gh-14024)', async function() {
+ const post = new BlogPost();
+ post.init({
+ title: 'Test',
+ slug: 'test',
+ comments: [{ title: 'Test', date: new Date(), body: 'Test' }]
+ });
+
+ post.comments.push({ title: 'new comment', body: 'test' });
+
+ assert.equal(post.isModified('comments.0.title', { ignoreAtomics: true }), false);
+ assert.equal(post.isModified('comments.0.body', { ignoreAtomics: true }), false);
+ assert.equal(post.get('comments')[0].isModified('body', { ignoreAtomics: true }), false);
+ });
+ it('with push and set (gh-14024)', async function() {
+ const post = new BlogPost();
+ post.init({
+ title: 'Test',
+ slug: 'test',
+ comments: [{ title: 'Test', date: new Date(), body: 'Test' }]
+ });
+
+ post.comments.push({ title: 'new comment', body: 'test' });
+ post.get('comments')[0].set('title', 'Woot');
+
+ assert.equal(post.isModified('comments', { ignoreAtomics: true }), true);
+ assert.equal(post.isModified('comments.0.title', { ignoreAtomics: true }), true);
+ assert.equal(post.isDirectModified('comments.0.title'), true);
+ assert.equal(post.isDirectModified('comments.0.body'), false);
+ assert.equal(post.isModified('comments.0.body', { ignoreAtomics: true }), false);
+
+ assert.equal(post.isModified('comments', { ignoreAtomics: true }), true);
+ assert.equal(post.isModified('comments.0.title', { ignoreAtomics: true }), true);
+ assert.equal(post.isDirectModified('comments.0.title'), true);
+ assert.equal(post.isDirectModified('comments.0.body'), false);
+ assert.equal(post.isModified('comments.0.body', { ignoreAtomics: true }), false);
+ });
it('with accessors', function() {
const post = new BlogPost();
post.init({
@@ -224,7 +266,7 @@ describe('document modified', function() {
});
});
- it('on entire document', function(done) {
+ it('on entire document', async function() {
const doc = {
title: 'Test',
slug: 'test',
@@ -243,33 +285,32 @@ describe('document modified', function() {
]
};
- BlogPost.create(doc, function(err, post) {
- assert.ifError(err);
- BlogPost.findById(post.id, function(err, postRead) {
- assert.ifError(err);
- // set the same data again back to the document.
- // expected result, nothing should be set to modified
- assert.equal(postRead.isModified('comments'), false);
- assert.equal(postRead.isNew, false);
- postRead.set(postRead.toObject());
-
- assert.equal(postRead.isModified('title'), false);
- assert.equal(postRead.isModified('slug'), false);
- assert.equal(postRead.isModified('date'), false);
- assert.equal(postRead.isModified('meta.date'), false);
- assert.equal(postRead.isModified('meta.visitors'), false);
- assert.equal(postRead.isModified('published'), false);
- assert.equal(postRead.isModified('mixed'), false);
- assert.equal(postRead.isModified('numbers'), false);
- assert.equal(postRead.isModified('owners'), false);
- assert.equal(postRead.isModified('comments'), false);
- const arr = postRead.comments.slice();
- arr[2] = postRead.comments.create({ title: 'index' });
- postRead.comments = arr;
- assert.equal(postRead.isModified('comments'), true);
- done();
- });
- });
+ const post = await BlogPost.create(doc);
+
+ const postRead = await BlogPost.findById(post.id);
+
+ // set the same data again back to the document.
+ // expected result, nothing should be set to modified
+ assert.equal(postRead.isModified('comments'), false);
+ assert.equal(postRead.isNew, false);
+ postRead.set(postRead.toObject());
+
+ assert.equal(postRead.isModified('title'), false);
+ assert.equal(postRead.isModified('slug'), false);
+ assert.equal(postRead.isModified('date'), false);
+ assert.equal(postRead.isModified('meta.date'), false);
+ assert.equal(postRead.isModified('meta.visitors'), false);
+ assert.equal(postRead.isModified('published'), false);
+ assert.equal(postRead.isModified('mixed'), false);
+ assert.equal(postRead.isModified('numbers'), false);
+ assert.equal(postRead.isModified('owners'), false);
+ assert.equal(postRead.isModified('comments'), false);
+
+ const arr = postRead.comments.slice();
+ arr[2] = postRead.comments.create({ title: 'index' });
+ postRead.comments = arr;
+
+ assert.equal(postRead.isModified('comments'), true);
});
it('should let you set ref paths (gh-1530)', async function() {
@@ -417,30 +458,29 @@ describe('document modified', function() {
assert.ok(test.bars[1]._id);
});
- it('updates embedded doc parents upon direct assignment (gh-5189)', function(done) {
+ it('updates embedded doc parents upon direct assignment (gh-5189)', async function() {
const familySchema = new Schema({
children: [{ name: { type: String, required: true } }]
});
db.deleteModel(/Test/);
const Family = db.model('Test', familySchema);
- Family.create({
+ const family = await Family.create({
children: [
{ name: 'John' },
{ name: 'Mary' }
]
- }, function(err, family) {
- family.set({ children: family.children.slice(1) });
- family.children.forEach(function(child) {
- child.set({ name: 'Maryanne' });
- });
-
- assert.equal(family.validateSync(), undefined);
- done();
});
+
+ family.set({ children: family.children.slice(1) });
+ family.children.forEach(function(child) {
+ child.set({ name: 'Maryanne' });
+ });
+
+ assert.equal(family.validateSync(), undefined);
});
});
- it('should support setting mixed paths by string (gh-1418)', function(done) {
+ it('should support setting mixed paths by string (gh-1418)', async function() {
const BlogPost = db.model('Test', new Schema({ mixed: {} }));
let b = new BlogPost();
b.init({ mixed: {} });
@@ -460,23 +500,16 @@ describe('document modified', function() {
assert.equal(b.get(path), 4);
b = new BlogPost({ mixed: {} });
- b.save(function(err) {
- assert.ifError(err);
-
- path = 'mixed.9a.x';
- b.set(path, 8);
- assert.ok(b.isModified(path));
- assert.equal(b.get(path), 8);
-
- b.save(function(err) {
- assert.ifError(err);
- BlogPost.findById(b, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.get(path), 8);
- done();
- });
- });
- });
+ await b.save();
+ path = 'mixed.9a.x';
+ b.set(path, 8);
+ assert.ok(b.isModified(path));
+ assert.equal(b.get(path), 8);
+
+ await b.save();
+ const doc = await BlogPost.findById(b);
+ assert.equal(doc.get(path), 8);
+
});
it('should mark multi-level nested schemas as modified (gh-1754)', async function() {
@@ -507,7 +540,7 @@ describe('document modified', function() {
assert.equal(p.child[0].grandChild[0].name, 'Jason');
});
- it('should reset the modified state after calling unmarkModified', function(done) {
+ it('should reset the modified state after calling unmarkModified', async function() {
const b = new BlogPost();
assert.equal(b.isModified('author'), false);
b.author = 'foo';
@@ -517,32 +550,25 @@ describe('document modified', function() {
assert.equal(b.isModified('author'), false);
assert.equal(b.isModified(), false);
- b.save(function(err) {
- assert.strictEqual(null, err);
-
- BlogPost.findById(b._id, function(err2, b2) {
- assert.strictEqual(null, err2);
-
- assert.equal(b2.isModified('author'), false);
- assert.equal(b2.isModified(), false);
- b2.author = 'bar';
- assert.equal(b2.isModified('author'), true);
- assert.equal(b2.isModified(), true);
- b2.unmarkModified('author');
- assert.equal(b2.isModified('author'), false);
- assert.equal(b2.isModified(), false);
-
- b2.save(function(err3) {
- assert.strictEqual(err3, null);
- BlogPost.findById(b._id, function(err4, b3) {
- assert.strictEqual(err4, null);
- // was not saved because modified state was unset
- assert.equal(b3.author, 'foo');
- done();
- });
- });
- });
- });
+ await b.save();
+
+ const b2 = await BlogPost.findById(b._id);
+
+ assert.equal(b2.isModified('author'), false);
+ assert.equal(b2.isModified(), false);
+ b2.author = 'bar';
+ assert.equal(b2.isModified('author'), true);
+ assert.equal(b2.isModified(), true);
+ b2.unmarkModified('author');
+ assert.equal(b2.isModified('author'), false);
+ assert.equal(b2.isModified(), false);
+
+ await b2.save();
+
+ const b3 = await BlogPost.findById(b._id);
+
+ // was not saved because modified state was unset
+ assert.equal(b3.author, 'foo');
});
});
});
diff --git a/test/document.populate.test.js b/test/document.populate.test.js
index efb8848cd98..bbfbc1df99a 100644
--- a/test/document.populate.test.js
+++ b/test/document.populate.test.js
@@ -552,7 +552,7 @@ describe('document.populate', function() {
const docs = await Person.create([{ name: 'Axl Rose' }, { name: 'Slash' }]);
- const band = await Band.create({
+ let band = await Band.create({
name: 'Guns N\' Roses',
members: [docs[0]._id, docs[1]],
lead: docs[0]._id
@@ -561,18 +561,44 @@ describe('document.populate', function() {
await band.populate('members');
assert.equal(band.members[0].name, 'Axl Rose');
+ assert.ok(band.members.isMongooseArray);
+ assert.ok(band.members.addToSet);
band.depopulate('members');
assert.ok(!band.members[0].name);
assert.equal(band.members[0].toString(), docs[0]._id.toString());
assert.equal(band.members[1].toString(), docs[1]._id.toString());
+ assert.ok(band.members.isMongooseArray);
+ assert.ok(band.members.addToSet);
+ assert.deepStrictEqual(band.getChanges(), {});
assert.ok(!band.populated('members'));
assert.ok(!band.populated('lead'));
- await band.populate('lead');
+ await band.populate('lead');
assert.equal(band.lead.name, 'Axl Rose');
band.depopulate('lead');
assert.ok(!band.lead.name);
+ assert.deepStrictEqual(band.getChanges(), {});
assert.equal(band.lead.toString(), docs[0]._id.toString());
+
+ const newId = new mongoose.Types.ObjectId();
+ band.lead = newId;
+ assert.deepStrictEqual(band.getChanges(), { $set: { lead: newId } });
+
+ band = await Band.findById(band._id).orFail();
+ await band.populate('members');
+
+ assert.equal(band.members[0].name, 'Axl Rose');
+ assert.ok(band.members.isMongooseArray);
+ assert.ok(band.members.addToSet);
+ band.depopulate('members');
+ assert.ok(!band.members[0].name);
+ assert.equal(band.members[0].toString(), docs[0]._id.toString());
+ assert.equal(band.members[1].toString(), docs[1]._id.toString());
+ assert.ok(band.members.isMongooseArray);
+ assert.ok(band.members.addToSet);
+ assert.deepStrictEqual(band.getChanges(), {});
+ assert.ok(!band.populated('members'));
+ assert.ok(!band.populated('lead'));
});
it('depopulates all (gh-6073)', async function() {
@@ -706,7 +732,62 @@ describe('document.populate', function() {
author.depopulate('books');
assert.ok(author.books);
assert.strictEqual(author.books.length, 0);
+ });
+
+ it('depopulates after pushing manually populated (gh-2509)', async function() {
+ const Book = db.model(
+ 'Book',
+ new mongoose.Schema({
+ name: String,
+ chapters: Number
+ })
+ );
+ const Author = db.model(
+ 'Person',
+ new mongoose.Schema({
+ name: String,
+ books: { type: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Book' }], default: [] }
+ })
+ );
+ const books = await Book.create([
+ { name: 'Lost Years of Merlin' },
+ { name: 'Seven Songs of Merlin' },
+ { name: 'Fires of Merlin' }
+ ]);
+ let author = new Author({
+ name: 'T.A. Barron',
+ books: [books[0]._id]
+ });
+ await author.save();
+ await author.populate('books');
+ assert.ok(author.books);
+ assert.strictEqual(author.books.length, 1);
+
+ author.books.push(books[1]);
+ author.depopulate('books');
+ assert.ok(author.books);
+ assert.ok(author.books.isMongooseArray);
+ assert.ok(!author.$populated('books'));
+ assert.deepStrictEqual(author.books, [books[0]._id, books[1]._id]);
+ await author.save();
+
+ author = await Author.findById(author._id).orFail();
+ assert.strictEqual(author.books.length, 2);
+ assert.deepStrictEqual(author.books, [books[0]._id, books[1]._id]);
+ await author.populate('books');
+ author.books.pull(books[0]._id);
+ assert.strictEqual(author.books.length, 1);
+ assert.equal(author.books[0].name, 'Seven Songs of Merlin');
+ author.depopulate();
+ assert.ok(author.books);
+ assert.ok(author.books.isMongooseArray);
+ assert.deepStrictEqual(author.books, [books[1]._id]);
+ await author.save();
+
+ author = await Author.findById(author._id).orFail();
+ assert.strictEqual(author.books.length, 1);
+ assert.deepStrictEqual(author.books, [books[1]._id]);
});
});
@@ -934,4 +1015,64 @@ describe('document.populate', function() {
assert.ok(foundBook.populated('authorId'));
assert.ok(foundBook.authorId.populated('websiteId'));
});
+
+ it('works when populating a nested document inside an array parent (gh-14435)', async function() {
+ const CodeSchema = new Schema({
+ code: String
+ });
+
+ const UserSchema = new Schema({
+ username: String,
+ extras: [
+ new Schema({
+ config: new Schema({
+ paymentConfiguration: {
+ paymentMethods: [
+ {
+ type: Schema.Types.ObjectId,
+ ref: 'Code'
+ }
+ ]
+ }
+ })
+ })
+ ]
+ });
+
+ const Code = db.model('Code', CodeSchema);
+ const CodeUser = db.model('CodeUser', UserSchema);
+
+ const code = await Code.create({
+ code: 'test code'
+ });
+
+ await CodeUser.create({
+ username: 'TestUser',
+ extras: [
+ {
+ config: {
+ paymentConfiguration: {
+ paymentMethods: [code._id]
+ }
+ }
+ }
+ ]
+ });
+
+ const codeUser = await CodeUser.findOne({ username: 'TestUser' }).populate(
+ 'extras.config.paymentConfiguration.paymentMethods'
+ );
+
+ assert.ok(codeUser.username);
+ assert.strictEqual(codeUser.username, 'TestUser');
+ assert.ok(codeUser.extras);
+ assert.strictEqual(codeUser.extras.length, 1);
+ assert.ok(codeUser.extras[0]);
+ assert.ok(codeUser.extras[0].config);
+ assert.ok(codeUser.extras[0].config.paymentConfiguration);
+ assert.ok(codeUser.extras[0].config.paymentConfiguration.paymentMethods);
+ assert.strictEqual(codeUser.extras[0].config.paymentConfiguration.paymentMethods.length, 1);
+ assert.deepStrictEqual(codeUser.extras[0].config.paymentConfiguration.paymentMethods[0]._id, code._id);
+ assert.strictEqual(codeUser.extras[0].config.paymentConfiguration.paymentMethods[0].code, 'test code');
+ });
});
diff --git a/test/document.strict.test.js b/test/document.strict.test.js
index db8b9764852..119e0daf788 100644
--- a/test/document.strict.test.js
+++ b/test/document.strict.test.js
@@ -111,14 +111,12 @@ describe('document: strict mode:', function() {
assert.ok(!s3.rouge);
});
- it('when using Model#create', function(done) {
+ it('when using Model#create', async function() {
// strict on create
- Strict.create({ content: 'sample2', rouge: 'data' }, function(err, doc) {
- assert.equal(doc.content, 'sample2');
- assert.ok(!('rouge' in doc));
- assert.ok(!doc.rouge);
- done();
- });
+ const doc = await Strict.create({ content: 'sample2', rouge: 'data' });
+ assert.equal(doc.content, 'sample2');
+ assert.ok(!('rouge' in doc));
+ assert.ok(!doc.rouge);
});
});
@@ -157,7 +155,7 @@ describe('document: strict mode:', function() {
assert.ok(!s.shouldnt);
});
- it('sub doc', function(done) {
+ it('sub doc', async function() {
const lax = new Schema({
ts: { type: Date, default: Date.now },
content: String
@@ -195,12 +193,10 @@ describe('document: strict mode:', function() {
assert.ok(!s3.dox[0].rouge);
// strict on create
- Strict.create({ dox: [{ content: 'sample2', rouge: 'data' }] }, function(err, doc) {
- assert.equal(doc.dox[0].content, 'sample2');
- assert.ok(!('rouge' in doc.dox[0]));
- assert.ok(!doc.dox[0].rouge);
- done();
- });
+ const doc = await Strict.create({ dox: [{ content: 'sample2', rouge: 'data' }] });
+ assert.equal(doc.dox[0].content, 'sample2');
+ assert.ok(!('rouge' in doc.dox[0]));
+ assert.ok(!doc.dox[0].rouge);
});
it('virtuals', function() {
@@ -243,7 +239,7 @@ describe('document: strict mode:', function() {
assert.equal(setCount, 2);
});
- it('can be overridden during set()', function(done) {
+ it('can be overridden during set()', async function() {
const strict = new Schema({
bool: Boolean
});
@@ -255,27 +251,19 @@ describe('document: strict mode:', function() {
const doc = s.toObject();
doc.notInSchema = true;
- Strict.collection.insertOne(doc, { w: 1 }, function(err) {
- assert.ifError(err);
- Strict.findById(doc._id, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc._doc.bool, true);
- assert.equal(doc._doc.notInSchema, true);
- doc.bool = undefined;
- doc.set('notInSchema', undefined, { strict: false });
- doc.save(function() {
- Strict.findById(doc._id, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc._doc.bool, undefined);
- assert.equal(doc._doc.notInSchema, undefined);
- done();
- });
- });
- });
- });
+ await Strict.collection.insertOne(doc, { w: 1 });
+ const foundDoc = await Strict.findById(doc._id);
+ assert.equal(foundDoc._doc.bool, true);
+ assert.equal(foundDoc._doc.notInSchema, true);
+ foundDoc.bool = undefined;
+ foundDoc.set('notInSchema', undefined, { strict: false });
+ await foundDoc.save();
+ const foundDoc2 = await Strict.findById(doc._id);
+ assert.equal(foundDoc2._doc.bool, undefined);
+ assert.equal(foundDoc2._doc.notInSchema, undefined);
});
- it('can be overridden during update()', function(done) {
+ it('can be overridden during update()', async function() {
const strict = new Schema({
bool: Boolean
});
@@ -287,30 +275,20 @@ describe('document: strict mode:', function() {
const doc = s.toObject();
doc.notInSchema = true;
- Strict.collection.insertOne(doc, function(err) {
- assert.ifError(err);
-
- Strict.findById(doc._id, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc._doc.bool, true);
- assert.equal(doc._doc.notInSchema, true);
-
- Strict.updateOne({ _id: doc._id }, { $unset: { bool: 1, notInSchema: 1 } }, { strict: false },
- function(err) {
- assert.ifError(err);
-
- Strict.findById(doc._id, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc._doc.bool, undefined);
- assert.equal(doc._doc.notInSchema, undefined);
- done();
- });
- });
- });
- });
+ await Strict.collection.insertOne(doc);
+
+ const doc2 = await Strict.findById(doc._id);
+ assert.equal(doc2._doc.bool, true);
+ assert.equal(doc2._doc.notInSchema, true);
+
+ await Strict.updateOne({ _id: doc._id }, { $unset: { bool: 1, notInSchema: 1 } }, { strict: false });
+
+ const doc3 = await Strict.findById(doc._id);
+ assert.equal(doc3._doc.bool, undefined);
+ assert.equal(doc3._doc.notInSchema, undefined);
});
- it('can be overwritten with findOneAndUpdate (gh-1967)', function(done) {
+ it('can be overwritten with findOneAndUpdate (gh-1967)', async function() {
const strict = new Schema({
bool: Boolean
});
@@ -322,27 +300,17 @@ describe('document: strict mode:', function() {
const doc = s.toObject();
doc.notInSchema = true;
- Strict.collection.insertOne(doc, { w: 1 }, function(err) {
- assert.ifError(err);
-
- Strict.findById(doc._id, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc._doc.bool, true);
- assert.equal(doc._doc.notInSchema, true);
-
- Strict.findOneAndUpdate({ _id: doc._id }, { $unset: { bool: 1, notInSchema: 1 } }, { strict: false, w: 1 },
- function(err) {
- assert.ifError(err);
-
- Strict.findById(doc._id, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc._doc.bool, undefined);
- assert.equal(doc._doc.notInSchema, undefined);
- done();
- });
- });
- });
- });
+ await Strict.collection.insertOne(doc, { w: 1 });
+
+ const doc1 = await Strict.findById(doc._id);
+ assert.equal(doc1._doc.bool, true);
+ assert.equal(doc1._doc.notInSchema, true);
+
+ await Strict.findOneAndUpdate({ _id: doc._id }, { $unset: { bool: 1, notInSchema: 1 } }, { strict: false, w: 1 });
+
+ const doc2 = await Strict.findById(doc._id);
+ assert.equal(doc2._doc.bool, undefined);
+ assert.equal(doc2._doc.notInSchema, undefined);
});
describe('"throws" mode', function() {
diff --git a/test/document.test.js b/test/document.test.js
index 5eb33c0f86f..7a2e1e607ad 100644
--- a/test/document.test.js
+++ b/test/document.test.js
@@ -8,7 +8,7 @@ const start = require('./common');
const Document = require('../lib/document');
const EventEmitter = require('events').EventEmitter;
-const ArraySubdocument = require('../lib/types/ArraySubdocument');
+const ArraySubdocument = require('../lib/types/arraySubdocument');
const Query = require('../lib/query');
const assert = require('assert');
const idGetter = require('../lib/helpers/schema/idGetter');
@@ -100,15 +100,6 @@ schema.path('date').set(function(v) {
return v;
});
-/**
- * Method subject to hooks. Simply fires the callback once the hooks are
- * executed.
- */
-
-TestDocument.prototype.hooksTest = function(fn) {
- fn(null, arguments);
-};
-
const childSchema = new Schema({ counter: Number });
const parentSchema = new Schema({
@@ -146,17 +137,18 @@ describe('document', function() {
});
});
- describe('delete', function() {
+ describe('deleteOne', function() {
it('deletes the document', async function() {
const schema = new Schema({ x: String });
const Test = db.model('Test', schema);
const test = new Test({ x: 'test' });
const doc = await test.save();
- await doc.delete();
+ const q = doc.deleteOne();
+ assert.ok(q instanceof mongoose.Query, `Expected query, got ${q.constructor.name}`);
+ await q;
const found = await Test.findOne({ _id: doc._id });
assert.strictEqual(found, null);
-
});
});
@@ -432,9 +424,10 @@ describe('document', function() {
delete ret.oids;
ret._id = ret._id.toString();
};
+ delete doc.schema._defaultToObjectOptionsMap;
clone = doc.toObject();
assert.equal(doc.id, clone._id);
- assert.ok(undefined === clone.em);
+ assert.strictEqual(clone.em, undefined);
assert.ok(undefined === clone.numbers);
assert.ok(undefined === clone.oids);
assert.equal(clone.test, 'test');
@@ -451,6 +444,7 @@ describe('document', function() {
return { myid: ret._id.toString() };
};
+ delete doc.schema._defaultToObjectOptionsMap;
clone = doc.toObject();
assert.deepEqual(out, clone);
@@ -488,6 +482,7 @@ describe('document', function() {
// all done
delete doc.schema.options.toObject;
+ delete doc.schema._defaultToObjectOptionsMap;
});
it('toObject transform', async function() {
@@ -519,6 +514,34 @@ describe('document', function() {
docs.toObject({ transform: true });
});
+ it('propagates toObject transform function to all subdocuments (gh-14589)', async function() {
+ const schema = new mongoose.Schema({
+ name: String,
+ docArr: [{ name: String }],
+ subdoc: new mongoose.Schema({ name: String })
+ });
+ const TestModel = db.model('Test', schema);
+
+ const doc = new TestModel({
+ name: 'test',
+ docArr: [{ name: 'test' }],
+ subdoc: { name: 'test' }
+ });
+
+ // pass the transform as an inline option. Deletes `_id` property
+ // from both the top-level document and the subdocument.
+ const obj = doc.toObject({ transform: deleteId });
+
+ assert.equal(obj._id, undefined);
+ assert.equal(obj.subdoc._id, undefined);
+ assert.equal(obj.docArr[0]._id, undefined);
+
+ function deleteId(doc, ret) {
+ delete ret._id;
+ return ret;
+ }
+ });
+
it('disabling aliases in toObject options (gh-7548)', function() {
const schema = new mongoose.Schema({
name: {
@@ -602,12 +625,12 @@ describe('document', function() {
assert.equal(doc.val, 'test2');
});
- it('allows you to skip validation on save (gh-2981)', function() {
+ it('allows you to skip validation on save (gh-2981)', async function() {
const schema = new Schema({ name: { type: String, required: true } });
const MyModel = db.model('Test', schema);
const doc = new MyModel();
- return doc.save({ validateBeforeSave: false });
+ await doc.save({ validateBeforeSave: false });
});
it('doesnt use custom toObject options on save', async function() {
@@ -682,11 +705,6 @@ describe('document', function() {
name: String,
email: String
});
- const topicSchema = new Schema({
- title: String,
- email: String,
- followers: [userSchema]
- });
userSchema.options.toObject = {
transform: function(doc, ret) {
@@ -694,6 +712,12 @@ describe('document', function() {
}
};
+ const topicSchema = new Schema({
+ title: String,
+ email: String,
+ followers: [userSchema]
+ });
+
topicSchema.options.toObject = {
transform: function(doc, ret) {
ret.title = ret.title.toLowerCase();
@@ -721,32 +745,27 @@ describe('document', function() {
lastName: String,
password: String
});
-
userSchema.virtual('fullName').get(function() {
return this.firstName + ' ' + this.lastName;
});
-
userSchema.set('toObject', { virtuals: false });
const postSchema = new Schema({
owner: { type: Schema.Types.ObjectId, ref: 'User' },
content: String
});
-
postSchema.virtual('capContent').get(function() {
return this.content.toUpperCase();
});
-
postSchema.set('toObject', { virtuals: true });
+
const User = db.model('User', userSchema);
const Post = db.model('BlogPost', postSchema);
const user = new User({ firstName: 'Joe', lastName: 'Smith', password: 'password' });
-
const savedUser = await user.save();
const post = await Post.create({ owner: savedUser._id, content: 'lorem ipsum' });
-
const newPost = await Post.findById(post._id).populate('owner').exec();
const obj = newPost.toObject();
@@ -792,6 +811,48 @@ describe('document', function() {
assert.strictEqual(myModel.toObject().foo, void 0);
});
+
+ it('does not minimize single nested subdocs if they are required (gh-14058) (gh-11247)', async function() {
+ const nestedSchema = Schema({ bar: String }, { _id: false });
+ const schema = Schema({ foo: { type: nestedSchema, required: true } });
+
+ const MyModel = db.model('Test', schema);
+
+ const myModel = await MyModel.create({ foo: {} });
+
+ assert.deepStrictEqual(myModel.toObject().foo, {});
+ });
+
+ it('should propagate toObject to implicitly created schemas (gh-13599) (gh-13325)', async function() {
+ const transformCalls = [];
+ const userSchema = Schema({
+ firstName: String,
+ company: {
+ type: {
+ companyId: { type: Schema.Types.ObjectId },
+ companyName: String
+ }
+ }
+ }, {
+ toObject: {
+ virtuals: true,
+ transform(doc, ret) {
+ transformCalls.push(doc);
+ return ret;
+ }
+ }
+ });
+
+ userSchema.virtual('company.details').get(() => 42);
+
+ const User = db.model('User', userSchema);
+ const user = new User({ firstName: 'test', company: { companyName: 'foo' } });
+ assert.equal(transformCalls.length, 0);
+ const obj = user.toObject();
+ assert.strictEqual(obj.company.details, 42);
+ assert.equal(transformCalls.length, 1);
+ assert.strictEqual(transformCalls[0], user);
+ });
});
describe('toJSON', function() {
@@ -817,6 +878,7 @@ describe('document', function() {
};
doc.schema.options.toJSON = { virtuals: true };
+ delete doc.schema._defaultToObjectOptionsMap;
let clone = doc.toJSON();
assert.equal(clone.test, 'test');
assert.ok(clone.oids instanceof Array);
@@ -830,6 +892,7 @@ describe('document', function() {
delete path.casterConstructor.prototype.toJSON;
doc.schema.options.toJSON = { minimize: false };
+ delete doc.schema._defaultToObjectOptionsMap;
clone = doc.toJSON();
assert.equal(clone.nested2.constructor.name, 'Object');
assert.equal(Object.keys(clone.nested2).length, 1);
@@ -865,6 +928,7 @@ describe('document', function() {
ret._id = ret._id.toString();
};
+ delete doc.schema._defaultToObjectOptionsMap;
clone = doc.toJSON();
assert.equal(clone._id, doc.id);
assert.ok(undefined === clone.em);
@@ -884,6 +948,7 @@ describe('document', function() {
return { myid: ret._id.toString() };
};
+ delete doc.schema._defaultToObjectOptionsMap;
clone = doc.toJSON();
assert.deepEqual(out, clone);
@@ -921,6 +986,7 @@ describe('document', function() {
// all done
delete doc.schema.options.toJSON;
+ delete doc.schema._defaultToObjectOptionsMap;
});
it('jsonifying an object', function() {
@@ -931,7 +997,7 @@ describe('document', function() {
// parse again
const obj = JSON.parse(json);
- assert.equal(obj.test, 'woot');
+ assert.equal(obj.test, 'woot', JSON.stringify(obj));
assert.equal(obj._id, oidString);
});
@@ -978,6 +1044,39 @@ describe('document', function() {
assert.equal(foundAlicJson.friends, undefined);
assert.equal(foundAlicJson.name, 'Alic');
});
+ it('should propagate toJSON to implicitly created schemas (gh-13599) (gh-13325)', async function() {
+ const transformCalls = [];
+ const userSchema = Schema({
+ firstName: String,
+ company: {
+ type: {
+ companyId: { type: Schema.Types.ObjectId },
+ companyName: String
+ }
+ }
+ }, {
+ id: false,
+ toJSON: {
+ virtuals: true,
+ transform(doc, ret) {
+ transformCalls.push(doc);
+ return ret;
+ }
+ }
+ });
+
+ userSchema.virtual('company.details').get(() => 'foo');
+
+ const User = db.model('User', userSchema);
+ const doc = new User({
+ firstName: 'test',
+ company: { companyName: 'Acme Inc' }
+ });
+ const obj = doc.toJSON();
+ assert.strictEqual(obj.company.details, 'foo');
+ assert.equal(transformCalls.length, 1);
+ assert.strictEqual(transformCalls[0], doc);
+ });
});
describe('inspect', function() {
@@ -1365,29 +1464,36 @@ describe('document', function() {
});
describe('works on arrays', function() {
- it('with required', function(done) {
+ it('with required', async function() {
const schema = new Schema({
name: String,
arr: { type: [], required: true }
});
const M = db.model('Test', schema);
const m = new M({ name: 'gh1109-1', arr: null });
- m.save(function(err) {
- assert.ok(/Path `arr` is required/.test(err));
- m.arr = null;
- m.save(function(err) {
- assert.ok(/Path `arr` is required/.test(err));
- m.arr = [];
- m.arr.push('works');
- m.save(function(err) {
- assert.ifError(err);
- done();
- });
- });
- });
+ try {
+ await m.save();
+
+ assert.ok(false);
+ } catch (error) {
+ assert.ok(/Path `arr` is required/.test(error));
+ }
+
+ m.arr = null;
+ try {
+ await m.save();
+
+ assert.ok(false);
+ } catch (error) {
+ assert.ok(/Path `arr` is required/.test(error));
+ }
+
+ m.arr = [];
+ m.arr.push('works');
+ await m.save();
});
- it('with custom validator', function(done) {
+ it('with custom validator', async function() {
let called = false;
function validator(val) {
@@ -1404,20 +1510,20 @@ describe('document', function() {
const M = db.model('Test', schema);
const m = new M({ name: 'gh1109-2', arr: [1] });
assert.equal(called, false);
- m.save(function(err) {
+ try {
+ await m.save();
+ throw new Error('Should not have succeeded');
+ } catch (err) {
assert.equal(String(err), 'ValidationError: arr: BAM');
assert.equal(called, true);
m.arr.push(2);
called = false;
- m.save(function(err) {
- assert.equal(called, true);
- assert.ifError(err);
- done();
- });
- });
+ await m.save();
+ assert.equal(called, true);
+ }
});
- it('with both required + custom validator', function(done) {
+ it('with both required + custom validator', async function() {
function validator(val) {
return val && val.length > 1;
}
@@ -1430,30 +1536,35 @@ describe('document', function() {
const M = db.model('Test', schema);
const m = new M({ name: 'gh1109-3', arr: null });
- m.save(function(err) {
+ try {
+ await m.save();
+ throw new Error('Should not get here');
+ } catch (err) {
assert.equal(err.errors.arr.message, 'Path `arr` is required.');
- m.arr = [{ nice: true }];
- m.save(function(err) {
- assert.equal(String(err), 'ValidationError: arr: BAM');
- m.arr.push(95);
- m.save(function(err) {
- assert.ifError(err);
- done();
- });
- });
- });
+ }
+
+ m.arr = [{ nice: true }];
+ try {
+ await m.save();
+ throw new Error('Should not get here');
+ } catch (err) {
+ assert.equal(String(err), 'ValidationError: arr: BAM');
+ }
+
+ m.arr.push(95);
+ await m.save();
});
});
- it('validator should run only once gh-1743', function(done) {
+ it('validator should run only once gh-1743', async function() {
let count = 0;
const Control = new Schema({
test: {
type: String,
- validate: function(value, done) {
+ validate: function() {
count++;
- return done(true);
+ return true;
}
}
});
@@ -1469,10 +1580,9 @@ describe('document', function() {
}]
});
- post.save(function() {
- assert.equal(count, 1);
- done();
- });
+ await post.save();
+
+ assert.equal(count, 1);
});
it('validator should run only once per sub-doc gh-1743', async function() {
@@ -1509,7 +1619,7 @@ describe('document', function() {
});
});
- it('#invalidate', function(done) {
+ it('#invalidate', async function() {
let InvalidateSchema = null;
let Post = null;
let post = null;
@@ -1524,7 +1634,10 @@ describe('document', function() {
'val', 'custom error');
assert.ok(_err instanceof ValidationError);
- post.save(function(err) {
+ try {
+ await post.save();
+ assert.ok(false);
+ } catch (err) {
assert.ok(err instanceof MongooseError);
assert.ok(err instanceof ValidationError);
assert.ok(err.errors.baz instanceof ValidatorError);
@@ -1532,12 +1645,9 @@ describe('document', function() {
assert.equal(err.errors.baz.path, 'baz');
assert.equal(err.errors.baz.value, 'val');
assert.equal(err.errors.baz.kind, 'custom error');
+ }
- post.save(function(err) {
- assert.strictEqual(err, null);
- done();
- });
- });
+ await post.save();
});
describe('#equals', function() {
@@ -1608,17 +1718,14 @@ describe('document', function() {
});
});
- it('works with undefined (gh-1892)', function(done) {
+ it('works with undefined (gh-1892)', async function() {
const d = new TestDocument();
d.nested.setr = undefined;
assert.equal(d.nested.setr, 'undefined setter');
dateSetterCalled = false;
d.date = undefined;
- d.validate(function(err) {
- assert.ifError(err);
- assert.ok(dateSetterCalled);
- done();
- });
+ await d.validate();
+ assert.ok(dateSetterCalled);
});
it('passes priorVal (gh-8629)', function() {
@@ -1879,7 +1986,7 @@ describe('document', function() {
const Person = db.model('Person', personSchema);
const createdPerson = await Person.create({ name: 'Hafez' });
- const removedPerson = await Person.findOneAndRemove({ _id: createdPerson._id });
+ const removedPerson = await Person.findOneAndDelete({ _id: createdPerson._id });
removedPerson.isNew = true;
@@ -1901,8 +2008,7 @@ describe('document', function() {
let threw = false;
try {
await createdPerson.save();
- }
- catch (err) {
+ } catch (err) {
threw = true;
assert.equal(err.code, 11000);
}
@@ -1920,7 +2026,7 @@ describe('document', function() {
const err = await person.save().then(() => null, err => err);
assert.equal(err instanceof DocumentNotFoundError, true);
- assert.equal(err.message, `No document found for query "{ _id: new ObjectId("${person._id}") }" on model "Person"`);
+ assert.equal(err.message, `No document found for query "{ _id: new ObjectId('${person._id}') }" on model "Person"`);
});
it('saving a document when version bump required, throws a VersionError when document is not found (gh-10974)', async function() {
@@ -1955,31 +2061,12 @@ describe('document', function() {
}
catch (err) {
assert.equal(err instanceof DocumentNotFoundError, true);
- assert.equal(err.message, `No document found for query "{ _id: new ObjectId("${person._id}") }" on model "Person"`);
+ assert.equal(err.message, `No document found for query "{ _id: new ObjectId('${person._id}') }" on model "Person"`);
threw = true;
}
assert.equal(threw, true);
});
-
- it('passes save custom options to Model.exists(...) when no changes are present (gh-8739)', async function() {
- const personSchema = new Schema({ name: String });
-
- let optionInMiddleware;
-
- personSchema.pre('findOne', function(next) {
- optionInMiddleware = this.getOptions().customOption;
-
- return next();
- });
-
- const Person = db.model('Person', personSchema);
-
- const person = await Person.create({ name: 'Hafez' });
- await person.save({ customOption: 'test' });
-
- assert.equal(optionInMiddleware, 'test');
- });
});
it('properly calls queue functions (gh-2856)', function() {
@@ -2137,7 +2224,7 @@ describe('document', function() {
assert.equal(doc2.data.email, 'some@example.com');
});
- it('doesnt attempt to cast generic objects as strings (gh-3030)', function(done) {
+ it('doesnt attempt to cast generic objects as strings (gh-3030)', async function() {
const M = db.model('Test', {
myStr: {
type: String
@@ -2146,13 +2233,10 @@ describe('document', function() {
const t = new M({ myStr: { thisIs: 'anObject' } });
assert.ok(!t.myStr);
- t.validate(function(error) {
- assert.ok(error);
- done();
- });
+ await assert.rejects(t.validate());
});
- it('single embedded schemas 1 (gh-2689)', function(done) {
+ it('single embedded schemas 1 (gh-2689)', async function() {
const userSchema = new mongoose.Schema({
name: String,
email: String
@@ -2178,23 +2262,16 @@ describe('document', function() {
const Event = db.model('Event', eventSchema);
const e = new Event({ name: 'test', user: { name: 123, email: 'val' } });
- e.save(function(error) {
- assert.ifError(error);
- assert.strictEqual(e.user.name, '123');
- assert.equal(eventHookCount, 1);
- assert.equal(userHookCount, 1);
-
- Event.findOne({ user: { name: '123', email: 'val' } }, function(err, doc) {
- assert.ifError(err);
- assert.ok(doc);
-
- Event.findOne({ user: { $in: [{ name: '123', email: 'val' }] } }, function(err, doc) {
- assert.ifError(err);
- assert.ok(doc);
- done();
- });
- });
- });
+ await e.save();
+ assert.strictEqual(e.user.name, '123');
+ assert.equal(eventHookCount, 1);
+ assert.equal(userHookCount, 1);
+
+ const doc = await Event.findOne({ user: { name: '123', email: 'val' } });
+ assert.ok(doc);
+
+ const doc2 = await Event.findOne({ user: { $in: [{ name: '123', email: 'val' }] } });
+ assert.ok(doc2);
});
it('single embedded schemas with validation (gh-2689)', function() {
@@ -2446,6 +2523,20 @@ describe('document', function() {
await doc.save();
});
+ it('should use schema-level validateModifiedOnly option if not in options', async function() {
+ const testSchema = new Schema({ title: { type: String, required: true }, other: String }, { validateModifiedOnly: true });
+
+ const Test = db.model('Test', testSchema);
+
+ const docs = await Test.create([{ }], { validateBeforeSave: false });
+
+ const doc = docs[0];
+ doc.other = 'hello world';
+ assert.equal(doc.validateSync(), undefined);
+ const error = await doc.save().then(() => null, err => err);
+ assert.equal(error, null);
+ });
+
it('handles non-errors', async function() {
const schema = new Schema({
name: { type: String, required: true }
@@ -2464,6 +2555,88 @@ describe('document', function() {
// Does not throw
await Model.create({ name: 'test' });
});
+
+ it('fully validates modified subdocs (gh-14677)', async function() {
+ const embedSchema = new mongoose.Schema({
+ field1: {
+ type: String,
+ required: true
+ },
+ field2: String
+ });
+ const testSchema = new mongoose.Schema({
+ testField: {
+ type: String,
+ required: true
+ },
+ testArray: [embedSchema]
+ });
+ const TestModel = db.model('Test', testSchema);
+
+ let doc = new TestModel({ testArray: [{ field2: 'test' }] });
+ let err = await doc.validate({ validateModifiedOnly: true }).then(() => null, err => err);
+ assert.ok(err);
+ assert.ok(err.errors['testArray.0.field1']);
+ assert.equal(err.errors['testArray.0.field1'].kind, 'required');
+
+ await TestModel.collection.insertOne(doc.toObject());
+ doc = await TestModel.findById(doc._id).orFail();
+ doc.testArray[0].field2 = 'test modified';
+ err = await doc.validate({ validateModifiedOnly: true }).then(() => null, err => err);
+ assert.ifError(err);
+
+ err = await doc.validate().then(() => null, err => err);
+ assert.ok(err);
+ assert.ok(err.errors['testArray.0.field1']);
+ assert.equal(err.errors['testArray.0.field1'].kind, 'required');
+
+ doc.testArray[0] = { field2: 'test modified 3' };
+ err = await doc.validate({ validateModifiedOnly: true }).then(() => null, err => err);
+ assert.ok(err);
+ assert.ok(err.errors['testArray.0.field1']);
+ assert.equal(err.errors['testArray.0.field1'].kind, 'required');
+ });
+
+ it('fully validates modified single nested subdocs (gh-14677)', async function() {
+ const embedSchema = new mongoose.Schema({
+ field1: {
+ type: String,
+ required: true
+ },
+ field2: String
+ });
+ const testSchema = new mongoose.Schema({
+ testField: {
+ type: String,
+ required: true
+ },
+ subdoc: embedSchema
+ });
+ const TestModel = db.model('Test', testSchema);
+
+ let doc = new TestModel({ subdoc: { field2: 'test' } });
+ let err = await doc.validate({ validateModifiedOnly: true }).then(() => null, err => err);
+ assert.ok(err);
+ assert.ok(err.errors['subdoc.field1']);
+ assert.equal(err.errors['subdoc.field1'].kind, 'required');
+
+ await TestModel.collection.insertOne(doc.toObject());
+ doc = await TestModel.findById(doc._id).orFail();
+ doc.subdoc.field2 = 'test modified';
+ err = await doc.validate({ validateModifiedOnly: true }).then(() => null, err => err);
+ assert.ifError(err);
+
+ err = await doc.validate().then(() => null, err => err);
+ assert.ok(err);
+ assert.ok(err.errors['subdoc.field1']);
+ assert.equal(err.errors['subdoc.field1'].kind, 'required');
+
+ doc.subdoc = { field2: 'test modified 3' };
+ err = await doc.validate({ validateModifiedOnly: true }).then(() => null, err => err);
+ assert.ok(err);
+ assert.ok(err.errors['subdoc.field1']);
+ assert.equal(err.errors['subdoc.field1'].kind, 'required');
+ });
});
describe('bug fixes', function() {
@@ -2515,7 +2688,7 @@ describe('document', function() {
assert.equal(gnr.leadSinger.firstName(), 'Axl');
});
- it('single embedded schemas with models (gh-3535)', function(done) {
+ it('single embedded schemas with models (gh-3535)', async function() {
const personSchema = new Schema({ name: String });
const Person = db.model('Person', personSchema);
@@ -2525,11 +2698,8 @@ describe('document', function() {
const axl = new Person({ name: 'Axl Rose' });
const gnr = new Band({ leadSinger: axl });
- gnr.save(function(error) {
- assert.ifError(error);
- assert.equal(gnr.leadSinger.name, 'Axl Rose');
- done();
- });
+ await gnr.save();
+ assert.equal(gnr.leadSinger.name, 'Axl Rose');
});
it('single embedded schemas with indexes (gh-3594)', function() {
@@ -2668,18 +2838,22 @@ describe('document', function() {
assert.equal(preCalls, 1);
});
- it('nested single embedded doc validation (gh-3702)', function(done) {
+ it('nested single embedded doc validation (gh-3702)', async() => {
const childChildSchema = new Schema({ count: { type: Number, min: 1 } });
const childSchema = new Schema({ child: childChildSchema });
const parentSchema = new Schema({ child: childSchema });
const Parent = db.model('Parent', parentSchema);
const obj = { child: { child: { count: 0 } } };
- Parent.create(obj, function(error) {
+
+ try {
+ await Parent.create(obj);
+
+ assert.ok(false);
+ } catch (error) {
assert.ok(error);
assert.ok(/ValidationError/.test(error.toString()));
- done();
- });
+ }
});
it('handles virtuals with dots correctly (gh-3618)', function() {
@@ -2916,6 +3090,51 @@ describe('document', function() {
assert.strictEqual(nestedModel.isNew, false);
});
+ it('manual populattion with ref function (gh-15138)', async function() {
+ const userSchema = new mongoose.Schema({
+ username: { type: String },
+ phone: { type: mongoose.Schema.Types.ObjectId, ref: 'phones' }
+ });
+
+ const userSchemaWithFunc = new mongoose.Schema({
+ username: { type: String },
+ phone: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: function() {
+ return 'phones';
+ }
+ }
+ });
+
+ const phoneSchema = new mongoose.Schema({
+ phoneNumber: { type: String },
+ extension: { type: String }
+ });
+
+ const User = db.model('users', userSchema);
+ const UserWithFunc = db.model('usersWithFunc', userSchemaWithFunc);
+ const Phone = db.model('phones', phoneSchema);
+
+ const phone = await Phone.create({
+ phoneNumber: '123456789',
+ extension: '123'
+ });
+
+ const user = await User.create({
+ username: 'John Doe',
+ phone
+ });
+
+ const userWithFunc = await UserWithFunc.create({
+ username: 'John Doe',
+ phone
+ });
+
+ assert.ok(user.populated('phone'));
+ assert.ok(userWithFunc.populated('phone'));
+ assert.equal(userWithFunc.phone.extension, '123');
+ });
+
it('manual population with refPath (gh-7070)', async function() {
const ChildModelSchema = new mongoose.Schema({
name: String
@@ -3024,6 +3243,29 @@ describe('document', function() {
assert.ok(!('names' in doc));
});
+ it('can set array default to null (gh-14717)', async function() {
+ const schema = new Schema({
+ names: {
+ type: [String],
+ default: null
+ },
+ tags: {
+ type: [{ tag: String }],
+ default: null
+ }
+ });
+
+ const Model = db.model('Test', schema);
+ const m = new Model();
+ assert.strictEqual(m.names, null);
+ assert.strictEqual(m.tags, null);
+ await m.save();
+
+ const doc = await Model.collection.findOne({ _id: m._id });
+ assert.strictEqual(doc.names, null);
+ assert.strictEqual(doc.tags, null);
+ });
+
it('validation works when setting array index (gh-3816)', async function() {
const mySchema = new mongoose.Schema({
items: [
@@ -3101,13 +3343,13 @@ describe('document', function() {
assert.equal(p.child.$parent(), p);
});
- it('removing parent doc calls remove hooks on subdocs (gh-2348) (gh-4566)', async function() {
+ it('removing parent doc calls deleteOne hooks on subdocs (gh-2348) (gh-4566)', async function() {
const ChildSchema = new Schema({
name: String
});
const called = {};
- ChildSchema.pre('remove', function(next) {
+ ChildSchema.pre('deleteOne', { document: true, query: false }, function(next) {
called[this.name] = true;
next();
});
@@ -3124,7 +3366,7 @@ describe('document', function() {
child: { name: 'Anakin' }
});
- await doc.remove();
+ await doc.deleteOne();
assert.deepEqual(called, {
Jacen: true,
@@ -3137,13 +3379,6 @@ describe('document', function() {
assert.equal(doc.child.name, 'Anakin');
});
- it('strings of length 12 are valid oids (gh-3365)', async function() {
- const schema = new Schema({ myId: mongoose.Schema.Types.ObjectId });
- const M = db.model('Test', schema);
- const doc = new M({ myId: 'blablablabla' });
- await doc.validate();
- });
-
it('set() empty obj unmodifies subpaths (gh-4182)', async function() {
const omeletteSchema = new Schema({
topping: {
@@ -3166,105 +3401,30 @@ describe('document', function() {
assert.strictEqual(doc.topping.meat, void 0);
});
- it('emits cb errors on model for save (gh-3499)', function(done) {
- const testSchema = new Schema({ name: String });
+ it('clears subpaths when removing single nested (gh-4216)', async function() {
+ const RecurrenceSchema = new Schema({
+ frequency: Number,
+ interval: {
+ type: String,
+ enum: ['days', 'weeks', 'months', 'years']
+ }
+ }, { _id: false });
- const Test = db.model('Test', testSchema);
- Test.on('error', function(error) {
- assert.equal(error.message, 'fail!');
- done();
+ const EventSchema = new Schema({
+ name: {
+ type: String,
+ trim: true
+ },
+ recurrence: RecurrenceSchema
});
- new Test({}).save(function() {
- throw new Error('fail!');
- });
- });
-
- it('emits cb errors on model for save with hooks (gh-3499)', function(done) {
- const testSchema = new Schema({ name: String });
-
- testSchema.pre('save', function(next) {
- next();
- });
-
- testSchema.post('save', function(doc, next) {
- next();
- });
-
- const Test = db.model('Test', testSchema);
-
- Test.on('error', function(error) {
- assert.equal(error.message, 'fail!');
- done();
- });
-
- new Test({}).save(function() {
- throw new Error('fail!');
- });
- });
-
- it('emits cb errors on model for find() (gh-3499)', function(done) {
- const testSchema = new Schema({ name: String });
-
- const Test = db.model('Test', testSchema);
-
- Test.on('error', function(error) {
- assert.equal(error.message, 'fail!');
- done();
- });
-
- Test.find({}, function() {
- throw new Error('fail!');
- });
- });
-
- it('emits cb errors on model for find() + hooks (gh-3499)', function(done) {
- const testSchema = new Schema({ name: String });
-
- testSchema.post('find', function(results, next) {
- assert.equal(results.length, 0);
- next();
- });
-
- const Test = db.model('Test', testSchema);
-
- Test.on('error', function(error) {
- assert.equal(error.message, 'fail!');
- done();
- });
-
- Test.find({}, function() {
- throw new Error('fail!');
- });
- });
-
- it('clears subpaths when removing single nested (gh-4216)', function(done) {
- const RecurrenceSchema = new Schema({
- frequency: Number,
- interval: {
- type: String,
- enum: ['days', 'weeks', 'months', 'years']
- }
- }, { _id: false });
-
- const EventSchema = new Schema({
- name: {
- type: String,
- trim: true
- },
- recurrence: RecurrenceSchema
- });
-
- const Event = db.model('Test', EventSchema);
- const ev = new Event({
- name: 'test',
- recurrence: { frequency: 2, interval: 'days' }
- });
- ev.recurrence = null;
- ev.save(function(error) {
- assert.ifError(error);
- done();
+ const Event = db.model('Test', EventSchema);
+ const ev = new Event({
+ name: 'test',
+ recurrence: { frequency: 2, interval: 'days' }
});
+ ev.recurrence = null;
+ await ev.save();
});
it('setting path to empty object works (gh-4218)', async function() {
@@ -3274,7 +3434,7 @@ describe('document', function() {
field1: { type: Number, default: 1 }
}
}
- });
+ }, { minimize: false });
const MyModel = db.model('Test', schema);
@@ -3293,7 +3453,7 @@ describe('document', function() {
field1: { type: Number, default: 1 }
}
}
- });
+ }, { minimize: false });
const MyModel = db.model('Test', schema);
@@ -3452,7 +3612,7 @@ describe('document', function() {
});
});
- it('single nested isNew (gh-4369)', function(done) {
+ it('single nested isNew (gh-4369)', async function() {
const childSchema = new Schema({
name: String
});
@@ -3461,20 +3621,18 @@ describe('document', function() {
});
const Parent = db.model('Test', parentSchema);
- let remaining = 2;
+ let called = 0;
const doc = new Parent({ child: { name: 'Jacen' } });
doc.child.on('isNew', function(val) {
assert.ok(!val);
assert.ok(!doc.child.isNew);
- --remaining || done();
+ ++called;
});
- doc.save(function(error, doc) {
- assert.ifError(error);
- assert.ok(!doc.child.isNew);
- --remaining || done();
- });
+ const savedDoc = await doc.save();
+ assert.ok(!savedDoc.child.isNew);
+ assert.equal(called, 1);
});
it('deep default array values (gh-4540)', function() {
@@ -3491,7 +3649,7 @@ describe('document', function() {
});
});
- it('default values with subdoc array (gh-4390)', function(done) {
+ it('default values with subdoc array (gh-4390)', async function() {
const childSchema = new Schema({
name: String
});
@@ -3503,30 +3661,30 @@ describe('document', function() {
const Parent = db.model('Parent', parentSchema);
- Parent.create({}, function(error, doc) {
- assert.ifError(error);
- const arr = doc.toObject().child.map(function(doc) {
- assert.ok(doc._id);
- delete doc._id;
- return doc;
- });
- assert.deepEqual(arr, [{ name: 'test' }]);
- done();
+ const doc = await Parent.create({});
+ const arr = doc.toObject().child.map(function(doc) {
+ assert.ok(doc._id);
+ delete doc._id;
+ return doc;
});
+ assert.deepEqual(arr, [{ name: 'test' }]);
});
- it('handles invalid dates (gh-4404)', function(done) {
+ it('handles invalid dates (gh-4404)', async function() {
const testSchema = new Schema({
date: Date
});
const Test = db.model('Test', testSchema);
- Test.create({ date: new Date('invalid date') }, function(error) {
+ try {
+ await Test.create({ date: new Date('invalid date') });
+
+ assert.ok(false);
+ } catch (error) {
assert.ok(error);
assert.equal(error.errors['date'].name, 'CastError');
- done();
- });
+ }
});
it('setting array subpath (gh-4472)', function() {
@@ -3680,7 +3838,7 @@ describe('document', function() {
assert.ifError(err);
});
- it('setting full path under single nested schema works (gh-4578) (gh-4528)', function(done) {
+ it('setting full path under single nested schema works (gh-4578) (gh-4528)', async function() {
const ChildSchema = new mongoose.Schema({
age: Number
});
@@ -3694,16 +3852,13 @@ describe('document', function() {
const M = db.model('Test', ParentSchema);
- M.create({ age: 45 }, function(error, doc) {
- assert.ifError(error);
- assert.ok(!doc.family.child);
- doc.set('family.child.age', 15);
- assert.ok(doc.family.child.schema);
- assert.ok(doc.isModified('family.child'));
- assert.ok(doc.isModified('family.child.age'));
- assert.equal(doc.family.child.toObject().age, 15);
- done();
- });
+ const doc = await M.create({ age: 45 });
+ assert.ok(!doc.family.child);
+ doc.set('family.child.age', 15);
+ assert.ok(doc.family.child.schema);
+ assert.ok(doc.isModified('family.child'));
+ assert.ok(doc.isModified('family.child.age'));
+ assert.equal(doc.family.child.toObject().age, 15);
});
it('setting a nested path retains nested modified paths (gh-5206)', async function() {
@@ -3731,6 +3886,11 @@ describe('document', function() {
assert.deepEqual(
kitty.modifiedPaths(),
+ ['surnames']
+ );
+
+ assert.deepEqual(
+ kitty.modifiedPaths({ includeChildren: true }),
['surnames', 'surnames.docarray']
);
});
@@ -3781,7 +3941,7 @@ describe('document', function() {
assert.equal(person.toObject().car.toHexString(), car._id.toHexString());
});
- it('single nested doc conditional required (gh-4654)', function(done) {
+ it('single nested doc conditional required (gh-4654)', async function() {
const ProfileSchema = new Schema({
firstName: String,
lastName: String
@@ -3801,13 +3961,16 @@ describe('document', function() {
});
const User = db.model('User', UserSchema);
- User.create({ email: 'test' }, function(error) {
+ try {
+ await User.create({ email: 'test' });
+
+ assert.ok(false);
+ } catch (error) {
assert.equal(error.errors['profile'].message, 'profile required');
- done();
- });
+ }
});
- it('handles setting single nested schema to equal value (gh-4676)', function(done) {
+ it('handles setting single nested schema to equal value (gh-4676)', async function() {
const companySchema = new mongoose.Schema({
_id: false,
name: String,
@@ -3822,14 +3985,11 @@ describe('document', function() {
const User = db.model('User', userSchema);
const user = new User({ company: { name: 'Test' } });
- user.save(function(error) {
- assert.ifError(error);
- user.company.description = 'test';
- assert.ok(user.isModified('company'));
- user.company = user.company;
- assert.ok(user.isModified('company'));
- done();
- });
+ await user.save();
+ user.company.description = 'test';
+ assert.ok(user.isModified('company'));
+ user.company = user.company;
+ assert.ok(user.isModified('company'));
});
it('handles setting single nested doc to null after setting (gh-4766)', function(done) {
@@ -3930,7 +4090,7 @@ describe('document', function() {
catch(done);
});
- it('embedded docs dont mark parent as invalid (gh-4681)', function(done) {
+ it('embedded docs dont mark parent as invalid (gh-4681)', async() => {
const NestedSchema = new mongoose.Schema({
nestedName: { type: String, required: true },
createdAt: { type: Date, required: true }
@@ -3942,12 +4102,15 @@ describe('document', function() {
const Root = db.model('Test', RootSchema);
const root = new Root({ rootName: 'root', nested: [{ }] });
- root.save(function(error) {
+ try {
+ await root.save();
+
+ assert.ok(false);
+ } catch (error) {
assert.ok(error);
assert.deepEqual(Object.keys(error.errors).sort(),
['nested.0.createdAt', 'nested.0.nestedName']);
- done();
- });
+ }
});
it('should depopulate the shard key when saving (gh-4658)', function(done) {
@@ -4076,7 +4239,7 @@ describe('document', function() {
assert.strictEqual(doc.createdAt, undefined);
});
- it('timestamps with nested paths (gh-5051)', function(done) {
+ it('timestamps with nested paths (gh-5051)', async function() {
const schema = new Schema({ props: {} }, {
timestamps: {
createdAt: 'props.createdAt',
@@ -4086,15 +4249,13 @@ describe('document', function() {
const M = db.model('Test', schema);
const now = Date.now();
- M.create({}, function(error, doc) {
- assert.ok(doc.props.createdAt);
- assert.ok(doc.props.createdAt instanceof Date);
- assert.ok(doc.props.createdAt.valueOf() >= now);
- assert.ok(doc.props.updatedAt);
- assert.ok(doc.props.updatedAt instanceof Date);
- assert.ok(doc.props.updatedAt.valueOf() >= now);
- done();
- });
+ const doc = await M.create({});
+ assert.ok(doc.props.createdAt);
+ assert.ok(doc.props.createdAt instanceof Date);
+ assert.ok(doc.props.createdAt.valueOf() >= now);
+ assert.ok(doc.props.updatedAt);
+ assert.ok(doc.props.updatedAt instanceof Date);
+ assert.ok(doc.props.updatedAt.valueOf() >= now);
});
it('Declaring defaults in your schema with timestamps defined (gh-6024)', function() {
@@ -4113,8 +4274,6 @@ describe('document', function() {
});
it('supports $where in pre save hook (gh-4004)', function(done) {
- const Promise = global.Promise;
-
const schema = new Schema({
name: String
}, { timestamps: true, versionKey: null });
@@ -4503,7 +4662,7 @@ describe('document', function() {
assert.equal(nick.vehicle.name, 'Eleanor');
});
- it('handles errors in sync validators (gh-2185)', function(done) {
+ it('handles errors in sync validators (gh-2185)', async function() {
const schema = new Schema({
name: {
type: String,
@@ -4519,14 +4678,16 @@ describe('document', function() {
assert.ok(error);
assert.equal(error.errors['name'].reason.message, 'woops!');
- new M({ name: 'test' }).validate(function(error) {
+ try {
+ await new M({ name: 'test' }).validate();
+ assert.ok(false);
+ } catch (error) {
assert.ok(error);
assert.equal(error.errors['name'].reason.message, 'woops!');
- done();
- });
+ }
});
- it('allows hook as a schema key (gh-5047)', function(done) {
+ it('allows hook as a schema key (gh-5047)', async function() {
const schema = new mongoose.Schema({
name: String,
hook: { type: String }
@@ -4534,10 +4695,7 @@ describe('document', function() {
const Model = db.model('Test', schema);
- Model.create({ hook: 'test ' }, function(error) {
- assert.ifError(error);
- done();
- });
+ await Model.create({ hook: 'test ' });
});
it('save errors with callback and promise work (gh-5216)', function(done) {
@@ -4563,7 +4721,7 @@ describe('document', function() {
});
});
- it('post hooks on child subdocs run after save (gh-5085)', function(done) {
+ it('post hooks on child subdocs run after save (gh-5085)', async function() {
const ChildModelSchema = new mongoose.Schema({
text: {
type: String
@@ -4578,15 +4736,10 @@ describe('document', function() {
const Model = db.model('Parent', ParentModelSchema);
- Model.create({ children: [{ text: 'test' }] }, function(error) {
- assert.ifError(error);
- Model.findOne({}, function(error, doc) {
- assert.ifError(error);
- assert.equal(doc.children.length, 1);
- assert.equal(doc.children[0].text, 'test');
- done();
- });
- });
+ await Model.create({ children: [{ text: 'test' }] });
+ const doc = await Model.findOne({});
+ assert.equal(doc.children.length, 1);
+ assert.equal(doc.children[0].text, 'test');
});
it('post hooks on array child subdocs run after save (gh-5085) (gh-6926)', function() {
@@ -4752,7 +4905,7 @@ describe('document', function() {
assert.ok(!doc.child.nested.childPath);
});
- it('JSON.stringify nested errors (gh-5208)', function(done) {
+ it('JSON.stringify nested errors (gh-5208)', async function() {
const AdditionalContactSchema = new Schema({
contactName: {
type: String,
@@ -4799,17 +4952,15 @@ describe('document', function() {
]
}
});
- contact.validate(function(error) {
- assert.ok(error);
- assert.ok(error.errors['contact.additionalContacts.0.contactValue']);
+ const error = await contact.validate().then(() => null, err => err);
+ assert.ok(error.errors['contact.additionalContacts.0.contactValue']);
+
+ // This `JSON.stringify()` should not throw
+ assert.ok(JSON.stringify(error).indexOf('contactValue') !== -1);
- // This `JSON.stringify()` should not throw
- assert.ok(JSON.stringify(error).indexOf('contactValue') !== -1);
- done();
- });
});
- it('handles errors in subdoc pre validate (gh-5215)', function(done) {
+ it('handles errors in subdoc pre validate (gh-5215)', async function() {
const childSchema = new mongoose.Schema({});
childSchema.pre('validate', function(next) {
@@ -4822,15 +4973,13 @@ describe('document', function() {
const Parent = db.model('Parent', parentSchema);
- Parent.create({ child: {} }, function(error) {
- assert.ok(error);
- assert.ok(error.errors['child']);
- assert.equal(error.errors['child'].message, 'child pre validate');
- done();
- });
+ const error = await Parent.create({ child: {} }).catch(error => error);
+ assert.ok(error);
+ assert.ok(error.errors['child']);
+ assert.equal(error.errors['child'].message, 'child pre validate');
});
- it('custom error types (gh-4009)', function(done) {
+ it('custom error types (gh-4009)', async function() {
const CustomError = function() {};
const testSchema = new mongoose.Schema({
@@ -4845,21 +4994,27 @@ describe('document', function() {
const Test = db.model('Test', testSchema);
- Test.create({}, function(error) {
+ try {
+ await Test.create({});
+ assert.ok(false);
+ } catch (error) {
assert.ok(error);
assert.ok(error.errors['num']);
assert.ok(error.errors['num'] instanceof CustomError);
- Test.create({ num: 1 }, function(error) {
- assert.ok(error);
- assert.ok(error.errors['num']);
- assert.ok(error.errors['num'].constructor.name, 'ValidatorError');
- assert.ok(!(error.errors['num'] instanceof CustomError));
- done();
- });
- });
+ }
+
+ try {
+ await Test.create({ num: 1 });
+ assert.ok(false);
+ } catch (error) {
+ assert.ok(error);
+ assert.ok(error.errors['num']);
+ assert.ok(error.errors['num'].constructor.name, 'ValidatorError');
+ assert.ok(!(error.errors['num'] instanceof CustomError));
+ }
});
- it('saving a doc with nested string array (gh-5282)', function(done) {
+ it('saving a doc with nested string array (gh-5282)', async function() {
const testSchema = new mongoose.Schema({
strs: [[String]]
});
@@ -4870,11 +5025,8 @@ describe('document', function() {
strs: [['a', 'b']]
});
- t.save(function(error, t) {
- assert.ifError(error);
- assert.deepEqual(t.toObject().strs, [['a', 'b']]);
- done();
- });
+ await t.save();
+ assert.deepEqual(t.toObject().strs, [['a', 'b']]);
});
it('push() onto a nested doc array (gh-6398)', async function() {
@@ -4930,16 +5082,14 @@ describe('document', function() {
assert.equal(doc.array[0][0][1].key, 'lucky');
});
- it('null _id (gh-5236)', function(done) {
+ it('null _id (gh-5236)', async function() {
const childSchema = new mongoose.Schema({});
const M = db.model('Test', childSchema);
const m = new M({ _id: null });
- m.save(function(error, doc) {
- assert.equal(doc._id, null);
- done();
- });
+ const doc = await m.save();
+ assert.equal(doc._id, null);
});
it('setting populated path with typeKey (gh-5313)', function() {
@@ -4969,7 +5119,7 @@ describe('document', function() {
assert.equal(person.books[1].title, '1984');
});
- it('save twice with write concern (gh-5294)', function(done) {
+ it('save twice with write concern (gh-5294)', async function() {
const schema = new mongoose.Schema({
name: String
}, {
@@ -4979,17 +5129,12 @@ describe('document', function() {
const M = db.model('Test', schema);
- M.create({ name: 'Test' }, function(error, doc) {
- assert.ifError(error);
- doc.name = 'test2';
- doc.save(function(error) {
- assert.ifError(error);
- done();
- });
- });
+ const doc = await M.create({ name: 'Test' });
+ doc.name = 'test2';
+ await doc.save();
});
- it('undefined field with conditional required (gh-5296)', function(done) {
+ it('undefined field with conditional required (gh-5296)', async function() {
const schema = Schema({
name: {
type: String,
@@ -5002,10 +5147,7 @@ describe('document', function() {
const Model = db.model('Test', schema);
- Model.create({ name: undefined }, function(error) {
- assert.ifError(error);
- done();
- });
+ await Model.create({ name: undefined });
});
it('dotted virtuals in toObject (gh-5473)', function() {
@@ -5056,7 +5198,7 @@ describe('document', function() {
}).
then(function(doc) {
doc.child = {};
- return doc.save();
+ return Parent.updateOne({ _id: doc._id }, { $set: { child: {} } }, { minimize: false });
}).
then(function() {
return Parent.findOne();
@@ -5092,7 +5234,7 @@ describe('document', function() {
assert.ok(ownPropertyNames.indexOf('last') !== -1, ownPropertyNames.join(','));
});
- it('modifying array with existing ids (gh-5523)', function(done) {
+ it('modifying array with existing ids (gh-5523)', async function() {
const friendSchema = new mongoose.Schema(
{
_id: String,
@@ -5132,16 +5274,13 @@ describe('document', function() {
name: 'Val'
});
- user.save(function(error) {
- assert.ifError(error);
- User.findOne({ _id: user._id }, function(error, doc) {
- assert.ifError(error);
- assert.deepEqual(doc.toObject().social.friends[0], {
- _id: 'val',
- name: 'Val'
- });
- done();
- });
+ await user.save();
+
+ const doc = await User.findOne({ _id: user._id });
+
+ assert.deepEqual(doc.toObject().social.friends[0], {
+ _id: 'val',
+ name: 'Val'
});
});
@@ -5259,7 +5398,7 @@ describe('document', function() {
catch(done);
});
- it('single nested subdoc post remove hooks (gh-5388)', function(done) {
+ it('single nested subdoc post deleteOne hooks (gh-5388)', async function() {
const contentSchema = new Schema({
blocks: [{ type: String }],
summary: { type: String }
@@ -5267,7 +5406,7 @@ describe('document', function() {
let called = 0;
- contentSchema.post('remove', function() {
+ contentSchema.post('deleteOne', { document: true, query: false }, function() {
++called;
});
@@ -5285,16 +5424,9 @@ describe('document', function() {
}
});
- note.save(function(error) {
- assert.ifError(error);
- note.remove(function(error) {
- assert.ifError(error);
- setTimeout(function() {
- assert.equal(called, 1);
- done();
- }, 50);
- });
- });
+ await note.save();
+ await note.deleteOne();
+ assert.equal(called, 1);
});
it('push populated doc onto empty array triggers manual population (gh-5504)', function() {
@@ -5333,7 +5465,7 @@ describe('document', function() {
assert.ok(referrerE.reference[0] instanceof Referrer);
});
- it('single nested conditional required scope (gh-5569)', function(done) {
+ it('single nested conditional required scope (gh-5569)', async function() {
const scopes = [];
const ThingSchema = new mongoose.Schema({
@@ -5359,16 +5491,17 @@ describe('document', function() {
let doc = new SuperDocument();
doc.thing.undefinedDisallowed = null;
- doc.save(function(error) {
- assert.ifError(error);
- doc = new SuperDocument();
- doc.thing.undefinedDisallowed = undefined;
- doc.save(function(error) {
- assert.ok(error);
- assert.ok(error.errors['thing.undefinedDisallowed']);
- done();
- });
- });
+ await doc.save();
+
+ doc = new SuperDocument();
+ doc.thing.undefinedDisallowed = undefined;
+
+ try {
+ await doc.save();
+ } catch (error) {
+ assert.ok(error);
+ assert.ok(error.errors['thing.undefinedDisallowed']);
+ }
});
it('single nested setters only get called once (gh-5601)', function() {
@@ -5451,7 +5584,7 @@ describe('document', function() {
assert.deepStrictEqual(res.name, { value: 'JOHN SMITH' });
});
- it('setting doc array to array of top-level docs works (gh-5632)', function(done) {
+ it('setting doc array to array of top-level docs works (gh-5632)', async function() {
const MainSchema = new Schema({
name: { type: String },
children: [{
@@ -5462,37 +5595,25 @@ describe('document', function() {
const Model = db.model('Test', MainSchema);
const RelatedModel = db.model('Test1', RelatedSchema);
- RelatedModel.create({ name: 'test' }, function(error, doc) {
- assert.ifError(error);
- Model.create({ name: 'test1', children: [doc] }, function(error, m) {
- assert.ifError(error);
- m.children = [doc];
- m.save(function(error) {
- assert.ifError(error);
- assert.equal(m.children.length, 1);
- assert.equal(m.children[0].name, 'test');
- done();
- });
- });
- });
+ const doc = await RelatedModel.create({ name: 'test' });
+ const m = await Model.create({ name: 'test1', children: [doc] });
+ m.children = [doc];
+ await m.save();
+ assert.equal(m.children.length, 1);
+ assert.equal(m.children[0].name, 'test');
});
- it('Using set as a schema path (gh-1939)', function(done) {
+ it('Using set as a schema path (gh-1939)', async function() {
const testSchema = new Schema({ set: String });
const Test = db.model('Test', testSchema);
const t = new Test({ set: 'test 1' });
assert.equal(t.set, 'test 1');
- t.save(function(error) {
- assert.ifError(error);
- t.set = 'test 2';
- t.save(function(error) {
- assert.ifError(error);
- assert.equal(t.set, 'test 2');
- done();
- });
- });
+ await t.save();
+ t.set = 'test 2';
+ await t.save();
+ assert.equal(t.set, 'test 2');
});
it('handles array defaults correctly (gh-5780)', function() {
@@ -5633,6 +5754,7 @@ describe('document', function() {
const Test = db.model('Test', testSchema);
const doc = new Test({ arr: [new mongoose.Types.ObjectId()] });
+ assert.equal(called, 0);
assert.deepEqual(doc.toObject({ getters: true }).arr, [42]);
assert.equal(called, 1);
});
@@ -5651,7 +5773,6 @@ describe('document', function() {
const Test = db.model('Test', testSchema);
-
let doc = await Test.create({ arr: [new mongoose.Types.ObjectId()] });
assert.equal(called, 1);
@@ -5756,7 +5877,7 @@ describe('document', function() {
});
});
- it('Single nested subdocs using discriminator can be modified (gh-5693)', function(done) {
+ it('Single nested subdocs using discriminator can be modified (gh-5693)', async() => {
const eventSchema = new Schema({ message: String }, {
discriminatorKey: 'kind',
_id: false
@@ -5782,26 +5903,21 @@ describe('document', function() {
}
});
- doc.save(function(error) {
- assert.ifError(error);
- assert.equal(doc.event.message, 'Test');
- assert.equal(doc.event.kind, 'Clicked');
- assert.equal(doc.event.element, 'Amazon Link');
-
- doc.set('event', {
- kind: 'Purchased',
- product: 'Professional AngularJS'
- });
+ await doc.save();
+ assert.equal(doc.event.message, 'Test');
+ assert.equal(doc.event.kind, 'Clicked');
+ assert.equal(doc.event.element, 'Amazon Link');
- doc.save(function(error) {
- assert.ifError(error);
- assert.equal(doc.event.kind, 'Purchased');
- assert.equal(doc.event.product, 'Professional AngularJS');
- assert.ok(!doc.event.element);
- assert.ok(!doc.event.message);
- done();
- });
+ doc.set('event', {
+ kind: 'Purchased',
+ product: 'Professional AngularJS'
});
+
+ await doc.save();
+ assert.equal(doc.event.kind, 'Purchased');
+ assert.equal(doc.event.product, 'Professional AngularJS');
+ assert.ok(!doc.event.element);
+ assert.ok(!doc.event.message);
});
it('required function only gets called once (gh-6801)', function() {
@@ -5854,7 +5970,7 @@ describe('document', function() {
await Model.create({});
});
- it('doc array: set then remove (gh-3511)', function(done) {
+ it('doc array: set then remove (gh-3511)', async function() {
const ItemChildSchema = new mongoose.Schema({
name: {
type: String,
@@ -5872,27 +5988,22 @@ describe('document', function() {
children: [{ name: 'test1' }, { name: 'test2' }]
});
- p.save(function(error) {
- assert.ifError(error);
- ItemParent.findById(p._id, function(error, doc) {
- assert.ifError(error);
- assert.ok(doc);
- assert.equal(doc.children.length, 2);
-
- doc.children[1].name = 'test3';
- doc.children.remove(doc.children[0]);
-
- doc.save(function(error) {
- assert.ifError(error);
- ItemParent.findById(doc._id, function(error, doc) {
- assert.ifError(error);
- assert.equal(doc.children.length, 1);
- assert.equal(doc.children[0].name, 'test3');
- done();
- });
- });
- });
- });
+ await p.save();
+
+ const doc = await ItemParent.findById(p._id);
+
+ assert.ok(doc);
+ assert.equal(doc.children.length, 2);
+
+ doc.children[1].name = 'test3';
+ doc.children.remove(doc.children[0]);
+
+ await doc.save();
+
+ const doc2 = await ItemParent.findById(doc._id);
+
+ assert.equal(doc2.children.length, 1);
+ assert.equal(doc2.children[0].name, 'test3');
});
it('doc array: modify then sort (gh-7556)', async function() {
@@ -6027,7 +6138,7 @@ describe('document', function() {
assert.equal(doc.media[0].position, 'right');
});
- it('consistent context for nested docs (gh-5347)', function(done) {
+ it('consistent context for nested docs (gh-5347)', async function() {
const contexts = [];
const childSchema = new mongoose.Schema({
phoneNumber: {
@@ -6049,7 +6160,7 @@ describe('document', function() {
const Parent = db.model('Parent', parentSchema);
- Parent.create({
+ const doc = await Parent.create({
name: 'test',
children: [
{
@@ -6059,19 +6170,20 @@ describe('document', function() {
}
}
]
- }, function(error, doc) {
- assert.ifError(error);
- const child = doc.children.id(doc.children[0]._id);
- child.phoneNumber = '345';
- assert.equal(contexts.length, 1);
- doc.save(function(error) {
- assert.ifError(error);
- assert.equal(contexts.length, 2);
- assert.ok(contexts[0].toObject().notifications.isEnabled);
- assert.ok(contexts[1].toObject().notifications.isEnabled);
- done();
- });
});
+
+ const child = doc.children.id(doc.children[0]._id);
+ child.phoneNumber = '345';
+
+ assert.equal(contexts.length, 1);
+
+ await doc.save();
+
+ assert.equal(contexts.length, 2);
+
+ assert.ok(contexts[0].toObject().notifications.isEnabled);
+
+ assert.ok(contexts[1].toObject().notifications.isEnabled);
});
it('accessing arrays in setters on initial document creation (gh-6155)', function() {
@@ -6270,7 +6382,7 @@ describe('document', function() {
assert.deepEqual(isNew, [true]);
});
- it('modify multiple subdoc paths (gh-4405)', function(done) {
+ it('modify multiple subdoc paths (gh-4405)', async() => {
const ChildObjectSchema = new Schema({
childProperty1: String,
childProperty2: String,
@@ -6294,27 +6406,24 @@ describe('document', function() {
childProperty3: 'c'
}
});
- p.save(function(error) {
- assert.ifError(error);
- Parent.findById(p._id, function(error, p) {
- assert.ifError(error);
- p.parentProperty1 = 'foo';
- p.parentProperty2 = 'bar';
- p.child.childProperty1 = 'ping';
- p.child.childProperty2 = 'pong';
- p.child.childProperty3 = 'weee';
- p.save(function(error) {
- assert.ifError(error);
- Parent.findById(p._id, function(error, p) {
- assert.ifError(error);
- assert.equal(p.child.childProperty1, 'ping');
- assert.equal(p.child.childProperty2, 'pong');
- assert.equal(p.child.childProperty3, 'weee');
- done();
- });
- });
- });
- });
+
+ await p.save();
+
+ const p1 = await Parent.findById(p._id);
+
+ p1.parentProperty1 = 'foo';
+ p1.parentProperty2 = 'bar';
+ p1.child.childProperty1 = 'ping';
+ p1.child.childProperty2 = 'pong';
+ p1.child.childProperty3 = 'weee';
+
+ await p1.save();
+
+ const p2 = await Parent.findById(p._id);
+
+ assert.equal(p2.child.childProperty1, 'ping');
+ assert.equal(p2.child.childProperty2, 'pong');
+ assert.equal(p2.child.childProperty3, 'weee');
});
it('doesnt try to cast populated embedded docs (gh-6390)', async function() {
@@ -6384,7 +6493,9 @@ describe('document', function() {
name: String,
folders: {
type: [{ folderId: String }],
- validate: v => assert.ok(v.length === new Set(v.map(el => el.folderId)).size, 'Duplicate')
+ validate: v => {
+ assert.ok(v.length === new Set(v.map(el => el.folderId)).size, 'Duplicate');
+ }
}
}]
}
@@ -6745,8 +6856,8 @@ describe('document', function() {
let removeCount1 = 0;
let removeCount2 = 0;
- schema.pre('remove', () => ++removeCount1);
- schema.pre('remove', { document: true, query: false }, () => ++removeCount2);
+ schema.pre('deleteOne', { document: true }, () => ++removeCount1);
+ schema.pre('deleteOne', { document: true, query: false }, () => ++removeCount2);
const Model = db.model('Test', schema);
@@ -6771,7 +6882,7 @@ describe('document', function() {
assert.equal(removeCount1, 0);
assert.equal(removeCount2, 0);
- await doc.remove();
+ await doc.deleteOne();
assert.equal(removeCount1, 1);
assert.equal(removeCount2, 1);
@@ -6873,6 +6984,40 @@ describe('document', function() {
assert.equal(mapTest.toObject({}).test.key1.name, 'value1');
});
+ it('flattenObjectIds option for toObject() (gh-13341) (gh-2790)', function() {
+ const schema = new Schema({
+ _id: 'ObjectId',
+ nested: {
+ id: 'ObjectId'
+ },
+ subdocument: new Schema({}),
+ documentArray: [new Schema({})]
+ }, { versionKey: false });
+
+ const Test = db.model('Test', schema);
+
+ const doc = new Test({
+ _id: new mongoose.Types.ObjectId('0'.repeat(24)),
+ nested: {
+ id: new mongoose.Types.ObjectId('1'.repeat(24))
+ },
+ subdocument: {
+ _id: new mongoose.Types.ObjectId('2'.repeat(24))
+ },
+ documentArray: [{ _id: new mongoose.Types.ObjectId('3'.repeat(24)) }]
+ });
+ assert.deepStrictEqual(doc.toObject({ flattenObjectIds: true }), {
+ _id: '0'.repeat(24),
+ nested: {
+ id: '1'.repeat(24)
+ },
+ subdocument: {
+ _id: '2'.repeat(24)
+ },
+ documentArray: [{ _id: '3'.repeat(24) }]
+ });
+ });
+
it('`collection` property with strict: false (gh-7276)', async function() {
const schema = new Schema({}, { strict: false, versionKey: false });
const Model = db.model('Test', schema);
@@ -7510,7 +7655,7 @@ describe('document', function() {
});
it('handles nested properties named `on` (gh-11656)', async function() {
- const schema = new mongoose.Schema({ on: String }, { supressReservedKeysWarning: true });
+ const schema = new mongoose.Schema({ on: String }, { suppressReservedKeysWarning: true });
const Model = db.model('Test', schema);
await Model.create({ on: 'test string' });
@@ -7680,7 +7825,11 @@ describe('document', function() {
schema.path('createdAt').immutable(true);
assert.ok(schema.path('createdAt').$immutable);
- assert.equal(schema.path('createdAt').setters.length, 1);
+ assert.equal(
+ schema.path('createdAt').setters.length,
+ 1,
+ schema.path('createdAt').setters.map(setter => setter.toString())
+ );
schema.path('createdAt').immutable(false);
assert.ok(!schema.path('createdAt').$immutable);
@@ -8073,6 +8222,38 @@ describe('document', function() {
await person.save();
});
+ it('set() merge option with double nested', async function() {
+ const PersonSchema = new Schema({
+ info: {
+ address: {
+ city: String,
+ country: { type: String, default: 'UK' },
+ postcode: String
+ }
+ }
+ });
+
+ const Person = db.model('Person', PersonSchema);
+
+
+ const person = new Person({
+ info: {
+ address: {
+ country: 'United States',
+ city: 'New York'
+ }
+ }
+ });
+
+ const update = { info: { address: { postcode: '12H' } } };
+
+ person.set(update, undefined, { merge: true });
+
+ assert.equal(person.info.address.city, 'New York');
+ assert.equal(person.info.address.postcode, '12H');
+ assert.equal(person.info.address.country, 'United States');
+ });
+
it('setting single nested subdoc with timestamps (gh-8251)', async function() {
const ActivitySchema = Schema({ description: String }, { timestamps: true });
const RequestSchema = Schema({ activity: ActivitySchema });
@@ -8203,7 +8384,7 @@ describe('document', function() {
assert.deepEqual(Object.keys(err.errors), ['age']);
});
- it('array push with $position (gh-4322)', async function() {
+ it('array push with $position (gh-14244) (gh-4322)', async function() {
const schema = Schema({
nums: [Number]
});
@@ -8227,12 +8408,13 @@ describe('document', function() {
$each: [0],
$position: 0
});
- assert.throws(() => {
- doc.nums.push({ $each: [5] });
- }, /Cannot call.*multiple times/);
- assert.throws(() => {
- doc.nums.push(5);
- }, /Cannot call.*multiple times/);
+ assert.deepStrictEqual(doc.nums.$__getAtomics(), [['$push', { $each: [0], $position: 0 }]]);
+
+ doc.nums.push({ $each: [5] });
+ assert.deepStrictEqual(doc.nums.$__getAtomics(), [['$set', [0, 1, 2, 3, 4, 5]]]);
+
+ doc.nums.push({ $each: [0.5], $position: 1 });
+ assert.deepStrictEqual(doc.nums.$__getAtomics(), [['$set', [0, 0.5, 1, 2, 3, 4, 5]]]);
});
it('setting a path to a single nested document should update the single nested doc parent (gh-8400)', function() {
@@ -9239,9 +9421,52 @@ describe('document', function() {
assert.ok(foo.isModified('subdoc.bar'));
});
- it('correctly tracks saved state for deeply nested objects (gh-10773) (gh-9396)', async function() {
- const PaymentSchema = Schema({ status: String }, { _id: false });
- const OrderSchema = new Schema({
+ it('does not unmark modified if there is no initial value (gh-9396)', async function() {
+ const IClientSchema = new Schema({
+ jwt: {
+ token_crypt: { type: String, template: false, maxSize: 8 * 1024 },
+ token_salt: { type: String, template: false }
+ }
+ });
+
+ const encrypt = function(doc, path, value) {
+ doc.set(path + '_crypt', value + '_crypt');
+ doc.set(path + '_salt', value + '_salt');
+ };
+
+ const decrypt = function(doc, path) {
+ return doc.get(path + '_crypt').replace('_crypt', '');
+ };
+
+ IClientSchema.virtual('jwt.token')
+ .get(function() {
+ return decrypt(this, 'jwt.token');
+ })
+ .set(function(value) {
+ encrypt(this, 'jwt.token', value);
+ });
+
+
+ const iclient = db.model('Test', IClientSchema);
+ const test = new iclient({
+ jwt: {
+ token: 'firstToken'
+ }
+ });
+
+ await test.save();
+ const entry = await iclient.findById(test._id).orFail();
+ entry.set('jwt.token', 'secondToken');
+ entry.set(entry.toJSON());
+ await entry.save();
+
+ const { jwt } = await iclient.findById(test._id).orFail();
+ assert.strictEqual(jwt.token, 'secondToken');
+ });
+
+ it('correctly tracks saved state for deeply nested objects (gh-10773) (gh-9396)', async function() {
+ const PaymentSchema = Schema({ status: String }, { _id: false });
+ const OrderSchema = new Schema({
status: String,
payments: {
payout: PaymentSchema
@@ -9750,7 +9975,7 @@ describe('document', function() {
doc.schema = 'test2';
await doc.save();
- await fromDb.remove();
+ await fromDb.deleteOne();
doc.name = 'test3';
const err = await doc.save().then(() => null, err => err);
assert.ok(err);
@@ -9860,14 +10085,14 @@ describe('document', function() {
assert.ok(doc);
});
- it('Makes sure pre remove hook is executed gh-9885', async function() {
+ it('Makes sure pre deleteOne hook is executed (gh-9885)', async function() {
const SubSchema = new Schema({
myValue: {
type: String
}
}, {});
let count = 0;
- SubSchema.pre('remove', function(next) {
+ SubSchema.pre('deleteOne', { document: true, query: false }, function(next) {
count++;
next();
});
@@ -9884,21 +10109,20 @@ describe('document', function() {
const Model = db.model('TestModel', thisSchema);
-
await Model.deleteMany({}); // remove all existing documents
const newModel = {
foo: 'bar',
mySubdoc: [{ myValue: 'some value' }]
};
const document = await Model.create(newModel);
- document.mySubdoc[0].remove();
+ document.mySubdoc[0].deleteOne();
await document.save().catch((error) => {
console.error(error);
});
assert.equal(count, 1);
});
- it('gh9880', function(done) {
+ it('gh9880', async function() {
const testSchema = new Schema({
prop: String,
nestedProp: {
@@ -9907,36 +10131,32 @@ describe('document', function() {
});
const Test = db.model('Test', testSchema);
- new Test({
+ const doc = await new Test({
prop: 'Test',
nestedProp: null
- }).save((err, doc) => {
- doc.id;
- doc.nestedProp;
+ }).save();
- // let's clone this document:
- new Test({
- prop: 'Test 2',
- nestedProp: doc.nestedProp
- });
+ doc.id;
+ doc.nestedProp;
- Test.updateOne({
- _id: doc._id
- }, {
- nestedProp: null
- }, (err) => {
- assert.ifError(err);
- Test.findOne({
- _id: doc._id
- }, (err, updatedDoc) => {
- assert.ifError(err);
- new Test({
- prop: 'Test 3',
- nestedProp: updatedDoc.nestedProp
- });
- done();
- });
- });
+ new Test({
+ prop: 'Test 2',
+ nestedProp: doc.nestedProp
+ });
+
+ await Test.updateOne({
+ _id: doc._id
+ }, {
+ nestedProp: null
+ });
+
+ const updatedDoc = await Test.findOne({
+ _id: doc._id
+ });
+
+ new Test({
+ prop: 'Test 3',
+ nestedProp: updatedDoc.nestedProp
});
});
@@ -10410,35 +10630,6 @@ describe('document', function() {
assert.equal(user.get, 12);
});
});
- describe('Document#remove', () => {
- it('is available as `$remove`', async() => {
- const userSchema = new Schema({ name: String });
- const User = db.model('User', userSchema);
-
- const user = new User({ name: 'Hafez' });
- await user.save();
- await user.$remove();
- const userFromDB = await User.findOne({ _id: user._id });
-
- assert.ok(userFromDB == null);
- });
- it('can be used as a property in documents', async() => {
- const userSchema = new Schema({
- remove: Number
- });
-
- const User = db.model('User', userSchema);
- const user = new User({ remove: 12 });
-
- assert.equal(user.remove, 12);
-
- await user.save();
- await user.$remove();
- const userFromDB = await User.findOne({ _id: user._id });
-
- assert.ok(userFromDB == null);
- });
- });
});
describe('virtuals `pathsToSkip` (gh-10120)', () => {
@@ -10691,7 +10882,10 @@ describe('document', function() {
assert.ok(!band.populated('members'));
assert.ok(!band.populated('lead'));
assert.ok(!band.populated('embeddedMembers.member'));
+ assert.ok(band.members.isMongooseArray);
+ assert.ok(band.embeddedMembers.isMongooseArray);
assert.ok(!band.embeddedMembers[0].member.name);
+ // assert.ok(!band.embeddedMembers[0].$populated('member'));
});
it('should allow dashes in the path name (gh-10677)', async function() {
@@ -12092,6 +12286,27 @@ describe('document', function() {
assert.strictEqual(clonedDoc.$session(), session);
});
+ it('$clone() with single nested and doc array (gh-14353) (gh-11849)', async function() {
+ const schema = new mongoose.Schema({
+ subdocArray: [{
+ name: String
+ }],
+ subdoc: new mongoose.Schema({ name: String })
+ });
+ const Test = db.model('Test', schema);
+
+ const item = await Test.create({ subdocArray: [{ name: 'test 1' }], subdoc: { name: 'test 2' } });
+
+ const doc = await Test.findById(item._id);
+ const clonedDoc = doc.$clone();
+
+ assert.ok(clonedDoc.subdocArray[0].$__);
+ assert.ok(clonedDoc.subdoc.$__);
+
+ assert.deepEqual(doc.subdocArray[0], clonedDoc.subdocArray[0]);
+ assert.deepEqual(doc.subdoc, clonedDoc.subdoc);
+ });
+
it('can create document with document array and top-level key named `schema` (gh-12480)', async function() {
const AuthorSchema = new Schema({
fullName: { type: 'String', required: true }
@@ -12101,7 +12316,7 @@ describe('document', function() {
schema: { type: 'String', required: true },
title: { type: 'String', required: true },
authors: [AuthorSchema]
- }, { supressReservedKeysWarning: true });
+ }, { suppressReservedKeysWarning: true });
const Book = db.model('Book', BookSchema);
@@ -12279,6 +12494,104 @@ describe('document', function() {
assert.equal(fromDb.obj.subArr[0].str, 'subArr.test1');
});
+ it('can set() from top-level on nested schema with strict: false (gh-13327)', async function() {
+ const testSchema = new Schema({
+ d: new Schema({}, { strict: false, _id: false })
+ });
+ const Test = db.model('Test', testSchema);
+
+ const x = new Test();
+ x.set('d.x.y', 1);
+ assert.strictEqual(x.d.x.y, 1);
+ assert.deepStrictEqual(x.get('d.x'), { y: 1 });
+ assert.strictEqual(x.get('d.x.y'), 1);
+ await x.save();
+
+ const fromDb = await Test.findById(x._id).lean();
+ assert.equal(fromDb.d.x.y, 1);
+ });
+
+ it('can set() from top-level on path underneath map of mixed (gh-13327)', async function() {
+ const testSchema = new Schema({
+ c: {
+ type: Map,
+ of: 'Mixed'
+ }
+ });
+ const Test = db.model('Test', testSchema);
+
+ const x = new Test();
+ x.set('c.x.y', 1);
+ assert.strictEqual(x.get('c.x.y'), 1);
+ await x.save();
+
+ const fromDb = await Test.findById(x._id).lean();
+ assert.equal(fromDb.c.x.y, 1);
+ });
+
+ it('should allow storing keys with dots in name in mixed under nested (gh-13530)', async function() {
+ const TestModelSchema = new mongoose.Schema({
+ metadata:
+ {
+ labels: mongoose.Schema.Types.Mixed
+ }
+ });
+ const TestModel = db.model('Test', TestModelSchema);
+ const { _id } = await TestModel.create({
+ metadata: {
+ labels: { 'my.label.com': 'true' }
+ }
+ });
+ const doc = await TestModel.findById(_id).lean();
+ assert.deepStrictEqual(doc.metadata, {
+ labels: {
+ 'my.label.com': 'true'
+ }
+ });
+ });
+
+ it('cleans up all array subdocs modified state on save (gh-13582)', async function() {
+ const ElementSchema = new mongoose.Schema({
+ elementName: String
+ });
+
+ const MyDocSchema = new mongoose.Schema({
+ docName: String,
+ elements: [ElementSchema]
+ });
+
+ const Test = db.model('Test', MyDocSchema);
+ let doc = new Test({ docName: 'MyDocName' });
+ doc.elements.push({ elementName: 'ElementName1' });
+ doc.elements.push({ elementName: 'ElementName2' });
+ doc = await doc.save();
+ assert.deepStrictEqual(doc.elements[0].modifiedPaths(), []);
+ assert.deepStrictEqual(doc.elements[1].modifiedPaths(), []);
+ });
+
+ it('cleans up all nested subdocs modified state on save (gh-13609)', async function() {
+ const TwoElementSchema = new mongoose.Schema({
+ elementName: String
+ });
+
+ const TwoNestedSchema = new mongoose.Schema({
+ nestedName: String,
+ elements: [TwoElementSchema]
+ });
+
+ const TwoDocDefaultSchema = new mongoose.Schema({
+ docName: String,
+ nested: { type: TwoNestedSchema, default: {} }
+ });
+
+ const Test = db.model('Test', TwoDocDefaultSchema);
+ const doc = new Test({ docName: 'MyDocName' });
+ doc.nested.nestedName = 'qdwqwd';
+ doc.nested.elements.push({ elementName: 'ElementName1' });
+ await doc.save();
+ assert.deepStrictEqual(doc.nested.modifiedPaths(), []);
+ });
+
it('avoids prototype pollution on init', async function() {
const Example = db.model('Example', new Schema({ hello: String }));
@@ -12295,10 +12608,1636 @@ describe('document', function() {
const test = {};
assert.strictEqual(test.polluted, undefined);
assert.strictEqual(Object.prototype.polluted, undefined);
+
+ const example2 = await new Example({ hello: 'world!' }).save();
+ await Example.findByIdAndUpdate(example2._id, {
+ $rename: {
+ hello: 'constructor.polluted'
+ }
+ });
+
+ await Example.find();
+ const test2 = {};
+ assert.strictEqual(test2.constructor.polluted, undefined);
+ assert.strictEqual(Object.polluted, undefined);
+ });
+
+ it('does not modify array when calling getters (gh-13748)', async function() {
+ // create simple setter to add a sufix
+ const addSufix = (name) => {
+ return name + '-sufix';
+ };
+
+ // create simple gettrer to remove last 6 letters (should be "-sufix")
+ const removeSufix = (name) => {
+ return ('' + name).slice(0, -6);
+ };
+
+ const userSchema = new mongoose.Schema(
+ {
+ name: String,
+ age: Number,
+ profession: {
+ type: String,
+ get: removeSufix,
+ set: addSufix
+ },
+ hobbies: [{ type: String, get: removeSufix, set: addSufix }]
+ },
+ {
+ toObject: { getters: true },
+ toJSON: { getters: true }
+ }
+ );
+ const User = db.model('User', userSchema);
+
+ const usr = await User.create({
+ name: 'John',
+ age: 18,
+ profession: 'teacher',
+ hobbies: ['swimming', 'football']
+ });
+
+ const oneUser = await User.findById(usr._id).orFail();
+ assert.equal(oneUser.profession, 'teacher');
+ assert.equal(oneUser.profession, 'teacher');
+ assert.equal(oneUser.profession, 'teacher');
+ assert.equal(oneUser.hobbies[0], 'swimming');
+ assert.equal(oneUser.hobbies[0], 'swimming');
+ assert.equal(oneUser.hobbies[0], 'swimming');
+
+ assert.equal(oneUser.get('hobbies.0'), 'swimming');
+ assert.equal(oneUser.get('hobbies.0'), 'swimming');
+ assert.equal(oneUser.get('hobbies.0'), 'swimming');
+ });
+
+ it('sets defaults on subdocs with subdoc projection (gh-13720)', async function() {
+ const subSchema = new mongoose.Schema({
+ propertyA: { type: String, default: 'A' },
+ propertyB: { type: String, default: 'B' }
+ });
+ const userSchema = new mongoose.Schema({
+ name: String,
+ sub: { type: subSchema, default: () => ({}) }
+ });
+ const User = db.model('User', userSchema);
+ await User.insertMany([{ name: 'user' }]);
+ await User.updateMany({}, { $unset: { 'sub.propertyA': '' } });
+ const nestedProjectionDoc = await User.findOne({}, { name: 1, 'sub.propertyA': 1, 'sub.propertyB': 1 });
+ assert.strictEqual(nestedProjectionDoc.sub.propertyA, 'A');
+ });
+
+ it('handles bigint (gh-13791)', async function() {
+ const testSchema = new mongoose.Schema({
+ n: Number,
+ reward: BigInt
+ });
+ const Test = db.model('Test', testSchema);
+
+ const a = await Test.create({ n: 1, reward: 14055648105137340n });
+ const b = await Test.findOne({ n: 1 });
+ assert.equal(a.reward, 14055648105137340n);
+ assert.equal(b.reward, 14055648105137340n);
+ });
+ it('should allow null values in list in self assignment (gh-13859)', async function() {
+ const objSchema = new Schema({
+ date: Date,
+ value: Number
+ });
+
+ const testSchema = new Schema({
+ intArray: [Number],
+ strArray: [String],
+ objArray: [objSchema]
+ });
+ const Test = db.model('Test', testSchema);
+
+ const doc = new Test({
+ intArray: [1, 2, 3, null],
+ strArray: ['b', null, 'c'],
+ objArray: [
+ { date: new Date(1000), value: 1 },
+ null,
+ { date: new Date(3000), value: 3 }
+ ]
+ });
+ await doc.save();
+ doc.intArray = doc.intArray;
+ doc.strArray = doc.strArray;
+ doc.objArray = doc.objArray; // this is the trigger for the error
+ assert.ok(doc);
+ await doc.save();
+ assert.ok(doc);
+ });
+
+ it('bulkSave() picks up changes in pre("save") middleware (gh-13799)', async() => {
+ const schema = new Schema({ name: String, _age: { type: Number, min: 0, default: 0 } });
+ schema.pre('save', function() {
+ this._age = this._age + 1;
+ });
+
+ const Person = db.model('Person', schema, 'Persons');
+ const person = new Person({ name: 'Jean-Luc Picard', _age: 59 });
+
+ await Person.bulkSave([person]);
+
+ let updatedPerson = await Person.findById(person._id);
+
+ assert.equal(updatedPerson?._age, 60);
+
+ await Person.bulkSave([updatedPerson]);
+
+ updatedPerson = await Person.findById(person._id);
+
+ assert.equal(updatedPerson?._age, 61);
+ });
+
+ it('bulkSave() allows skipping validation with validateBeforeSave (gh-15156)', async() => {
+ const schema = new Schema({ name: { type: String, required: true } });
+ const MyModel = db.model('Test', schema);
+
+ const doc = new MyModel();
+ await MyModel.bulkSave([doc], { validateBeforeSave: false });
+
+ assert.ok(await MyModel.exists({ _id: doc._id }));
+ });
+
+ it('handles default embedded discriminator values (gh-13835)', async function() {
+ const childAbstractSchema = new Schema(
+ { kind: { type: Schema.Types.String, enum: ['concreteKind'], required: true, default: 'concreteKind' } },
+ { discriminatorKey: 'kind', _id: false }
+ );
+ const childConcreteSchema = new Schema({ concreteProp: { type: Number, required: true } });
+
+ const parentSchema = new Schema(
+ {
+ child: {
+ type: childAbstractSchema,
+ required: true
+ }
+ },
+ { _id: false }
+ );
+
+ parentSchema.path('child').discriminator('concreteKind', childConcreteSchema);
+
+ const ParentModel = db.model('Test', parentSchema);
+
+ const parent = new ParentModel({ child: { concreteProp: 123 } });
+ assert.strictEqual(parent.child.concreteProp, 123);
+ assert.strictEqual(parent.get('child.concreteProp'), 123);
+ assert.strictEqual(parent.toObject().child.concreteProp, 123);
+ });
+
+ it('avoids saving changes to deselected paths (gh-13145) (gh-13062)', async function() {
+ const testSchema = new mongoose.Schema({
+ name: { type: String, required: true },
+ age: { type: Number, required: true, select: false },
+ links: { type: String, required: true, select: false }
+ });
+
+ const Test = db.model('Test', testSchema);
+
+ const { _id } = await Test.create({
+ name: 'Test Testerson',
+ age: 0,
+ links: 'some init links'
+ });
+
+ const doc = await Test.findById(_id);
+ doc.links = undefined;
+ const err = await doc.save().then(() => null, err => err);
+ assert.ok(err);
+ assert.ok(err.errors['links']);
+ assert.equal(err.errors['links'].message, 'Path `links` is required.');
+ });
+
+ it('fires pre validate hooks on 4 level single nested subdocs (gh-13876)', async function() {
+ let attachmentSchemaPreValidateCalls = 0;
+ const attachmentSchema = new Schema({ name: String });
+ attachmentSchema.pre('validate', () => { ++attachmentSchemaPreValidateCalls; });
+
+ const richImageSchema = new Schema({ attachment: { type: attachmentSchema, required: false } });
+ const brandingSchema = new Schema({ logo: richImageSchema });
+ const instanceSchema = new Schema({ branding: brandingSchema });
+ const TestModel = db.model('Test', instanceSchema);
+
+ const instance = await TestModel.create({ branding: { logo: {} } });
+ assert.strictEqual(attachmentSchemaPreValidateCalls, 0);
+ const doc = await TestModel.findById(instance._id);
+
+ doc.set('branding.logo.attachment', { name: 'coolLogo' });
+ await doc.save();
+ assert.strictEqual(attachmentSchemaPreValidateCalls, 1);
+
+ instance.set('branding.logo.attachment', { name: 'coolOtherLogo' });
+ await instance.save();
+ assert.strictEqual(attachmentSchemaPreValidateCalls, 2);
+ });
+
+ it('fires pre validate hooks on 5 level deep single nested subdoc when modifying after save() (gh-14591)', async function() {
+ let preValidate = [];
+
+ const createSchema = (path, subSchema) => {
+ const schema = new Schema({ [path]: subSchema });
+ schema.pre('validate', function() {
+ preValidate.push(path);
+ });
+ return schema;
+ };
+
+ const e = createSchema('e', { type: String });
+ const d = createSchema('d', { type: e });
+ const c = createSchema('c', { type: d });
+ const b = createSchema('b', { type: c });
+ const a = createSchema('a', { type: b });
+ b.add({ otherProp: String });
+ const NestedModelHooksTestModel = db.model('Test', a);
+
+ const newData = { a: { b: { c: { d: { e: new Date().toString() } } } } };
+ newData.a.otherProp = 'test';
+ const doc = await new NestedModelHooksTestModel(newData).save();
+ preValidate = [];
+ doc.set({ 'a.b.c.d.e': 'updated nested value' });
+ await doc.save();
+ assert.deepStrictEqual(preValidate, ['a', 'b', 'c', 'd', 'e']);
+
+ const fromDb = await NestedModelHooksTestModel.findById(doc._id);
+ assert.strictEqual(fromDb.a.otherProp, 'test');
+ });
+
+ it('returns constructor if using $model() with no args (gh-13878)', async function() {
+ const testSchema = new Schema({ name: String });
+ const Test = db.model('Test', testSchema);
+
+ const doc = new Test();
+ assert.strictEqual(doc.$model(), Test);
+ });
+
+ it('avoids creating separate subpaths entry for every element in array (gh-13874)', async function() {
+ const tradeSchema = new mongoose.Schema({ tradeId: Number, content: String });
+
+ const testSchema = new mongoose.Schema(
+ {
+ userId: Number,
+ tradeMap: {
+ type: Map,
+ of: tradeSchema
+ }
+ }
+ );
+
+ const TestModel = db.model('Test', testSchema);
+
+
+ const userId = 100;
+ const user = await TestModel.create({ userId, tradeMap: new Map() });
+
+ // add subDoc
+ for (let id = 1; id <= 10; id++) {
+ const trade = { tradeId: id, content: 'test' };
+ user.tradeMap.set(trade.tradeId.toString(), trade);
+ }
+ await user.save();
+ await TestModel.deleteOne({ userId });
+
+ assert.ok(Object.keys(TestModel.schema.subpaths).length <= 3);
+ });
+
+ it('handles embedded discriminators defined using Schema.prototype.discriminator (gh-13898)', async function() {
+ const baseNestedDiscriminated = new Schema({
+ type: { type: Number, required: true }
+ }, { discriminatorKey: 'type' });
+
+ class BaseClass {
+ whoAmI() {
+ return 'I am baseNestedDiscriminated';
+ }
+ }
+ BaseClass.type = 1;
+
+ baseNestedDiscriminated.loadClass(BaseClass);
+
+ class NumberTyped extends BaseClass {
+ whoAmI() {
+ return 'I am NumberTyped';
+ }
+ }
+ NumberTyped.type = 3;
+
+ class StringTyped extends BaseClass {
+ whoAmI() {
+ return 'I am StringTyped';
+ }
+ }
+ StringTyped.type = 4;
+
+ baseNestedDiscriminated.discriminator(1, new Schema({}).loadClass(NumberTyped));
+ baseNestedDiscriminated.discriminator('3', new Schema({}).loadClass(StringTyped));
+
+ const containsNestedSchema = new Schema({
+ nestedDiscriminatedTypes: { type: [baseNestedDiscriminated], required: true }
+ });
+
+ class ContainsNested {
+ whoAmI() {
+ return 'I am ContainsNested';
+ }
+ }
+ containsNestedSchema.loadClass(ContainsNested);
+
+ const Test = db.model('Test', containsNestedSchema);
+ const instance = await Test.create({ type: 1, nestedDiscriminatedTypes: [{ type: 1 }, { type: '3' }] });
+ assert.deepStrictEqual(
+ instance.nestedDiscriminatedTypes.map(i => i.whoAmI()),
+ ['I am NumberTyped', 'I am StringTyped']
+ );
+ });
+
+ it('handles embedded discriminators defined using Schema.prototype.discriminator after defining schema (gh-14109) (gh-13898)', async function() {
+ const baseNestedDiscriminated = new Schema({
+ type: { type: Number, required: true }
+ }, { discriminatorKey: 'type' });
+
+ class BaseClass {
+ whoAmI() {
+ return 'I am baseNestedDiscriminated';
+ }
+ }
+
+ baseNestedDiscriminated.loadClass(BaseClass);
+
+ class NumberTyped extends BaseClass {
+ whoAmI() {
+ return 'I am NumberTyped';
+ }
+ }
+
+ class StringTyped extends BaseClass {
+ whoAmI() {
+ return 'I am StringTyped';
+ }
+ }
+
+ const containsNestedSchema = new Schema({
+ nestedDiscriminatedTypes: { type: [baseNestedDiscriminated], required: true }
+ });
+
+ // After `containsNestedSchema`, in #13898 test these were before `containsNestedSchema`
+ baseNestedDiscriminated.discriminator(1, new Schema({}).loadClass(NumberTyped));
+ baseNestedDiscriminated.discriminator('3', new Schema({}).loadClass(StringTyped));
+
+ class ContainsNested {
+ whoAmI() {
+ return 'I am ContainsNested';
+ }
+ }
+ containsNestedSchema.loadClass(ContainsNested);
+
+ const Test = db.model('Test', containsNestedSchema);
+ const instance = await Test.create({ type: 1, nestedDiscriminatedTypes: [{ type: 1 }, { type: '3' }] });
+ assert.deepStrictEqual(
+ instance.nestedDiscriminatedTypes.map(i => i.whoAmI()),
+ ['I am NumberTyped', 'I am StringTyped']
+ );
+ });
+
+ it('handles embedded discriminators on nested path defined using Schema.prototype.discriminator (gh-14109) (gh-13898)', async function() {
+ const baseNestedDiscriminated = new Schema({
+ type: { type: Number, required: true }
+ }, { discriminatorKey: 'type' });
+
+ class BaseClass {
+ whoAmI() {
+ return 'I am baseNestedDiscriminated';
+ }
+ }
+
+ baseNestedDiscriminated.loadClass(BaseClass);
+
+ class NumberTyped extends BaseClass {
+ whoAmI() {
+ return 'I am NumberTyped';
+ }
+ }
+
+ class StringTyped extends BaseClass {
+ whoAmI() {
+ return 'I am StringTyped';
+ }
+ }
+
+ const containsNestedSchema = new Schema({
+ nestedDiscriminatedTypes: { type: [baseNestedDiscriminated], required: true }
+ });
+
+ baseNestedDiscriminated.discriminator(1, new Schema({}).loadClass(NumberTyped));
+ baseNestedDiscriminated.discriminator('3', new Schema({}).loadClass(StringTyped));
+
+ const l2Schema = new Schema({ l3: containsNestedSchema });
+ const l1Schema = new Schema({ l2: l2Schema });
+
+ const Test = db.model('Test', l1Schema);
+ const instance = await Test.create({
+ l2: {
+ l3: {
+ nestedDiscriminatedTypes: [{ type: 1 }, { type: '3' }]
+ }
+ }
+ });
+ assert.deepStrictEqual(
+ instance.l2.l3.nestedDiscriminatedTypes.map(i => i.whoAmI()),
+ ['I am NumberTyped', 'I am StringTyped']
+ );
+ });
+
+ it('handles middleware on embedded discriminators on nested path defined using Schema.prototype.discriminator (gh-14961)', async function() {
+ const eventSchema = new Schema(
+ { message: String },
+ { discriminatorKey: 'kind', _id: false }
+ );
+
+ const clickedSchema = new Schema({
+ element: String
+ }, { _id: false });
+
+ // This is the discriminator which we will use to test middleware
+ const purchasedSchema = new Schema({
+ product: String
+ }, { _id: false });
+
+ let eventSchemaPreValidateCalls = 0;
+ let eventSchemaPreSaveCalls = 0;
+ eventSchema.pre('validate', function() {
+ ++eventSchemaPreValidateCalls;
+ });
+ eventSchema.pre('save', function() {
+ ++eventSchemaPreSaveCalls;
+ });
+
+ let purchasedSchemaPreValidateCalls = 0;
+ let purchasedSchemaPreSaveCalls = 0;
+ purchasedSchema.pre('validate', function() {
+ ++purchasedSchemaPreValidateCalls;
+ });
+ purchasedSchema.pre('save', function() {
+ ++purchasedSchemaPreSaveCalls;
+ });
+
+ eventSchema.discriminator('Clicked', clickedSchema);
+ eventSchema.discriminator('Purchased', purchasedSchema);
+
+ const trackSchema = new Schema({
+ event: eventSchema
+ });
+
+ // Test
+
+ const MyModel = db.model('track', trackSchema);
+ const doc = new MyModel({
+ event: {
+ kind: 'Purchased',
+ message: 'Test',
+ product: 'iPhone'
+ }
+ });
+
+ await doc.save();
+ assert.equal(doc.event.message, 'Test');
+ assert.equal(doc.event.kind, 'Purchased');
+ assert.equal(doc.event.product, 'iPhone');
+
+ assert.strictEqual(eventSchemaPreValidateCalls, 1);
+ assert.strictEqual(eventSchemaPreSaveCalls, 1);
+ assert.strictEqual(purchasedSchemaPreValidateCalls, 1);
+ assert.strictEqual(purchasedSchemaPreSaveCalls, 1);
+ });
+
+ it('handles reusing schema with embedded discriminators defined using Schema.prototype.discriminator (gh-14162)', async function() {
+ const discriminated = new Schema({
+ type: { type: Number, required: true }
+ }, { discriminatorKey: 'type' });
+
+ discriminated.discriminator(1, new Schema({ prop1: String }));
+ discriminated.discriminator(3, new Schema({ prop2: String }));
+
+ const containerSchema = new Schema({ items: [discriminated] });
+ const containerModel = db.model('Test', containerSchema);
+ const containerModel2 = db.model('Test1', containerSchema);
+ const doc1 = new containerModel({ items: [{ type: 1, prop1: 'foo' }, { type: 3, prop2: 'bar' }] });
+ const doc2 = new containerModel2({ items: [{ type: 1, prop1: 'baz' }, { type: 3, prop2: 'qux' }] });
+ await doc1.save();
+ await doc2.save();
+
+ doc1.items.push({ type: 3, prop2: 'test1' });
+ doc2.items.push({ type: 3, prop2: 'test1' });
+ await doc1.save();
+ await doc2.save();
+ });
+
+ it('handles embedded recursive discriminators on nested path defined using Schema.prototype.discriminator (gh-14245)', async function() {
+ const baseSchema = new Schema({
+ type: { type: Number, required: true }
+ }, { discriminatorKey: 'type' });
+
+ class Base {
+ whoAmI() { return 'I am Base'; }
+ }
+
+ baseSchema.loadClass(Base);
+
+ class NumberTyped extends Base {
+ whoAmI() { return 'I am NumberTyped'; }
+ }
+
+ class StringTyped extends Base {
+ whoAmI() { return 'I am StringTyped'; }
+ }
+
+ const selfRefSchema = new Schema({
+ self: { type: [baseSchema], required: true }
+ });
+
+ class SelfReferenceTyped extends Base {
+ whoAmI() { return 'I am SelfReferenceTyped'; }
+ }
+
+ selfRefSchema.loadClass(SelfReferenceTyped);
+ baseSchema.discriminator(5, selfRefSchema);
+
+ const numberTypedSchema = new Schema({}).loadClass(NumberTyped);
+ const stringTypedSchema = new Schema({}).loadClass(StringTyped);
+ baseSchema.discriminator(1, numberTypedSchema);
+ baseSchema.discriminator(3, stringTypedSchema);
+ const containerSchema = new Schema({ items: [baseSchema] });
+ const containerModel = db.model('Test', containerSchema);
+
+ const instance = await containerModel.create({
+ items: [{ type: 5, self: [{ type: 1 }, { type: 3 }] }]
+ });
+
+ assert.equal(instance.items[0].whoAmI(), 'I am SelfReferenceTyped');
+ assert.deepStrictEqual(instance.items[0].self.map(item => item.whoAmI()), [
+ 'I am NumberTyped',
+ 'I am StringTyped'
+ ]);
+ });
+
+ it('can use `collection` as schema name (gh-13956)', async function() {
+ const schema = new mongoose.Schema({ name: String, collection: String });
+ const Test = db.model('Test', schema);
+
+ const doc = await Test.create({ name: 'foo', collection: 'bar' });
+ assert.strictEqual(doc.collection, 'bar');
+ doc.collection = 'baz';
+ await doc.save();
+ const { collection } = await Test.findById(doc);
+ assert.strictEqual(collection, 'baz');
+ });
+ it('avoids adding nested paths to markModified() output if adding a new field (gh-14024)', async function() {
+ const eventSchema = new Schema({
+ name: { type: String },
+ __stateBeforeSuspension: {
+ field1: { type: String },
+ field2: { type: String },
+ jsonField: {
+ name: { type: String },
+ name1: { type: String }
+ }
+ }
+ });
+ const Event = db.model('Event', eventSchema);
+ const eventObj = new Event({ name: 'event object', __stateBeforeSuspension: { field1: 'test', jsonField: { name: 'test3' } } });
+ await eventObj.save();
+ const newObject = { field1: 'test', jsonField: { name: 'test3', name1: 'test4' } };
+ eventObj.set('__stateBeforeSuspension', newObject);
+ assert.deepEqual(
+ eventObj.modifiedPaths(),
+ ['__stateBeforeSuspension', '__stateBeforeSuspension.jsonField']
+ );
+ });
+
+ it('should allow null values in list in self assignment (gh-14172) (gh-13859)', async function() {
+ const objSchema = new Schema({
+ date: Date,
+ value: Number
+ });
+
+ const testSchema = new Schema({
+ intArray: [Number],
+ strArray: [String],
+ objArray: [objSchema]
+ });
+ const Test = db.model('Test', testSchema);
+
+ const doc = new Test({
+ intArray: [1, 2, 3, null],
+ strArray: ['b', null, 'c'],
+ objArray: [
+ { date: new Date(1000), value: 1 },
+ null,
+ { date: new Date(3000), value: 3 }
+ ]
+ });
+ await doc.save();
+ doc.intArray = doc.intArray;
+ doc.strArray = doc.strArray;
+ doc.objArray = doc.objArray; // this is the trigger for the error
+ assert.ok(doc);
+ await doc.save();
+ assert.ok(doc);
+ });
+
+ it('avoids overwriting dotted paths in mixed path underneath nested path (gh-14178)', async function() {
+ const testSchema = new Schema({
+ __stateBeforeSuspension: {
+ field1: String,
+ field3: { type: Schema.Types.Mixed }
+ }
+ });
+ const Test = db.model('Test', testSchema);
+ const eventObj = new Test({
+ __stateBeforeSuspension: { field1: 'test' }
+ });
+ await eventObj.save();
+ const newO = eventObj.toObject();
+ newO.__stateBeforeSuspension.field3 = { '.ippo': 5 };
+ eventObj.set(newO);
+ await eventObj.save();
+
+ assert.strictEqual(eventObj.__stateBeforeSuspension.field3['.ippo'], 5);
+
+ const fromDb = await Test.findById(eventObj._id).lean().orFail();
+ assert.strictEqual(fromDb.__stateBeforeSuspension.field3['.ippo'], 5);
+ });
+
+ it('handles setting nested path to null (gh-14205)', function() {
+ const schema = new mongoose.Schema({
+ nested: {
+ key1: String,
+ key2: String
+ }
+ });
+
+ const Model = db.model('Test', schema);
+
+ const doc = new Model();
+ doc.init({
+ nested: { key1: 'foo', key2: 'bar' }
+ });
+
+ doc.set({ nested: null });
+ assert.strictEqual(doc.toObject().nested, null);
+ });
+
+ it('handles setting nested path to undefined (gh-14205)', function() {
+ const schema = new mongoose.Schema({
+ nested: {
+ key1: String,
+ key2: String
+ }
+ });
+
+ const Model = db.model('Test', schema);
+
+ const doc = new Model();
+ doc.init({
+ nested: { key1: 'foo', key2: 'bar' }
+ });
+
+ doc.set({ nested: void 0 });
+ assert.strictEqual(doc.toObject().nested, void 0);
+ });
+
+ it('handles setting nested path to spread doc with extra properties (gh-14269)', async function() {
+ const addressSchema = new mongoose.Schema(
+ {
+ street: String,
+ city: String,
+ state: String,
+ zip: Number
+ },
+ { _id: false }
+ );
+ const personSchema = new mongoose.Schema({
+ name: String,
+ age: Number,
+ address: addressSchema
+ });
+
+ const personModel = db.model('Person', personSchema);
+ const person = new personModel({
+ name: 'John',
+ age: 42,
+ address: {
+ street: '123 Fake St',
+ city: 'Springfield',
+ state: 'IL',
+ zip: 12345
+ }
+ });
+
+ await person.save();
+
+ person.address = {
+ ...person.address,
+ zip: 54321
+ };
+ assert.equal(person.address.zip, 54321);
+ });
+
+ it('includes virtuals in doc array toString() output if virtuals enabled on toObject (gh-14315)', function() {
+ const schema = new Schema({
+ docArr: [{ childId: mongoose.ObjectId }]
+ });
+ schema.virtual('docArr.child', { ref: 'Child', localField: 'docArr.childId', foreignField: '_id' });
+ schema.set('toObject', { virtuals: true });
+ schema.set('toJSON', { virtuals: true });
+ const Test = db.model('Test', schema);
+ const Child = db.model('Child', new Schema({
+ name: String
+ }));
+
+ const child = new Child({ name: 'test child' });
+ const doc = new Test({ docArr: [{ childId: child._id }] });
+ doc.docArr[0].child = child;
+ assert.ok(doc.docArr.toString().includes('child'), doc.docArr.toString());
+ assert.ok(doc.docArr.toString().includes('test child'), doc.docArr.toString());
+ });
+
+ it('minimizes when updating existing documents (gh-13782)', async function() {
+ const schema = new Schema({
+ metadata: {
+ type: {},
+ default: {},
+ required: true,
+ _id: false
+ }
+ }, { minimize: true });
+ const Model = db.model('Test', schema);
+ const m = new Model({ metadata: {} });
+ await m.save();
+
+ const x = await Model.findById(m._id).exec();
+ x.metadata = {};
+ await x.save();
+
+ const { metadata } = await Model.findById(m._id).lean().orFail();
+ assert.strictEqual(metadata, undefined);
+ });
+
+ it('saves when setting subdocument to empty object (gh-14420) (gh-13782)', async function() {
+ const SubSchema = new mongoose.Schema({
+ name: { type: String },
+ age: Number
+ }, { _id: false });
+
+ const MainSchema = new mongoose.Schema({
+ sub: {
+ type: SubSchema
+ }
+ });
+
+ const MainModel = db.model('Test', MainSchema);
+
+ const doc = new MainModel({ sub: { name: 'Hello World', age: 42 } });
+ await doc.save();
+
+ doc.sub = {};
+ await doc.save();
+
+ const savedDoc = await MainModel.findById(doc.id).orFail();
+ assert.strictEqual(savedDoc.sub, undefined);
+ });
+
+ it('validate supports validateAllPaths', async function() {
+ const schema = new mongoose.Schema({
+ name: {
+ type: String,
+ validate: v => !!v
+ },
+ age: {
+ type: Number,
+ validate: v => v == null || v < 200
+ },
+ subdoc: {
+ type: new Schema({
+ url: String
+ }, { _id: false }),
+ validate: v => v == null || v.url.length > 0
+ },
+ docArr: [{
+ subprop: {
+ type: String,
+ validate: v => v == null || v.length > 0
+ }
+ }]
+ });
+
+ const TestModel = db.model('Test', schema);
+
+ const doc = await TestModel.create({});
+ doc.name = '';
+ doc.age = 201;
+ doc.subdoc = { url: '' };
+ doc.docArr = [{ subprop: '' }];
+ await doc.save({ validateBeforeSave: false });
+ await doc.validate();
+
+ const err = await doc.validate({ validateAllPaths: true }).then(() => null, err => err);
+ assert.ok(err);
+ assert.equal(err.name, 'ValidationError');
+ assert.ok(err.errors['name']);
+ assert.ok(
+ err.errors['name'].message.includes('Validator failed for path `name` with value ``'),
+ err.errors['name'].message
+ );
+ assert.ok(err.errors['age']);
+ assert.ok(
+ err.errors['age'].message.includes('Validator failed for path `age` with value `201`'),
+ err.errors['age'].message
+ );
+ assert.ok(err.errors['subdoc']);
+ assert.ok(
+ err.errors['subdoc'].message.includes('Validator failed for path `subdoc` with value `{ url: \'\' }`'),
+ err.errors['subdoc'].message
+ );
+ assert.ok(err.errors['docArr.0.subprop']);
+ assert.ok(
+ err.errors['docArr.0.subprop'].message.includes('Validator failed for path `subprop` with value ``'),
+ err.errors['docArr.0.subprop'].message
+ );
+ });
+
+ it('validateSync() supports validateAllPaths', async function() {
+ const schema = new mongoose.Schema({
+ name: {
+ type: String,
+ validate: v => !!v
+ },
+ age: {
+ type: Number,
+ validate: v => v == null || v < 200
+ },
+ subdoc: {
+ type: new Schema({
+ url: String
+ }, { _id: false }),
+ validate: v => v == null || v.url.length > 0
+ },
+ docArr: [{
+ subprop: {
+ type: String,
+ validate: v => v == null || v.length > 0
+ }
+ }]
+ });
+
+ const TestModel = db.model('Test', schema);
+
+ const doc = await TestModel.create({});
+ doc.name = '';
+ doc.age = 201;
+ doc.subdoc = { url: '' };
+ doc.docArr = [{ subprop: '' }];
+ await doc.save({ validateBeforeSave: false });
+ await doc.validate();
+
+ const err = await doc.validateSync({ validateAllPaths: true });
+ assert.ok(err);
+ assert.equal(err.name, 'ValidationError');
+ assert.ok(err.errors['name']);
+ assert.ok(
+ err.errors['name'].message.includes('Validator failed for path `name` with value ``'),
+ err.errors['name'].message
+ );
+ assert.ok(err.errors['age']);
+ assert.ok(
+ err.errors['age'].message.includes('Validator failed for path `age` with value `201`'),
+ err.errors['age'].message
+ );
+ assert.ok(err.errors['subdoc']);
+ assert.ok(
+ err.errors['subdoc'].message.includes('Validator failed for path `subdoc` with value `{ url: \'\' }`'),
+ err.errors['subdoc'].message
+ );
+ assert.ok(err.errors['docArr.0.subprop']);
+ assert.ok(
+ err.errors['docArr.0.subprop'].message.includes('Validator failed for path `subprop` with value ``'),
+ err.errors['docArr.0.subprop'].message
+ );
+ });
+
+ it('minimize unsets property rather than setting to null (gh-14445)', async function() {
+ const SubSchema = new mongoose.Schema({
+ name: { type: String }
+ }, { _id: false });
+
+ const MainSchema = new mongoose.Schema({
+ name: String,
+ sub: {
+ type: SubSchema,
+ default: {}
+ }
+ });
+
+ const Test = db.model('Test', MainSchema);
+ const doc = new Test({ name: 'foo' });
+ await doc.save();
+
+ const savedDocFirst = await Test.findById(doc.id).orFail();
+ assert.deepStrictEqual(savedDocFirst.toObject({ minimize: false }).sub, {});
+
+ savedDocFirst.name = 'bar';
+ await savedDocFirst.save();
+
+ const lean = await Test.findById(doc.id).lean().orFail();
+ assert.strictEqual(lean.sub, undefined);
+
+ const savedDocSecond = await Test.findById(doc.id).orFail();
+ assert.deepStrictEqual(savedDocSecond.toObject({ minimize: false }).sub, {});
+ });
+
+ it('avoids depopulating populated subdocs underneath document arrays when copying to another document (gh-14418)', async function() {
+ const cartSchema = new mongoose.Schema({
+ products: [
+ {
+ product: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: 'Product'
+ },
+ quantity: Number
+ }
+ ],
+ singleProduct: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: 'Product'
+ }
+ });
+ const purchaseSchema = new mongoose.Schema({
+ products: [
+ {
+ product: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: 'Product'
+ },
+ quantity: Number
+ }
+ ],
+ singleProduct: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: 'Product'
+ }
+ });
+ const productSchema = new mongoose.Schema({
+ name: String
+ });
+
+ const Cart = db.model('Cart', cartSchema);
+ const Purchase = db.model('Purchase', purchaseSchema);
+ const Product = db.model('Product', productSchema);
+
+ const dbProduct = await Product.create({ name: 'Bug' });
+
+ const dbCart = await Cart.create({
+ products: [
+ {
+ product: dbProduct,
+ quantity: 2
+ }
+ ],
+ singleProduct: dbProduct
+ });
+
+ const foundCart = await Cart.findById(dbCart._id).
+ populate('products.product singleProduct');
+
+ const purchaseFromDbCart = new Purchase({
+ products: foundCart.products,
+ singleProduct: foundCart.singleProduct
+ });
+ assert.equal(purchaseFromDbCart.products[0].product.name, 'Bug');
+ assert.equal(purchaseFromDbCart.singleProduct.name, 'Bug');
+ });
+
+ it('handles virtuals that are stored as objects but getter returns string with toJSON (gh-14446)', async function() {
+ const childSchema = new mongoose.Schema();
+
+ childSchema.virtual('name')
+ .set(function(values) {
+ for (const [lang, val] of Object.entries(values)) {
+ this.set(`name.${lang}`, val);
+ }
+ })
+ .get(function() {
+ return this.$__getValue(`name.${this.lang}`);
+ });
+
+ childSchema.add({ name: { en: { type: String }, de: { type: String } } });
+
+ const ChildModel = db.model('Child', childSchema);
+ const ParentModel = db.model('Parent', new mongoose.Schema({
+ children: [childSchema]
+ }));
+
+ const child = await ChildModel.create({ name: { en: 'Stephen', de: 'Stefan' } });
+ child.lang = 'en';
+ assert.equal(child.name, 'Stephen');
+
+ const parent = await ParentModel.create({
+ children: [{ name: { en: 'Stephen', de: 'Stefan' } }]
+ });
+ parent.children[0].lang = 'de';
+ const obj = parent.toJSON({ getters: true });
+ assert.equal(obj.children[0].name, 'Stefan');
+ });
+
+ it('isDirectModified on paths underneath direct modified subdoc (gh-14502)', async function() {
+ const JsonFieldSchema = new Schema({
+ fieldA: String,
+ fieldB: String
+ });
+
+ const CommentSchema = new Schema({
+ title: String,
+ body: String,
+ jsonField: JsonFieldSchema
+ });
+
+ const BlogPostSchema = new Schema({
+ comment: CommentSchema
+ });
+
+ const Comments = db.model('Comments', CommentSchema);
+ const BlogPost = db.model('BlogPost', BlogPostSchema);
+
+ const comment1 = new Comments({});
+ comment1.init({
+ title: 'Test',
+ body: 'Test',
+ jsonField: {
+ fieldA: 'field A',
+ fieldB: 'field B'
+ }
+ });
+
+ const update = {
+ title: 'New test',
+ jsonField: {
+ fieldA: 'new Field A'
+ }
+ };
+ Object.assign(comment1, { ...update });
+
+ assert.ok(comment1.isDirectModified('jsonField.fieldA'));
+ assert.ok(comment1.jsonField.isDirectModified('fieldA'));
+
+ const blogPost = new BlogPost({});
+ blogPost.init({
+ comment: {
+ title: 'Test',
+ body: 'Test',
+ jsonField: {
+ fieldA: 'field A',
+ fieldB: 'field B'
+ }
+ }
+ });
+
+ Object.assign(blogPost.comment, { ...update });
+
+ assert.ok(blogPost.isDirectModified('comment.jsonField.fieldA'));
+ assert.ok(blogPost.comment.jsonField.isDirectModified('fieldA'));
+ });
+
+ it('avoids leaving subdoc _id in default state when setting subdocument to same value (gh-14722)', async function() {
+ const getUser = () => ({
+ _id: new mongoose.Types.ObjectId('66852317541ef0e22ae5214c'),
+ name: 'test1',
+ email: 'test@test.com'
+ });
+
+ const updateInfoSchema = new mongoose.Schema({
+ name: {
+ type: String, required: true
+ },
+ email: {
+ type: String,
+ required: true
+ }
+ }, {
+ versionKey: false
+ });
+ const schema = new mongoose.Schema({ name: String, updater: updateInfoSchema });
+
+ const TestModel = db.model('Test', schema);
+ const { _id } = await TestModel.create({ name: 'taco', updater: getUser() });
+ const doc = await TestModel.findById(_id).orFail();
+
+ doc.name = 'taco-edit';
+ doc.updater = getUser();
+ assert.deepStrictEqual(doc.getChanges(), { $set: { name: 'taco-edit' } });
+ assert.ok(!doc.$isDefault('updater._id'));
+ });
+
+ it('$clearModifiedPaths (gh-14268)', async function() {
+ const schema = new Schema({
+ name: String,
+ nested: {
+ subprop1: String
+ },
+ subdoc: new Schema({
+ subprop2: String
+ }, { _id: false }),
+ docArr: [new Schema({ subprop3: String }, { _id: false })]
+ });
+ const Test = db.model('Test', schema);
+
+ const doc = new Test({});
+ await doc.save();
+ doc.set({
+ name: 'test1',
+ nested: { subprop1: 'test2' },
+ subdoc: { subprop2: 'test3' },
+ docArr: [{ subprop3: 'test4' }]
+ });
+
+ assert.deepStrictEqual(doc.getChanges().$set, {
+ name: 'test1',
+ nested: { subprop1: 'test2' },
+ subdoc: { subprop2: 'test3' },
+ docArr: [{ subprop3: 'test4' }]
+ });
+ assert.deepStrictEqual(doc.getChanges().$inc, { __v: 1 });
+ doc.$clearModifiedPaths();
+ assert.deepStrictEqual(doc.getChanges(), {});
+
+ await doc.save();
+ const fromDb = await Test.findById(doc._id).lean();
+ assert.deepStrictEqual(fromDb, { _id: doc._id, __v: 0, docArr: [] });
+ });
+
+ it('$createModifiedPathsSnapshot and $restoreModifiedPathsSnapshot (gh-14268)', async function() {
+ const schema = new Schema({
+ name: String,
+ nested: {
+ subprop1: String
+ },
+ subdoc: new Schema({
+ subprop2: String
+ }, { _id: false }),
+ docArr: [new Schema({ subprop3: String }, { _id: false })]
+ });
+ const Test = db.model('Test', schema);
+
+ const doc = new Test({});
+ await doc.save();
+ doc.set({
+ name: 'test1',
+ nested: { subprop1: 'test2' },
+ subdoc: { subprop2: 'test3' },
+ docArr: [{ subprop3: 'test4' }]
+ });
+
+ assert.deepStrictEqual(doc.getChanges().$set, {
+ name: 'test1',
+ nested: { subprop1: 'test2' },
+ subdoc: { subprop2: 'test3' },
+ docArr: [{ subprop3: 'test4' }]
+ });
+ assert.deepStrictEqual(doc.getChanges().$inc, { __v: 1 });
+ assert.deepStrictEqual(doc.subdoc.getChanges(), { $set: { subprop2: 'test3' } });
+ assert.deepStrictEqual(doc.docArr[0].getChanges(), { $set: { subprop3: 'test4' } });
+
+ const snapshot = doc.$createModifiedPathsSnapshot();
+ doc.$clearModifiedPaths();
+
+ assert.deepStrictEqual(doc.getChanges(), {});
+ assert.deepStrictEqual(doc.subdoc.getChanges(), {});
+ assert.deepStrictEqual(doc.docArr[0].getChanges(), {});
+
+ doc.$restoreModifiedPathsSnapshot(snapshot);
+ assert.deepStrictEqual(doc.getChanges().$set, {
+ name: 'test1',
+ nested: { subprop1: 'test2' },
+ subdoc: { subprop2: 'test3' },
+ docArr: [{ subprop3: 'test4' }]
+ });
+ assert.deepStrictEqual(doc.getChanges().$inc, { __v: 1 });
+ assert.deepStrictEqual(doc.subdoc.getChanges(), { $set: { subprop2: 'test3' } });
+ assert.deepStrictEqual(doc.docArr[0].getChanges(), { $set: { subprop3: 'test4' } });
+
+ await doc.save();
+ const fromDb = await Test.findById(doc._id).lean();
+ assert.deepStrictEqual(fromDb, {
+ __v: 1,
+ _id: doc._id,
+ name: 'test1',
+ nested: { subprop1: 'test2' },
+ subdoc: { subprop2: 'test3' },
+ docArr: [{ subprop3: 'test4' }]
+ });
+ });
+ it('post deleteOne hook (gh-9885)', async function() {
+ const ChildSchema = new Schema({ name: String });
+ const called = {
+ preSave: 0,
+ postSave: 0,
+ preDeleteOne: 0,
+ postDeleteOne: 0
+ };
+ let postDeleteOneError = null;
+ ChildSchema.pre('save', function(next) {
+ ++called.preSave;
+ next();
+ });
+ ChildSchema.post('save', function(subdoc, next) {
+ ++called.postSave;
+ next();
+ });
+ ChildSchema.pre('deleteOne', { document: true, query: false }, function(next) {
+ ++called.preDeleteOne;
+ next();
+ });
+ ChildSchema.post('deleteOne', { document: true, query: false }, function(subdoc, next) {
+ ++called.postDeleteOne;
+ next(postDeleteOneError);
+ });
+ const ParentSchema = new Schema({ name: String, children: [ChildSchema] });
+ const ParentModel = db.model('Parent', ParentSchema);
+
+ const doc = new ParentModel({ name: 'Parent' });
+ await doc.save();
+ assert.deepStrictEqual(called, {
+ preSave: 0,
+ postSave: 0,
+ preDeleteOne: 0,
+ postDeleteOne: 0
+ });
+ doc.children.push({ name: 'Child 1' });
+ doc.children.push({ name: 'Child 2' });
+ doc.children.push({ name: 'Child 3' });
+ await doc.save();
+ assert.deepStrictEqual(called, {
+ preSave: 3,
+ postSave: 3,
+ preDeleteOne: 0,
+ postDeleteOne: 0
+ });
+ const child2 = doc.children[1];
+ child2.deleteOne();
+ await doc.save();
+ assert.deepStrictEqual(called, {
+ preSave: 5,
+ postSave: 5,
+ preDeleteOne: 1,
+ postDeleteOne: 1
+ });
+
+ postDeleteOneError = new Error('Test error in post deleteOne hook');
+ const child3 = doc.children[1];
+ child3.deleteOne();
+ await assert.rejects(
+ () => doc.save(),
+ /Test error in post deleteOne hook/
+ );
+ });
+
+ it('applies virtuals to subschemas if top-level schema has virtuals: true (gh-14771)', function() {
+ const userLabSchema = new mongoose.Schema({
+ capacityLevel: Number
+ });
+
+ userLabSchema.virtual('capacityLevelCeil').get(function() {
+ return Math.ceil(this.capacityLevel);
+ });
+
+ const labPlotSchema = new mongoose.Schema({
+ plotId: Number,
+ lab: userLabSchema
+ });
+
+ const userSchema = new mongoose.Schema({
+ username: String,
+ labPlots: [labPlotSchema]
+ }, { toObject: { virtuals: true }, toJSON: { virtuals: true } });
+
+ const User = db.model('User', userSchema);
+
+ const doc = new User({
+ username: 'test',
+ labPlots: [{
+ plotId: 1,
+ lab: { capacityLevel: 3.14 }
+ }]
+ });
+ assert.strictEqual(doc.toObject().labPlots[0].lab.capacityLevelCeil, 4);
+ assert.strictEqual(doc.toJSON().labPlots[0].lab.capacityLevelCeil, 4);
+ });
+
+ it('calls required with correct context on single nested properties (gh-14788)', async function() {
+ const requiredCalls = [];
+ function createCustomSchema() {
+ return new mongoose.Schema({
+ prop1: {
+ type: String,
+ required() {
+ requiredCalls.push(this);
+ return this.prop1 === undefined;
+ },
+ validate: {
+ validator(prop1) {
+ if (this.prop2 !== null && prop1 != null) {
+ throw new Error('cannot use prop1 if prop2 is defined!');
+ }
+ return true;
+ }
+ }
+ },
+ prop2: {
+ type: String,
+ required() {
+ requiredCalls.push(this);
+ return this.prop2 === undefined;
+ },
+ validate: {
+ validator(prop2) {
+ if (this.prop1 === null && prop2 === null) {
+ throw new Error('cannot be null if prop1 is missing!');
+ }
+ return true;
+ }
+ }
+ }
+ }, { _id: false });
+ }
+
+ const schema = new mongoose.Schema({
+ config: {
+ prop: {
+ type: createCustomSchema(),
+ required: true
+ }
+ }
+ });
+
+ const TestModel = db.model('Test', schema);
+ const doc = new TestModel({
+ config: {
+ prop: { prop1: null, prop2: 'test-value' }
+ }
+ });
+ await doc.validate();
+ assert.equal(requiredCalls.length, 2);
+ assert.strictEqual(requiredCalls[0], doc.config.prop);
+ assert.strictEqual(requiredCalls[1], doc.config.prop);
+ });
+
+ it('applies toObject() getters to 3 level deep subdocuments (gh-14840) (gh-14835)', async function() {
+ // Define nested schemas
+ const Level3Schema = new mongoose.Schema({
+ property: {
+ type: String,
+ get: (value) => value ? value.toUpperCase() : value
+ }
+ });
+
+ const Level2Schema = new mongoose.Schema({ level3: Level3Schema });
+ const Level1Schema = new mongoose.Schema({ level2: Level2Schema });
+ const MainSchema = new mongoose.Schema({ level1: Level1Schema });
+ const MainModel = db.model('Test', MainSchema);
+
+ const doc = await MainModel.create({
+ level1: {
+ level2: {
+ level3: {
+ property: 'testValue'
+ }
+ }
+ }
+ });
+
+ // Fetch and convert the document to an object with getters applied
+ const result = await MainModel.findById(doc._id);
+ const objectWithGetters = result.toObject({ getters: true, virtuals: false });
+ assert.strictEqual(objectWithGetters.level1.level2.level3.property, 'TESTVALUE');
+ });
+
+ it('handles inserting and saving large document with 10-level deep subdocs (gh-14897)', async function() {
+ const levels = 10;
+
+ let schema = new Schema({ test: { type: String, required: true } });
+ let doc = { test: 'gh-14897' };
+ for (let i = 0; i < levels; ++i) {
+ schema = new Schema({ level: Number, subdocs: [schema] });
+ doc = { level: (levels - i), subdocs: [{ ...doc }, { ...doc }] };
+ }
+
+ const Test = db.model('Test', schema);
+ const savedDoc = await Test.create(doc);
+
+ let cur = savedDoc;
+ for (let i = 0; i < levels - 1; ++i) {
+ cur = cur.subdocs[0];
+ }
+ cur.subdocs[0] = { test: 'updated' };
+ await savedDoc.save();
+ });
+
+ it('avoids flattening objectids on insertMany (gh-14935)', async function() {
+ const TestSchema = new Schema(
+ {
+ professionalId: {
+ type: Schema.Types.ObjectId
+ },
+ firstName: {
+ type: String
+ }
+ },
+ {
+ toObject: { flattenObjectIds: true }
+ }
+ );
+ const Test = db.model('Test', TestSchema);
+
+ const professionalId = new mongoose.Types.ObjectId();
+ await Test.insertMany([{ professionalId, firstName: 'test' }]);
+
+ const doc = await Test.findOne({ professionalId }).lean().orFail();
+ assert.ok(doc.professionalId instanceof mongoose.Types.ObjectId);
+ });
+
+ it('handles buffers stored as EJSON POJO (gh-14911)', async function() {
+ const pdfSchema = new mongoose.Schema({
+ pdfSettings: {
+ type: {
+ _id: false,
+ fileContent: { type: Buffer, required: true },
+ filePreview: { type: Buffer, required: true },
+ fileName: { type: String, required: true }
+ }
+ }
+ });
+ const PdfModel = db.model('Test', pdfSchema);
+
+ const _id = new mongoose.Types.ObjectId();
+ const buf = { $binary: Buffer.from('hello', 'utf8').toString('base64'), $type: '00' };
+ await PdfModel.collection.insertOne({
+ _id,
+ pdfSettings: {
+ fileContent: buf,
+ filePreview: buf,
+ fileName: 'sample.pdf'
+ }
+ });
+
+ const reloaded = await PdfModel.findById(_id);
+ assert.ok(Buffer.isBuffer(reloaded.pdfSettings.fileContent));
+ assert.strictEqual(reloaded.pdfSettings.fileContent.toString('utf8'), 'hello');
+ });
+
+ describe('gh-2306', function() {
+ it('allow define virtual on non-object path', function() {
+ const schema = new mongoose.Schema({ num: Number, str: String, nums: [Number] });
+ schema.path('nums').virtual('last').get(function() {
+ return this[this.length - 1];
+ });
+ schema.virtual('nums.first', { applyToArray: true }).get(function() {
+ return this[0];
+ });
+ schema.virtual('nums.selectedIndex', { applyToArray: true })
+ .get(function() {
+ return this.__selectedIndex;
+ })
+ .set(function(v) {
+ this.__selectedIndex = v;
+ });
+ const M = db.model('gh2306', schema);
+ const m = new M({ num: 2, str: 'a', nums: [1, 2, 3] });
+
+ assert.strictEqual(m.nums.last, 3);
+ assert.strictEqual(m.nums.first, 1);
+
+ assert.strictEqual(m.nums.selectedIndex, undefined);
+ m.nums.selectedIndex = 42;
+ assert.strictEqual(m.nums.__selectedIndex, 42);
+ });
+
+ it('works on document arrays', function() {
+ const schema = new mongoose.Schema({ books: [{ title: String, author: String }] });
+ schema.path('books').virtual('last').get(function() {
+ return this[this.length - 1];
+ });
+ schema.virtual('books.first', { applyToArray: true }).get(function() {
+ return this[0];
+ });
+ const M = db.model('Test', schema);
+ const m = new M({ books: [{ title: 'Casino Royale', author: 'Ian Fleming' }, { title: 'The Man With The Golden Gun', author: 'Ian Fleming' }] });
+
+ assert.strictEqual(m.books.first.title, 'Casino Royale');
+ assert.strictEqual(m.books.last.title, 'The Man With The Golden Gun');
+ });
+ });
+
+ it('clears modified subpaths when setting deeply nested subdoc to null (gh-14952)', async function() {
+ const currentMilestoneSchema = new Schema(
+ {
+ id: { type: String, required: true }
+ },
+ {
+ _id: false
+ }
+ );
+
+ const milestoneSchema = new Schema(
+ {
+ current: {
+ type: currentMilestoneSchema,
+ required: true
+ }
+ },
+ {
+ _id: false
+ }
+ );
+
+ const campaignSchema = new Schema(
+ {
+ milestones: {
+ type: milestoneSchema,
+ required: false
+ }
+ },
+ {
+ _id: false
+ }
+ );
+ const questSchema = new Schema(
+ {
+ campaign: { type: campaignSchema, required: false }
+ },
+ {
+ _id: false
+ }
+ );
+
+ const parentSchema = new Schema({
+ quests: [questSchema]
+ });
+
+ const ParentModel = db.model('Parent', parentSchema);
+ const doc = new ParentModel({
+ quests: [
+ {
+ campaign: {
+ milestones: {
+ current: {
+ id: 'milestone1'
+ }
+ }
+ }
+ }
+ ]
+ });
+
+ await doc.save();
+
+ // Set the nested schema to null
+ doc.quests[0].campaign.milestones.current = {
+ id: 'milestone1'
+ };
+ doc.quests[0].campaign.milestones.current = {
+ id: ''
+ };
+
+ doc.quests[0].campaign.milestones = null;
+ await doc.save();
+
+ const fromDb = await ParentModel.findById(doc._id).orFail();
+ assert.strictEqual(fromDb.quests[0].campaign.milestones, null);
+ });
+
+ it('handles custom error message for duplicate key errors (gh-12844)', async function() {
+ const schema = new Schema({
+ name: String,
+ email: { type: String, unique: [true, 'Email must be unique'] }
+ });
+ const Model = db.model('Test', schema);
+ await Model.init();
+
+ await Model.create({ email: 'test@example.com' });
+
+ let duplicateKeyError = await Model.create({ email: 'test@example.com' }).catch(err => err);
+ assert.strictEqual(duplicateKeyError.message, 'Email must be unique');
+ assert.strictEqual(duplicateKeyError.cause.code, 11000);
+
+ duplicateKeyError = await Model.updateOne({ name: 'test' }, { email: 'test@example.com' }, { upsert: true }).catch(err => err);
+ assert.strictEqual(duplicateKeyError.message, 'Email must be unique');
+ assert.strictEqual(duplicateKeyError.cause.code, 11000);
});
});
-describe('Check if instance function that is supplied in schema option is availabe', function() {
+describe('Check if instance function that is supplied in schema option is available', function() {
it('should give an instance function back rather than undefined', function ModelJS() {
const testSchema = new mongoose.Schema({}, { methods: { instanceFn() { return 'Returned from DocumentInstanceFn'; } } });
const TestModel = mongoose.model('TestModel', testSchema);
diff --git a/test/document.unit.test.js b/test/document.unit.test.js
index dbae16908ac..c8d89cf0c25 100644
--- a/test/document.unit.test.js
+++ b/test/document.unit.test.js
@@ -37,12 +37,12 @@ describe('toObject()', function() {
beforeEach(function() {
Stub = function() {
- const schema = this.$__schema = {
+ this.$__schema = {
options: { toObject: { minimize: false, virtuals: true } },
- virtuals: { virtual: 'test' }
+ virtuals: { virtual: { applyGetters: () => 'test' } }
};
+ this.$__schema._defaultToObjectOptions = () => this.$__schema.options.toObject;
this._doc = { empty: {} };
- this.get = function(path) { return schema.virtuals[path]; };
this.$__ = {};
};
Stub.prototype = Object.create(mongoose.Document.prototype);
@@ -61,8 +61,6 @@ describe('toObject()', function() {
it('doesnt crash with empty object (gh-3130)', function() {
const d = new Stub();
d._doc = undefined;
- assert.doesNotThrow(function() {
- d.toObject();
- });
+ d.toObject();
});
});
diff --git a/test/double.test.js b/test/double.test.js
new file mode 100644
index 00000000000..6bf7e6c59e7
--- /dev/null
+++ b/test/double.test.js
@@ -0,0 +1,432 @@
+'use strict';
+
+const assert = require('assert');
+const start = require('./common');
+const BSON = require('bson');
+
+const mongoose = start.mongoose;
+const Schema = mongoose.Schema;
+
+
+describe('Double', function() {
+ beforeEach(() => mongoose.deleteModel(/Test/));
+
+ it('is a valid schema type', function() {
+ const schema = new Schema({
+ myDouble: Schema.Types.Double
+ });
+ const Test = mongoose.model('Test', schema);
+
+ const doc = new Test({
+ myDouble: 13
+ });
+ assert.deepStrictEqual(doc.myDouble, new BSON.Double(13));
+ assert.equal(typeof doc.myDouble, 'object');
+ });
+
+ describe('supports the required property', function() {
+ it('when value is null', async function() {
+ const schema = new Schema({
+ Double: {
+ type: Schema.Types.Double,
+ required: true
+ }
+ });
+ const Test = mongoose.model('Test', schema);
+
+ const doc = new Test({
+ double: null
+ });
+
+ const err = await doc.validate().then(() => null, err => err);
+ assert.ok(err);
+ assert.ok(err.errors['Double']);
+ assert.equal(err.errors['Double'].name, 'ValidatorError');
+ assert.equal(
+ err.errors['Double'].message,
+ 'Path `Double` is required.'
+ );
+ });
+ it('when value is non-null', async function() {
+ const schema = new Schema({
+ Double: {
+ type: Schema.Types.Double,
+ required: true
+ }
+ });
+ const Test = mongoose.model('Test', schema);
+
+ const doc = new Test({
+ double: 3
+ });
+
+ const err = await doc.validate().then(() => null, err => err);
+ assert.ok(err);
+ assert.ok(err.errors['Double']);
+ assert.equal(err.errors['Double'].name, 'ValidatorError');
+ assert.equal(
+ err.errors['Double'].message,
+ 'Path `Double` is required.'
+ );
+ });
+ });
+
+ describe('special inputs', function() {
+ it('supports undefined as input', function() {
+ const schema = new Schema({
+ myDouble: {
+ type: Schema.Types.Double
+ }
+ });
+ const Test = mongoose.model('Test', schema);
+
+ const doc = new Test({
+ myDouble: undefined
+ });
+ assert.deepStrictEqual(doc.myDouble, undefined);
+ });
+
+ it('supports null as input', function() {
+ const schema = new Schema({
+ myDouble: {
+ type: Schema.Types.Double
+ }
+ });
+ const Test = mongoose.model('Test', schema);
+
+ const doc = new Test({
+ myDouble: null
+ });
+ assert.deepStrictEqual(doc.myDouble, null);
+ });
+ });
+
+ describe('valid casts', function() {
+ it('casts from decimal string', function() {
+ const schema = new Schema({
+ myDouble: {
+ type: Schema.Types.Double
+ }
+ });
+ const Test = mongoose.model('Test', schema);
+
+ const doc = new Test({
+ myDouble: '-42.008'
+ });
+ assert.deepStrictEqual(doc.myDouble, new BSON.Double(-42.008));
+ });
+
+ it('casts from exponential string', function() {
+ const schema = new Schema({
+ myDouble: {
+ type: Schema.Types.Double
+ }
+ });
+ const Test = mongoose.model('Test', schema);
+
+ const doc = new Test({
+ myDouble: '1.22008e45'
+ });
+ assert.deepStrictEqual(doc.myDouble, new BSON.Double(1.22008e45));
+ });
+
+ it('casts from infinite string', function() {
+ const schema = new Schema({
+ myDouble1: {
+ type: Schema.Types.Double
+ },
+ myDouble2: {
+ type: Schema.Types.Double
+ }
+ });
+ const Test = mongoose.model('Test', schema);
+
+ const doc = new Test({
+ myDouble1: 'Infinity',
+ myDouble2: '-Infinity'
+ });
+ assert.deepStrictEqual(doc.myDouble1, new BSON.Double(Infinity));
+ assert.deepStrictEqual(doc.myDouble2, new BSON.Double(-Infinity));
+ });
+
+ it('casts from NaN string', function() {
+ const schema = new Schema({
+ myDouble: {
+ type: Schema.Types.Double
+ }
+ });
+ const Test = mongoose.model('Test', schema);
+
+ const doc = new Test({
+ myDouble: 'NaN'
+ });
+ assert.deepStrictEqual(doc.myDouble, new BSON.Double('NaN'));
+ });
+
+ it('casts from number', function() {
+ const schema = new Schema({
+ myDouble: Schema.Types.Double
+ });
+ const Test = mongoose.model('Test', schema);
+
+ const doc = new Test({
+ myDouble: 988
+ });
+ assert.deepStrictEqual(doc.myDouble, new BSON.Double(988));
+ });
+
+ it('casts from bigint', function() {
+ const schema = new Schema({
+ myDouble: Schema.Types.Double
+ });
+ const Test = mongoose.model('Test', schema);
+
+ const doc = new Test({
+ myDouble: -997n
+ });
+ assert.deepStrictEqual(doc.myDouble, new BSON.Double(-997));
+ });
+
+ it('casts from BSON.Long', function() {
+ const schema = new Schema({
+ myDouble: Schema.Types.Double
+ });
+ const Test = mongoose.model('Test', schema);
+
+ const doc = new Test({
+ myDouble: BSON.Long.fromNumber(-997987)
+ });
+ assert.deepStrictEqual(doc.myDouble, new BSON.Double(-997987));
+ });
+
+ it('casts from BSON.Double', function() {
+ const schema = new Schema({
+ myDouble: Schema.Types.Double
+ });
+ const Test = mongoose.model('Test', schema);
+
+ const doc = new Test({
+ myDouble: new BSON.Double(-997983.33)
+ });
+ assert.deepStrictEqual(doc.myDouble, new BSON.Double(-997983.33));
+ });
+
+ it('casts boolean true to 1', function() {
+ const schema = new Schema({
+ myDouble: Schema.Types.Double
+ });
+ const Test = mongoose.model('Test', schema);
+
+ const doc = new Test({
+ myDouble: true
+ });
+ assert.deepStrictEqual(doc.myDouble, new BSON.Double(1));
+ });
+
+ it('casts boolean false to 0', function() {
+ const schema = new Schema({
+ myDouble: Schema.Types.Double
+ });
+ const Test = mongoose.model('Test', schema);
+
+ const doc = new Test({
+ myDouble: false
+ });
+ assert.deepStrictEqual(doc.myDouble, new BSON.Double(0));
+ });
+
+ it('casts empty string to null', function() {
+ const schema = new Schema({
+ myDouble: Schema.Types.Double
+ });
+ const Test = mongoose.model('Test', schema);
+
+ const doc = new Test({
+ myDouble: ''
+ });
+ assert.deepStrictEqual(doc.myDouble, null);
+ });
+
+ it('supports valueOf() function ', function() {
+ const schema = new Schema({
+ myDouble: Schema.Types.Double
+ });
+ const Test = mongoose.model('Test', schema);
+
+ const doc = new Test({
+ myDouble: { a: 'random', b: { c: 'whatever' }, valueOf: () => 83.008 }
+ });
+ assert.deepStrictEqual(doc.myDouble, new BSON.Double(83.008));
+ });
+ });
+
+ describe('cast errors', () => {
+ let Test;
+
+ beforeEach(function() {
+ const schema = new Schema({
+ myDouble: Schema.Types.Double
+ });
+ Test = mongoose.model('Test', schema);
+ });
+
+ describe('when a non-numeric string is provided to an Double field', () => {
+ it('throws a CastError upon validation', async() => {
+ const doc = new Test({
+ myDouble: 'helloworld'
+ });
+
+ assert.deepStrictEqual(doc.myDouble, undefined);
+ const err = await doc.validate().catch(e => e);
+ assert.ok(err);
+ assert.ok(err.errors['myDouble']);
+ assert.equal(err.errors['myDouble'].name, 'CastError');
+ assert.equal(
+ err.errors['myDouble'].message,
+ 'Cast to Double failed for value "helloworld" (type string) at path "myDouble"'
+ );
+ });
+ });
+ });
+
+ describe('custom casters', () => {
+ const defaultCast = mongoose.Schema.Types.Double.cast();
+
+ afterEach(() => {
+ mongoose.Schema.Types.Double.cast(defaultCast);
+ });
+
+ it('supports cast disabled', async() => {
+ mongoose.Schema.Types.Double.cast(false);
+ const schema = new Schema({
+ myDouble1: {
+ type: Schema.Types.Double
+ },
+ myDouble2: {
+ type: Schema.Types.Double
+ }
+ });
+ const Test = mongoose.model('Test', schema);
+ const doc = new Test({
+ myDouble1: '52',
+ myDouble2: new BSON.Double(52)
+ });
+ assert.deepStrictEqual(doc.myDouble1, undefined);
+ assert.deepStrictEqual(doc.myDouble2, new BSON.Double(52));
+
+ const err = await doc.validate().catch(e => e);
+ assert.ok(err);
+ assert.ok(err.errors['myDouble1']);
+ });
+
+ it('supports custom cast', () => {
+ mongoose.Schema.Types.Double.cast(v => {
+ if (isNaN(v)) {
+ return new BSON.Double(2);
+ }
+ return defaultCast(v);
+ });
+ const schema = new Schema({
+ myDouble: {
+ type: Schema.Types.Double
+ }
+ });
+
+ const Test = mongoose.model('Test', schema);
+ const doc = new Test({
+ myDouble: NaN
+ });
+ assert.deepStrictEqual(doc.myDouble, new BSON.Double(2));
+ });
+ });
+
+ describe('mongoDB integration', function() {
+ let db;
+ let Test;
+
+ before(async function() {
+ db = await start();
+
+ const schema = new Schema({
+ myDouble: Schema.Types.Double
+ });
+ db.deleteModel(/Test/);
+ Test = db.model('Test', schema);
+ });
+
+ after(async function() {
+ await db.close();
+ });
+
+ beforeEach(async() => {
+ await Test.deleteMany({});
+ });
+
+ describe('$type compatibility', function() {
+ it('is queryable as a JS number in MongoDB', async function() {
+ await Test.create({ myDouble: '42.04' });
+ const doc = await Test.findOne({ myDouble: { $type: 'number' } });
+ assert.ok(doc);
+ assert.deepStrictEqual(doc.myDouble, new BSON.Double(42.04));
+ });
+
+ it('is NOT queryable as a BSON Integer in MongoDB if the value is NOT integer', async function() {
+ await Test.create({ myDouble: '42.04' });
+ const doc = await Test.findOne({ myDouble: { $type: 'int' } });
+ assert.deepStrictEqual(doc, null);
+ });
+
+ it('is queryable as a BSON Double in MongoDB when a non-integer is provided', async function() {
+ await Test.create({ myDouble: '42.04' });
+ const doc = await Test.findOne({ myDouble: { $type: 'double' } });
+ assert.deepStrictEqual(doc.myDouble, new BSON.Double(42.04));
+ });
+
+ it('is queryable as a BSON Double in MongoDB when an integer is provided', async function() {
+ await Test.create({ myDouble: '42' });
+ const doc = await Test.findOne({ myDouble: { $type: 'double' } });
+ assert.deepStrictEqual(doc.myDouble, new BSON.Double(42));
+ });
+ });
+
+ it('can query with comparison operators', async function() {
+ await Test.create([
+ { myDouble: 1.2 },
+ { myDouble: 1.709 },
+ { myDouble: 1.710 },
+ { myDouble: 1.8 }
+ ]);
+
+ let docs = await Test.find({ myDouble: { $gte: 1.710 } }).sort({ myDouble: 1 });
+ assert.equal(docs.length, 2);
+ assert.deepStrictEqual(docs.map(doc => doc.myDouble), [new BSON.Double(1.710), new BSON.Double(1.8)]);
+
+ docs = await Test.find({ myDouble: { $lt: 1.710 } }).sort({ myDouble: -1 });
+ assert.equal(docs.length, 2);
+ assert.deepStrictEqual(docs.map(doc => doc.myDouble), [new BSON.Double(1.709), new BSON.Double(1.2)]);
+ });
+
+ it('supports populate()', async function() {
+ const parentSchema = new Schema({
+ child: {
+ type: Schema.Types.Double,
+ ref: 'Child'
+ }
+ });
+ const childSchema = new Schema({
+ _id: Schema.Types.Double,
+ name: String
+ });
+ const Parent = db.model('Parent', parentSchema);
+ const Child = db.model('Child', childSchema);
+
+ const { _id } = await Parent.create({ child: 42 });
+ await Child.create({ _id: 42, name: 'test-Double-populate' });
+
+ const doc = await Parent.findById(_id).populate('child');
+ assert.ok(doc);
+ assert.equal(doc.child.name, 'test-Double-populate');
+ assert.equal(doc.child._id, 42);
+ });
+ });
+});
diff --git a/test/driver.test.js b/test/driver.test.js
new file mode 100644
index 00000000000..e5005ad13f8
--- /dev/null
+++ b/test/driver.test.js
@@ -0,0 +1,115 @@
+'use strict';
+
+const { EventEmitter } = require('events');
+const assert = require('assert');
+const start = require('./common');
+
+const mongoose = start.mongoose;
+const Schema = mongoose.Schema;
+
+describe('driver', function() {
+ it('can set custom driver (gh-11900)', async function() {
+ const m = new mongoose.Mongoose();
+
+ class Collection {
+ findOne(filter, options) { // eslint-disable-line no-unused-vars
+ return Promise.resolve({ answer: 42 });
+ }
+ }
+ class Connection extends EventEmitter {
+ constructor(base) {
+ super();
+ this.base = base;
+ this.models = {};
+ }
+
+ collection() {
+ return new Collection();
+ }
+
+ async openUri() {
+ this.readyState = mongoose.ConnectionStates.connected;
+ return this;
+ }
+ }
+ const driver = {
+ Collection,
+ Connection,
+ plugins: [foo]
+ };
+
+ m.setDriver(driver);
+ assert.deepStrictEqual(m.plugins.slice(mongoose.plugins.length), [[foo, undefined]]);
+
+ await m.connect();
+
+ const Test = m.model('Test', m.Schema({ answer: Number }));
+
+ const res = await Test.findOne();
+ assert.deepEqual(res.toObject(), { answer: 42 });
+
+ function foo() {}
+ });
+
+ it('multiple drivers (gh-12638)', async function() {
+ const m1 = new mongoose.Mongoose();
+ const m2 = new mongoose.Mongoose();
+
+ class Connection1 extends EventEmitter {
+ constructor(base) {
+ super();
+ this.base = base;
+ this.models = {};
+ this.collections = {};
+ }
+
+ collection() {
+ return new Collection();
+ }
+
+ async openUri() {
+ this.readyState = mongoose.ConnectionStates.connected;
+ return this;
+ }
+ }
+ class Collection {
+ insertOne(doc, options) { // eslint-disable-line no-unused-vars
+ this.doc = doc;
+ return Promise.resolve();
+ }
+
+ findOne(filter, options) { // eslint-disable-line no-unused-vars
+ return Promise.resolve(this.doc);
+ }
+ }
+ class Connection2 extends Connection1 {}
+
+ const driver1 = {
+ Collection,
+ Connection: Connection1
+ };
+ const driver2 = {
+ Collection,
+ Connection: Connection2
+ };
+
+ m1.setDriver(driver1);
+ m2.setDriver(driver2);
+
+ await m1.connect('fake.com');
+ const Test = m1.model('Test', new Schema({ answer: Number }, { versionKey: false }));
+ await Test.create({ answer: 42 });
+ let doc = await Test.findOne();
+ /* eslint-disable no-unused-vars */
+ assert.deepEqual((({ _id, ...doc }) => doc)(doc.toObject()), { answer: 42 });
+
+ await m2.connect('fake.com');
+ const Test2 = m2.model('Test', new Schema({ question: String }, { versionKey: false }));
+ await Test2.create({ question: 'Calculating...' });
+ doc = await Test2.findOne();
+ assert.deepEqual((({ _id, ...doc }) => doc)(doc.toObject()), { question: 'Calculating...' });
+
+ doc = await Test.findOne();
+ assert.deepEqual((({ _id, ...doc }) => doc)(doc.toObject()), { answer: 42 });
+ });
+});
diff --git a/test/helpers/applyWriteConcern.test.js b/test/helpers/applyWriteConcern.test.js
new file mode 100644
index 00000000000..ce9e5783465
--- /dev/null
+++ b/test/helpers/applyWriteConcern.test.js
@@ -0,0 +1,22 @@
+'use strict';
+
+const assert = require('assert');
+const applyWriteConcern = require('../../lib/helpers/schema/applyWriteConcern');
+const start = require('../common');
+const mongoose = start.mongoose;
+
+describe('applyWriteConcern', function() {
+ let db;
+ before(function() {
+ db = start();
+ });
+ after(async function() {
+ await db.close();
+ });
+ it('should not overwrite user specified writeConcern options (gh-13592)', async function() {
+ const options = { writeConcern: { w: 'majority' } };
+ const testSchema = new mongoose.Schema({ name: String }, { writeConcern: { w: 0 } });
+ applyWriteConcern(testSchema, options);
+ assert.deepStrictEqual({ writeConcern: { w: 'majority' } }, options);
+ });
+});
diff --git a/test/helpers/clone.test.js b/test/helpers/clone.test.js
index d8c6a74f103..80540041c2b 100644
--- a/test/helpers/clone.test.js
+++ b/test/helpers/clone.test.js
@@ -88,7 +88,7 @@ describe('clone', () => {
$isSingleNested: true,
toObject(cloneOpts) {
assert.deepStrictEqual(
- Object.assign({}, baseOpts, { getters: false }),
+ Object.assign({}, baseOpts),
cloneOpts
);
const obj = JSON.parse(JSON.stringify(base));
@@ -219,10 +219,6 @@ describe('clone', () => {
});
});
- describe('wrapper', () => {
-
- });
-
describe('any else', () => {
it('valueOf', () => {
let called = false;
@@ -262,4 +258,55 @@ describe('clone', () => {
assert.equal(cloned.constructor, Object);
});
});
+
+ it('retains RegExp options gh-1355', function() {
+ const a = new RegExp('hello', 'igm');
+ assert.ok(a.global);
+ assert.ok(a.ignoreCase);
+ assert.ok(a.multiline);
+
+ const b = clone(a);
+ assert.equal(b.source, a.source);
+ assert.equal(a.global, b.global);
+ assert.equal(a.ignoreCase, b.ignoreCase);
+ assert.equal(a.multiline, b.multiline);
+ });
+
+ it('clones objects created with Object.create(null)', function() {
+ const o = Object.create(null);
+ o.a = 0;
+ o.b = '0';
+ o.c = 1;
+ o.d = '1';
+
+ const out = clone(o);
+ assert.strictEqual(0, out.a);
+ assert.strictEqual('0', out.b);
+ assert.strictEqual(1, out.c);
+ assert.strictEqual('1', out.d);
+ assert.equal(Object.keys(out).length, 4);
+ });
+
+ it('doesnt minimize empty objects in arrays to null (gh-7322)', function() {
+ const o = { arr: [{ a: 42 }, {}, {}] };
+
+ const out = clone(o, { minimize: true });
+ assert.deepEqual(out.arr[0], { a: 42 });
+ assert.deepEqual(out.arr[1], {});
+ assert.deepEqual(out.arr[2], {});
+ });
+
+ it('skips cloning types that have `toBSON()` if `bson` is set (gh-8299)', function() {
+ const o = {
+ toBSON() {
+ return 'toBSON';
+ },
+ valueOf() {
+ return 'valueOf()';
+ }
+ };
+
+ const out = clone(o, { bson: true });
+ assert.deepEqual(out, o);
+ });
});
diff --git a/test/helpers/document.getDeepestSubdocumentForPath.js b/test/helpers/document.getDeepestSubdocumentForPath.js
new file mode 100644
index 00000000000..c076ead3a12
--- /dev/null
+++ b/test/helpers/document.getDeepestSubdocumentForPath.js
@@ -0,0 +1,94 @@
+'use strict';
+
+const assert = require('assert');
+const getDeepestSubdocumentForPath = require('../../lib/helpers/document/getDeepestSubdocumentForPath');
+const { mongoose } = require('../common');
+
+describe('getDeepestSubdocumentForPath', function() {
+ beforeEach(() => mongoose.deleteModel(/Test/));
+ after(() => mongoose.deleteModel(/Test/));
+
+ it('returns top-level document if no subdocs', function() {
+ const Test = mongoose.model('Test', mongoose.Schema({ name: String }));
+ const doc = new Test({ name: 'foo' });
+
+ assert.strictEqual(getDeepestSubdocumentForPath(doc, ['name'], doc.schema), doc);
+ });
+
+ it('picks up single nested subdocs along the path', function() {
+ const Test = mongoose.model('Test', mongoose.Schema({
+ name: String,
+ nested: mongoose.Schema({
+ nestedPath: String
+ })
+ }));
+ const doc = new Test({ name: 'foo', nested: { nestedPath: 'bar' } });
+
+ assert.strictEqual(
+ getDeepestSubdocumentForPath(doc, ['nested', 'nestedPath'], doc.schema),
+ doc.nested
+ );
+ });
+
+ it('picks up document arrays', function() {
+ const Test = mongoose.model('Test', mongoose.Schema({
+ name: String,
+ docArr: [{
+ nestedPath: String
+ }]
+ }));
+ const doc = new Test({ name: 'foo', docArr: [{ nestedPath: 'bar' }] });
+
+ const res = getDeepestSubdocumentForPath(doc, ['docArr', '0', 'nestedPath'], doc.schema);
+ const expected = doc.docArr[0];
+ assert.strictEqual(res, expected);
+ });
+
+ it('picks up doubly nested subdocuments', function() {
+ const Test = mongoose.model('Test', mongoose.Schema({
+ name: String,
+ l1: mongoose.Schema({
+ l2: mongoose.Schema({
+ nested: String
+ })
+ })
+ }));
+ const doc = new Test({ name: 'foo', l1: { l2: { nested: 'bar' } } });
+
+ const res = getDeepestSubdocumentForPath(doc, ['l1', 'l2', 'nested'], doc.schema);
+ const expected = doc.l1.l2;
+ assert.strictEqual(res, expected);
+ });
+
+ it('returns deepest non-null subdoc', function() {
+ const Test = mongoose.model('Test', mongoose.Schema({
+ name: String,
+ l1: mongoose.Schema({
+ l2: mongoose.Schema({
+ nested: String
+ })
+ })
+ }));
+ const doc = new Test({ name: 'foo', l1: { l2: null } });
+
+ const res = getDeepestSubdocumentForPath(doc, ['l1', 'l2', 'nested'], doc.schema);
+ const expected = doc.l1;
+ assert.strictEqual(res, expected);
+ });
+
+ it('picks up single nested subdocs under document arrays', function() {
+ const Test = mongoose.model('Test', mongoose.Schema({
+ name: String,
+ docArr: [{
+ nested: mongoose.Schema({
+ l3Path: String
+ })
+ }]
+ }));
+ const doc = new Test({ name: 'foo', docArr: [{ nested: { l3Path: 'bar' } }] });
+
+ const res = getDeepestSubdocumentForPath(doc, ['docArr', '0', 'nested', 'l3Path'], doc.schema);
+ const expected = doc.docArr[0].nested;
+ assert.strictEqual(res, expected);
+ });
+});
diff --git a/test/helpers/getModelsMapForPopulate.test.js b/test/helpers/getModelsMapForPopulate.test.js
new file mode 100644
index 00000000000..5b2d6632f68
--- /dev/null
+++ b/test/helpers/getModelsMapForPopulate.test.js
@@ -0,0 +1,41 @@
+'use strict';
+
+const assert = require('assert');
+const start = require('../common');
+const util = require('../util');
+
+const mongoose = start.mongoose;
+const Schema = mongoose.Schema;
+
+describe('getModelsMapForPopulate', function() {
+ let db;
+
+ beforeEach(() => db.deleteModel(/.*/));
+
+ before(function() {
+ db = start();
+ });
+
+ after(async function() {
+ await db.close();
+ });
+
+ afterEach(() => util.clearTestData(db));
+ afterEach(() => util.stopRemainingOps(db));
+
+ it('should error on missing options on populate', async function() {
+ const sch = new Schema({
+ test: mongoose.Schema.Types.ObjectId
+ }, {
+ virtuals: {
+ someVirtual: {}
+ }
+ });
+
+ const model = db.model('Test', sch);
+
+ const doc = await model.create({ test: new mongoose.Types.ObjectId() });
+
+ await assert.rejects(() => model.findById(doc._id).populate('someVirtual').exec(), /Cannot populate virtual `someVirtual` on model `Test`, because options `localField` and \/ or `foreignField` are missing/);
+ });
+});
diff --git a/test/helpers/indexes.getRelatedIndexes.test.js b/test/helpers/indexes.getRelatedIndexes.test.js
index 3eb597f883d..de71b9e324a 100644
--- a/test/helpers/indexes.getRelatedIndexes.test.js
+++ b/test/helpers/indexes.getRelatedIndexes.test.js
@@ -92,6 +92,46 @@ describe('getRelatedIndexes', () => {
]
);
});
+ it('with base model that has discriminator, it includes discriminator indexes that only checks for existence', () => {
+ // Arrange
+ const eventSchema = new Schema(
+ { actorId: { type: Schema.Types.ObjectId } },
+ { autoIndex: false }
+ );
+ eventSchema.index({ actorId: 1 },
+ { unique: true,
+ partialFilterExpression: {
+ __t: { $exists: true }
+ }
+ });
+
+ const Event = db.model('Event', eventSchema);
+
+ const clickEventSchema = new Schema(
+ {
+ clickedAt: Date,
+ productCategory: String
+ },
+ { autoIndex: false }
+ );
+ Event.discriminator('ClickEvent', clickEventSchema);
+
+ // Act
+ const filteredSchemaIndexes = getRelatedSchemaIndexes(Event, Event.schema.indexes());
+
+ // Assert
+ assert.deepStrictEqual(
+ filteredSchemaIndexes,
+ [
+ [{ actorId: 1 },
+ { background: true,
+ unique: true,
+ partialFilterExpression: { __t: { $exists: true } }
+ }
+ ]
+ ]
+ );
+ });
it('with discriminator model, it only gets discriminator indexes', () => {
// Arrange
const eventSchema = new Schema(
diff --git a/test/helpers/indexes.isIndexSpecEqual.js b/test/helpers/indexes.isIndexSpecEqual.js
new file mode 100644
index 00000000000..0995836ddbd
--- /dev/null
+++ b/test/helpers/indexes.isIndexSpecEqual.js
@@ -0,0 +1,27 @@
+'use strict';
+
+const assert = require('assert');
+const isIndexSpecEqual = require('../../lib/helpers/indexes/isIndexSpecEqual');
+
+describe('isIndexSpecEqual', function() {
+ it('should return true for equal index specifications', () => {
+ const spec1 = { name: 1, age: -1 };
+ const spec2 = { name: 1, age: -1 };
+ const result = isIndexSpecEqual(spec1, spec2);
+ assert.strictEqual(result, true);
+ });
+
+ it('should return false for different key order', () => {
+ const spec1 = { name: 1, age: -1 };
+ const spec2 = { age: -1, name: 1 };
+ const result = isIndexSpecEqual(spec1, spec2);
+ assert.strictEqual(result, false);
+ });
+
+ it('should return false for different index keys', () => {
+ const spec1 = { name: 1, age: -1 };
+ const spec2 = { name: 1, dob: -1 };
+ const result = isIndexSpecEqual(spec1, spec2);
+ assert.strictEqual(result, false);
+ });
+});
diff --git a/test/helpers/isBsonType.test.js b/test/helpers/isBsonType.test.js
index e604edd224a..2fe16b68892 100644
--- a/test/helpers/isBsonType.test.js
+++ b/test/helpers/isBsonType.test.js
@@ -5,6 +5,8 @@ const isBsonType = require('../../lib/helpers/isBsonType');
const Decimal128 = require('mongodb').Decimal128;
const ObjectId = require('mongodb').ObjectId;
+const Double = require('mongodb').Double;
+const Int32 = require('mongodb').Int32;
describe('isBsonType', () => {
it('true for any object with _bsontype property equal typename', () => {
@@ -28,6 +30,14 @@ describe('isBsonType', () => {
});
it('true for ObjectId', () => {
- assert.ok(isBsonType(new ObjectId(), 'ObjectID'));
+ assert.ok(isBsonType(new ObjectId(), 'ObjectId'));
+ });
+
+ it('true for Double', () => {
+ assert.ok(isBsonType(new Double(), 'Double'));
+ });
+
+ it('true for Int32', () => {
+ assert.ok(isBsonType(new Int32(), 'Int32'));
});
});
diff --git a/test/helpers/isSimpleValidator.test.js b/test/helpers/isSimpleValidator.test.js
index e76d4c4bcb2..4e44ecb5986 100644
--- a/test/helpers/isSimpleValidator.test.js
+++ b/test/helpers/isSimpleValidator.test.js
@@ -4,7 +4,7 @@ const assert = require('assert');
require('../common'); // required for side-effect setup (so that the default driver is set-up)
const isSimpleValidator = require('../../lib/helpers/isSimpleValidator');
-const MongooseDocumentArray = require('../../lib/types/DocumentArray');
+const MongooseDocumentArray = require('../../lib/types/documentArray');
describe('isSimpleValidator', function() {
it('empty object', function() {
diff --git a/test/helpers/projection.applyProjection.test.js b/test/helpers/projection.applyProjection.test.js
index fadfe53fa25..e73d0d657ee 100644
--- a/test/helpers/projection.applyProjection.test.js
+++ b/test/helpers/projection.applyProjection.test.js
@@ -21,4 +21,15 @@ describe('applyProjection', function() {
assert.deepEqual(applyProjection(obj, { 'nested.str2': 0 }), { str: 'test', nested: { num3: 42 } });
assert.deepEqual(applyProjection(obj, { nested: { num3: 0 } }), { str: 'test', nested: { str2: 'test2' } });
});
+
+ it('handles projections underneath arrays (gh-14680)', function() {
+ const obj = {
+ _id: 12,
+ testField: 'foo',
+ testArray: [{ _id: 42, field1: 'bar' }]
+ };
+
+ assert.deepEqual(applyProjection(obj, { 'testArray.field1': 1 }), { testArray: [{ field1: 'bar' }] });
+ assert.deepEqual(applyProjection(obj, { 'testArray.field1': 0, _id: 0 }), { testField: 'foo', testArray: [{ _id: 42 }] });
+ });
});
diff --git a/test/helpers/projection.isExclusive.test.js b/test/helpers/projection.isExclusive.test.js
new file mode 100644
index 00000000000..2fc4a16b990
--- /dev/null
+++ b/test/helpers/projection.isExclusive.test.js
@@ -0,0 +1,12 @@
+'use strict';
+
+const assert = require('assert');
+
+require('../common'); // required for side-effect setup (so that the default driver is set-up)
+const isExclusive = require('../../lib/helpers/projection/isExclusive');
+
+describe('isExclusive', function() {
+ it('handles $elemMatch (gh-14893)', function() {
+ assert.strictEqual(isExclusive({ field: { $elemMatch: { test: new Date('2024-06-01') } }, otherProp: 1 }), false);
+ });
+});
diff --git a/test/helpers/projection.isInclusive.test.js b/test/helpers/projection.isInclusive.test.js
new file mode 100644
index 00000000000..3bb93635a50
--- /dev/null
+++ b/test/helpers/projection.isInclusive.test.js
@@ -0,0 +1,12 @@
+'use strict';
+
+const assert = require('assert');
+
+require('../common'); // required for side-effect setup (so that the default driver is set-up)
+const isInclusive = require('../../lib/helpers/projection/isInclusive');
+
+describe('isInclusive', function() {
+ it('handles $elemMatch (gh-14893)', function() {
+ assert.strictEqual(isInclusive({ field: { $elemMatch: { test: new Date('2024-06-01') } }, otherProp: 1 }), true);
+ });
+});
diff --git a/test/helpers/query.cast$expr.test.js b/test/helpers/query.cast$expr.test.js
index 416aabdfe1d..fd3f7cd5bfe 100644
--- a/test/helpers/query.cast$expr.test.js
+++ b/test/helpers/query.cast$expr.test.js
@@ -108,4 +108,43 @@ describe('castexpr', function() {
const res = cast$expr({ $not: { $in: ['42', '$nums'] } }, testSchema);
assert.deepStrictEqual(res, { $not: { $in: [42, '$nums'] } });
});
+
+ it('casts $round (gh-13881)', function() {
+ const testSchema = new Schema({ value: Number });
+
+ let res = cast$expr({ $eq: [{ $round: ['$value', '00'] }, 2] }, testSchema);
+ assert.deepStrictEqual(res, { $eq: [{ $round: ['$value', 0] }, 2] });
+
+ res = cast$expr({ $eq: [{ $round: ['$value'] }, 2] }, testSchema);
+ assert.deepStrictEqual(res, { $eq: [{ $round: ['$value'] }, 2] });
+ });
+
+ it('casts $switch (gh-14751)', function() {
+ const testSchema = new Schema({
+ name: String,
+ scores: [Number]
+ });
+ const res = cast$expr({
+ $eq: [
+ {
+ $switch: {
+ branches: [{ case: { $eq: ['$$NOW', '$$NOW'] }, then: true }],
+ default: false
+ }
+ },
+ true
+ ]
+ }, testSchema);
+ assert.deepStrictEqual(res, {
+ $eq: [
+ {
+ $switch: {
+ branches: [{ case: { $eq: ['$$NOW', '$$NOW'] }, then: true }],
+ default: false
+ }
+ },
+ true
+ ]
+ });
+ });
});
diff --git a/test/helpers/query.castUpdate.test.js b/test/helpers/query.castUpdate.test.js
index 32c296c5779..091c996faab 100644
--- a/test/helpers/query.castUpdate.test.js
+++ b/test/helpers/query.castUpdate.test.js
@@ -12,4 +12,30 @@ describe('castUpdate', function() {
const res = castUpdate(schema, obj);
assert.deepEqual(res, { $addToSet: { test: [1, 2, 3] } });
});
+
+ it('casts the update correctly when target discriminator type is missing', function() {
+ const shapeSchema = Schema({ name: String }, { discriminatorKey: 'kind' });
+ const schema = Schema({ shape: shapeSchema });
+
+ schema
+ .path('shape')
+ .discriminator(
+ 'gh8378_Circle',
+ Schema({ radius: String, color: String })
+ );
+
+ // schema
+ // .path('shape')
+ // .discriminator('gh8378_Square', Schema({ side: Number, color: String }));
+
+ const toBeUpdated = {
+ 'shape.name': 'aName',
+ 'shape.kind': 'gh8378_Square',
+ 'shape.side': 4,
+ 'shape.color': 'white'
+ };
+
+ const res = castUpdate(schema, { $set: toBeUpdated });
+ assert.deepEqual(res, { $set: toBeUpdated });
+ });
});
diff --git a/test/helpers/query.selectPopulatedFields.test.js b/test/helpers/query.selectPopulatedFields.test.js
new file mode 100644
index 00000000000..4a5924b8c64
--- /dev/null
+++ b/test/helpers/query.selectPopulatedFields.test.js
@@ -0,0 +1,36 @@
+'use strict';
+
+const assert = require('assert');
+const selectPopulatedFields = require('../../lib/helpers/query/selectPopulatedFields');
+
+describe('selectPopulatedFields', function() {
+ it('selects refPath', function() {
+ const fields = { name: 1 };
+ const userProvidedFields = { name: 1 };
+ const populateOptions = {
+ parent: {
+ refPath: 'parentModel'
+ }
+ };
+ selectPopulatedFields(fields, userProvidedFields, populateOptions);
+ assert.deepStrictEqual(fields, {
+ name: 1,
+ parent: 1,
+ parentModel: 1
+ });
+ });
+
+ it('adds refPath to projection if not deselected by user in exclusive projection', function() {
+ const fields = { name: 0, parentModel: 0 };
+ const userProvidedFields = { name: 0 };
+ const populateOptions = {
+ parent: {
+ refPath: 'parentModel'
+ }
+ };
+ selectPopulatedFields(fields, userProvidedFields, populateOptions);
+ assert.deepStrictEqual(fields, {
+ name: 0
+ });
+ });
+});
diff --git a/test/helpers/schema.getSubdocumentStrictValue.test.js b/test/helpers/schema.getSubdocumentStrictValue.test.js
new file mode 100644
index 00000000000..8283af2c31b
--- /dev/null
+++ b/test/helpers/schema.getSubdocumentStrictValue.test.js
@@ -0,0 +1,70 @@
+'use strict';
+
+const assert = require('assert');
+const getSubdocumentStrictValue = require('../../lib/helpers/schema/getSubdocumentStrictValue');
+const { mongoose } = require('../common');
+
+describe('getSubdocumentStrictValue', function() {
+ beforeEach(() => mongoose.deleteModel(/Test/));
+ after(() => mongoose.deleteModel(/Test/));
+
+ it('returns top-level document if no subdocs', function() {
+ const schema = mongoose.Schema({ name: String });
+
+ assert.strictEqual(getSubdocumentStrictValue(schema, ['name']), undefined);
+ });
+
+ it('picks up single nested subdocs along the path', function() {
+ const schema = mongoose.Schema({
+ name: String,
+ nested: mongoose.Schema({
+ nestedPath: String
+ }, { strict: 'throw' })
+ });
+
+ assert.strictEqual(
+ getSubdocumentStrictValue(schema, ['nested', 'nestedPath']),
+ 'throw'
+ );
+ });
+
+ it('picks up document arrays', function() {
+ const schema = mongoose.Schema({
+ name: String,
+ docArr: [mongoose.Schema({
+ nestedPath: String
+ }, { strict: 'throw' })]
+ });
+
+ const res = getSubdocumentStrictValue(schema, ['docArr', '0', 'nestedPath']);
+ assert.strictEqual(res, 'throw');
+ });
+
+ it('picks up doubly nested subdocuments', function() {
+ const schema = mongoose.Schema({
+ name: String,
+ l1: mongoose.Schema({
+ l2: mongoose.Schema({
+ nested: String
+ }, { strict: false })
+ }, { strict: 'throw' })
+ });
+
+ const res = getSubdocumentStrictValue(schema, ['l1', 'l2', 'nested']);
+ assert.strictEqual(res, false);
+ });
+
+ it('picks up single nested subdocs under document arrays', function() {
+ const schema = mongoose.Schema({
+ name: String,
+ docArr: [mongoose.Schema({
+ nested: mongoose.Schema({
+ l3Path: String
+ }, { strict: 'throw' })
+ }, { strict: false })]
+ });
+
+ const res = getSubdocumentStrictValue(schema, ['docArr', '0', 'nested', 'l3Path']);
+ assert.strictEqual(res, 'throw');
+ });
+});
diff --git a/test/helpers/update.castArrayFilters.test.js b/test/helpers/update.castArrayFilters.test.js
index d132663b590..0db3c031c9d 100644
--- a/test/helpers/update.castArrayFilters.test.js
+++ b/test/helpers/update.castArrayFilters.test.js
@@ -311,4 +311,42 @@ describe('castArrayFilters', function() {
assert.strictEqual(q.options.arrayFilters[0]['element.number'], 1);
});
+
+ it('correctly casts array of strings underneath doc array (gh-12565)', function() {
+ const userSchema = new Schema({
+ groups: [{
+ document: 'ObjectId',
+ tags: [String]
+ }]
+ });
+
+ const q = new Query();
+ q.schema = userSchema;
+
+ const groupId = new Types.ObjectId();
+ const filter = {
+ groups: {
+ $elemMatch: {
+ document: groupId,
+ tags: 'tag-to-update'
+ }
+ }
+ };
+ const update = {
+ $set: {
+ 'groups.$[group].tags.$[tag]': 42
+ }
+ };
+ const opts = {
+ arrayFilters: [
+ { 'group.document': groupId },
+ { tag: { $eq: 'tag-to-update' } }
+ ]
+ };
+ q.updateOne(filter, update, opts);
+ castArrayFilters(q);
+ q._update = q._castUpdate(q._update, false);
+
+ assert.strictEqual(q.getUpdate().$set['groups.$[group].tags.$[tag]'], '42');
+ });
});
diff --git a/test/index.test.js b/test/index.test.js
index 6c6a5797943..cfbd644f1f3 100644
--- a/test/index.test.js
+++ b/test/index.test.js
@@ -7,7 +7,6 @@ const start = require('./common');
const assert = require('assert');
const random = require('./util').random;
const stream = require('stream');
-const { EventEmitter } = require('events');
const collection = 'blogposts_' + random();
@@ -91,6 +90,19 @@ describe('mongoose module:', function() {
await mongoose.disconnect();
});
+ it('should collect the args correctly gh-13364', async function() {
+ const util = require('util');
+ const mongoose = new Mongoose();
+ const conn = await mongoose.connect(start.uri);
+ let actual = '';
+ mongoose.set('debug', (collectionName, methodName, ...methodArgs) => {
+ actual = `${collectionName}.${methodName}(${util.inspect(methodArgs).slice(2, -2)})`;
+ });
+ const user = conn.connection.collection('User');
+ await user.findOne({ key: 'value' });
+ assert.equal('User.findOne({ key: \'value\' })', actual);
+ });
+
it('{g,s}etting options', function() {
const mongoose = new Mongoose();
@@ -203,7 +215,7 @@ describe('mongoose module:', function() {
mongoose.set('toJSON', { virtuals: true });
- const schema = new Schema({});
+ const schema = new mongoose.Schema({});
schema.virtual('foo').get(() => 42);
const M = mongoose.model('Test', schema);
@@ -213,7 +225,7 @@ describe('mongoose module:', function() {
assert.equal(doc.toJSON({ virtuals: false }).foo, void 0);
- const schema2 = new Schema({}, { toJSON: { virtuals: true } });
+ const schema2 = new mongoose.Schema({}, { toJSON: { virtuals: true } });
schema2.virtual('foo').get(() => 'bar');
const M2 = mongoose.model('Test2', schema2);
@@ -227,7 +239,7 @@ describe('mongoose module:', function() {
mongoose.set('toObject', { virtuals: true });
- const schema = new Schema({});
+ const schema = new mongoose.Schema({});
schema.virtual('foo').get(() => 42);
const M = mongoose.model('Test', schema);
@@ -417,6 +429,25 @@ describe('mongoose module:', function() {
return Promise.resolve();
});
+ it('global plugins with applyPluginsToChildSchemas (gh-13887)', function() {
+ const m = new Mongoose();
+ m.set('applyPluginsToChildSchemas', false);
+
+ const called = [];
+ m.plugin(function(s) {
+ called.push(s);
+ });
+
+ const schema = new m.Schema({
+ subdoc: new m.Schema({ name: String }),
+ arr: [new m.Schema({ name: String })]
+ });
+
+ m.model('Test', schema);
+ assert.equal(called.length, 1);
+ assert.ok(called.indexOf(schema) !== -1);
+ });
+
it('global plugins recompile schemas (gh-7572)', function() {
function helloPlugin(schema) {
schema.virtual('greeting').get(() => 'hello');
@@ -454,7 +485,7 @@ describe('mongoose module:', function() {
const M = mongoose.model('gh6760', schema);
- const doc = new M({ testId: 'length12str0', testNum: 123, mixed: {} });
+ const doc = new M({ testId: '0'.repeat(24), testNum: 123, mixed: {} });
assert.ok(doc.testId instanceof mongoose.Types.ObjectId);
assert.ok(doc.testNum instanceof mongoose.Types.Decimal128);
@@ -571,9 +602,7 @@ describe('mongoose module:', function() {
mong.connect(start.uri, options);
mong.connection.on('open', function() {
- mong.disconnect(function() {
- done();
- });
+ mong.disconnect().then(() => done()).catch(err => done(err));
});
});
@@ -729,7 +758,7 @@ describe('mongoose module:', function() {
});
it('isValidObjectId (gh-3823)', function() {
- assert.ok(mongoose.isValidObjectId('0123456789ab'));
+ assert.ok(!mongoose.isValidObjectId('0123456789ab'));
assert.ok(mongoose.isValidObjectId('5f5c2d56f6e911019ec2acdc'));
assert.ok(mongoose.isValidObjectId('608DE01F32B6A93BBA314159'));
assert.ok(mongoose.isValidObjectId(new mongoose.Types.ObjectId()));
@@ -821,7 +850,6 @@ describe('mongoose module:', function() {
assert.ok(mongoose.Schema.Types);
assert.equal(typeof mongoose.SchemaType, 'function');
assert.equal(typeof mongoose.Query, 'function');
- assert.equal(typeof mongoose.Promise, 'function');
assert.equal(typeof mongoose.Model, 'function');
assert.equal(typeof mongoose.Document, 'function');
assert.equal(typeof mongoose.Error, 'function');
@@ -911,7 +939,7 @@ describe('mongoose module:', function() {
const goodIdString = '1'.repeat(24);
assert.deepStrictEqual(mongoose.isValidObjectId(goodIdString), true);
const goodIdString2 = '1'.repeat(12);
- assert.deepStrictEqual(mongoose.isValidObjectId(goodIdString2), true);
+ assert.deepStrictEqual(mongoose.isValidObjectId(goodIdString2), false);
});
it('Allows a syncIndexes shorthand mongoose.syncIndexes (gh-10893)', async function() {
const m = new mongoose.Mongoose();
@@ -1052,46 +1080,6 @@ describe('mongoose module:', function() {
});
});
- describe('custom drivers', function() {
- it('can set custom driver (gh-11900)', async function() {
- const m = new mongoose.Mongoose();
-
- class Collection {
- findOne(filter, options, cb) {
- cb(null, { answer: 42 });
- }
- }
- class Connection extends EventEmitter {
- constructor(base) {
- super();
- this.base = base;
- this.models = {};
- }
-
- collection() {
- return new Collection();
- }
-
- openUri(uri, opts, callback) {
- this.readyState = mongoose.ConnectionStates.connected;
- callback();
- }
- }
- const driver = {
- Collection,
- getConnection: () => Connection
- };
-
- m.setDriver(driver);
-
- await m.connect();
-
- const Test = m.model('Test', m.Schema({ answer: Number }));
-
- const res = await Test.findOne();
- assert.deepEqual(res.toObject(), { answer: 42 });
- });
- });
describe('global id option', function() {
it('can disable the id virtual on schemas gh-11966', async function() {
const m = new mongoose.Mongoose();
@@ -1192,4 +1180,52 @@ describe('mongoose module:', function() {
}
});
});
+
+ describe('createInitialConnection (gh-8302)', function() {
+ let m;
+
+ beforeEach(function() {
+ m = new mongoose.Mongoose();
+ });
+
+ afterEach(async function() {
+ await m.disconnect();
+ });
+
+ it('should delete existing connection when setting createInitialConnection to false', function() {
+ assert.ok(m.connection);
+ m.set('createInitialConnection', false);
+ assert.strictEqual(m.connection, undefined);
+ });
+
+ it('should create connection when createConnection is called', function() {
+ m.set('createInitialConnection', false);
+ const conn = m.createConnection();
+ assert.equal(conn, m.connection);
+ });
+
+ it('should create a new connection automatically when connect() is called if no existing default connection', async function() {
+ assert.ok(m.connection);
+ m.set('createInitialConnection', false);
+ assert.strictEqual(m.connection, undefined);
+
+ await m.connect(start.uri);
+ assert.ok(m.connection);
+ });
+
+ it('should not delete default connection if it has models', async function() {
+ assert.ok(m.connection);
+ m.model('Test', new m.Schema({ name: String }));
+ m.set('createInitialConnection', false);
+ assert.ok(m.connection);
+ });
+
+ it('should not delete default connection if it is connected', async function() {
+ assert.ok(m.connection);
+ await m.connect(start.uri);
+ m.set('createInitialConnection', false);
+ assert.ok(m.connection);
+ assert.equal(m.connection.readyState, 1);
+ });
+ });
});
diff --git a/test/int32.test.js b/test/int32.test.js
new file mode 100644
index 00000000000..b05fa82305b
--- /dev/null
+++ b/test/int32.test.js
@@ -0,0 +1,537 @@
+'use strict';
+
+const assert = require('assert');
+const start = require('./common');
+const BSON = require('bson');
+const sinon = require('sinon');
+
+const mongoose = start.mongoose;
+const Schema = mongoose.Schema;
+
+const INT32_MAX = 0x7FFFFFFF;
+const INT32_MIN = -0x80000000;
+
+describe('Int32', function() {
+ beforeEach(() => mongoose.deleteModel(/Test/));
+
+ it('is a valid schema type', function() {
+ const schema = new Schema({
+ myInt32: Schema.Types.Int32
+ });
+ const Test = mongoose.model('Test', schema);
+
+ const doc = new Test({
+ myInt32: 13
+ });
+ assert.strictEqual(doc.myInt32, 13);
+ assert.equal(typeof doc.myInt32, 'number');
+ });
+
+ describe('supports the required property', function() {
+ it('when value is null', async function() {
+ const schema = new Schema({
+ int32: {
+ type: Schema.Types.Int32,
+ required: true
+ }
+ });
+ const Test = mongoose.model('Test', schema);
+
+ const doc = new Test({
+ int: null
+ });
+
+ const err = await doc.validate().then(() => null, err => err);
+ assert.ok(err);
+ assert.ok(err.errors['int32']);
+ assert.equal(err.errors['int32'].name, 'ValidatorError');
+ assert.equal(
+ err.errors['int32'].message,
+ 'Path `int32` is required.'
+ );
+ });
+ it('when value is non-null', async function() {
+ const schema = new Schema({
+ int32: {
+ type: Schema.Types.Int32,
+ required: true
+ }
+ });
+ const Test = mongoose.model('Test', schema);
+
+ const doc = new Test({
+ int: 3
+ });
+
+ const err = await doc.validate().then(() => null, err => err);
+ assert.ok(err);
+ assert.ok(err.errors['int32']);
+ assert.equal(err.errors['int32'].name, 'ValidatorError');
+ assert.equal(
+ err.errors['int32'].message,
+ 'Path `int32` is required.'
+ );
+ });
+ });
+
+ describe('special inputs', function() {
+ it('supports INT32_MIN as input', function() {
+ const schema = new Schema({
+ myInt: {
+ type: Schema.Types.Int32
+ }
+ });
+ const Test = mongoose.model('Test', schema);
+
+ const doc = new Test({
+ myInt: INT32_MIN
+ });
+ assert.strictEqual(doc.myInt, INT32_MIN);
+ });
+
+ it('supports INT32_MAX as input', function() {
+ const schema = new Schema({
+ myInt: {
+ type: Schema.Types.Int32
+ }
+ });
+ const Test = mongoose.model('Test', schema);
+
+ const doc = new Test({
+ myInt: INT32_MAX
+ });
+ assert.strictEqual(doc.myInt, INT32_MAX);
+ });
+
+ it('supports undefined as input', function() {
+ const schema = new Schema({
+ myInt: {
+ type: Schema.Types.Int32
+ }
+ });
+ const Test = mongoose.model('Test', schema);
+
+ const doc = new Test({
+ myInt: undefined
+ });
+ assert.strictEqual(doc.myInt, undefined);
+ });
+
+ it('supports null as input', function() {
+ const schema = new Schema({
+ myInt: {
+ type: Schema.Types.Int32
+ }
+ });
+ const Test = mongoose.model('Test', schema);
+
+ const doc = new Test({
+ myInt: null
+ });
+ assert.strictEqual(doc.myInt, null);
+ });
+ });
+
+ describe('valid casts', function() {
+ it('casts from string', function() {
+ const schema = new Schema({
+ myInt: {
+ type: Schema.Types.Int32
+ }
+ });
+ const Test = mongoose.model('Test', schema);
+
+ const doc = new Test({
+ myInt: '-42'
+ });
+ assert.strictEqual(doc.myInt, -42);
+ });
+
+ it('casts from number', function() {
+ const schema = new Schema({
+ myInt: Schema.Types.Int32
+ });
+ const Test = mongoose.model('Test', schema);
+
+ const doc = new Test({
+ myInt: '-997.0'
+ });
+ assert.strictEqual(doc.myInt, -997);
+ });
+
+ it('casts from bigint', function() {
+ const schema = new Schema({
+ myInt: Schema.Types.Int32
+ });
+ const Test = mongoose.model('Test', schema);
+
+ const doc = new Test({
+ myInt: -997n
+ });
+ assert.strictEqual(doc.myInt, -997);
+ });
+
+ it('casts from BSON.Int32', function() {
+ const schema = new Schema({
+ myInt: Schema.Types.Int32
+ });
+ const Test = mongoose.model('Test', schema);
+
+ const doc = new Test({
+ myInt: new BSON.Int32(-997)
+ });
+ assert.strictEqual(doc.myInt, -997);
+ });
+
+ describe('long', function() {
+ after(function() {
+ sinon.restore();
+ });
+
+ it('casts from BSON.Long provided its value is within bounds of Int32', function() {
+ const schema = new Schema({
+ myInt: Schema.Types.Int32
+ });
+ const Test = mongoose.model('Test', schema);
+
+ const doc = new Test({
+ myInt: BSON.Long.fromNumber(-997)
+ });
+ assert.strictEqual(doc.myInt, -997);
+ });
+
+ it('calls Long.toNumber when casting long', function() {
+ // this is a perf optimization, since long.toNumber() is faster than Number(long)
+ const schema = new Schema({
+ myInt: Schema.Types.Int32
+ });
+ const Test = mongoose.model('Test', schema);
+
+ sinon.stub(BSON.Long.prototype, 'toNumber').callsFake(function() {
+ return 2;
+ });
+
+ const doc = new Test({
+ myInt: BSON.Long.fromNumber(-997)
+ });
+
+ assert.strictEqual(doc.myInt, 2);
+ });
+ });
+
+ it('casts from BSON.Double provided its value is an integer', function() {
+ const schema = new Schema({
+ myInt: Schema.Types.Int32
+ });
+ const Test = mongoose.model('Test', schema);
+
+ const doc = new Test({
+ myInt: new BSON.Double(-997)
+ });
+ assert.strictEqual(doc.myInt, -997);
+ });
+
+ it('casts boolean true to 1', function() {
+ const schema = new Schema({
+ myInt: Schema.Types.Int32
+ });
+ const Test = mongoose.model('Test', schema);
+
+ const doc = new Test({
+ myInt: true
+ });
+ assert.strictEqual(doc.myInt, 1);
+ });
+
+ it('casts boolean false to 0', function() {
+ const schema = new Schema({
+ myInt: Schema.Types.Int32
+ });
+ const Test = mongoose.model('Test', schema);
+
+ const doc = new Test({
+ myInt: false
+ });
+ assert.strictEqual(doc.myInt, 0);
+ });
+
+ it('casts empty string to null', function() {
+ const schema = new Schema({
+ myInt: Schema.Types.Int32
+ });
+ const Test = mongoose.model('Test', schema);
+
+ const doc = new Test({
+ myInt: ''
+ });
+ assert.strictEqual(doc.myInt, null);
+ });
+
+ it('supports valueOf() function ', function() {
+ const schema = new Schema({
+ myInt: Schema.Types.Int32
+ });
+ const Test = mongoose.model('Test', schema);
+
+ const doc = new Test({
+ myInt: { a: 'random', b: { c: 'whatever' }, valueOf: () => 83 }
+ });
+ assert.strictEqual(doc.myInt, 83);
+ });
+ });
+
+ describe('cast errors', () => {
+ let Test;
+
+ beforeEach(function() {
+ const schema = new Schema({
+ myInt: Schema.Types.Int32
+ });
+ Test = mongoose.model('Test', schema);
+ });
+
+ describe('when a non-integer decimal input is provided to an Int32 field', () => {
+ it('throws a CastError upon validation', async() => {
+ const doc = new Test({
+ myInt: -42.4
+ });
+
+ assert.strictEqual(doc.myInt, undefined);
+ const err = await doc.validate().catch(e => e);
+ assert.ok(err);
+ assert.ok(err.errors['myInt']);
+ assert.equal(err.errors['myInt'].name, 'CastError');
+ assert.equal(
+ err.errors['myInt'].message,
+ 'Cast to Int32 failed for value "-42.4" (type number) at path "myInt"'
+ );
+ });
+ });
+
+ describe('when a non-numeric string is provided to an Int32 field', () => {
+ it('throws a CastError upon validation', async() => {
+ const doc = new Test({
+ myInt: 'helloworld'
+ });
+
+ assert.strictEqual(doc.myInt, undefined);
+ const err = await doc.validate().catch(e => e);
+ assert.ok(err);
+ assert.ok(err.errors['myInt']);
+ assert.equal(err.errors['myInt'].name, 'CastError');
+ assert.equal(
+ err.errors['myInt'].message,
+ 'Cast to Int32 failed for value "helloworld" (type string) at path "myInt"'
+ );
+ });
+ });
+
+ describe('when a non-integer decimal string is provided to an Int32 field', () => {
+ it('throws a CastError upon validation', async() => {
+ const doc = new Test({
+ myInt: '1.2'
+ });
+
+ assert.strictEqual(doc.myInt, undefined);
+ const err = await doc.validate().catch(e => e);
+ assert.ok(err);
+ assert.ok(err.errors['myInt']);
+ assert.equal(err.errors['myInt'].name, 'CastError');
+ assert.equal(
+ err.errors['myInt'].message,
+ 'Cast to Int32 failed for value "1.2" (type string) at path "myInt"'
+ );
+ });
+ });
+
+ describe('when NaN is provided to an Int32 field', () => {
+ it('throws a CastError upon validation', async() => {
+ const doc = new Test({
+ myInt: NaN
+ });
+
+ assert.strictEqual(doc.myInt, undefined);
+ const err = await doc.validate().catch(e => e);
+ assert.ok(err);
+ assert.ok(err.errors['myInt']);
+ assert.equal(err.errors['myInt'].name, 'CastError');
+ assert.equal(
+ err.errors['myInt'].message,
+ 'Cast to Int32 failed for value "NaN" (type number) at path "myInt"'
+ );
+ });
+ });
+
+ describe('when value above INT32_MAX is provided to an Int32 field', () => {
+ it('throws a CastError upon validation', async() => {
+ const doc = new Test({
+ myInt: INT32_MAX + 1
+ });
+
+ assert.strictEqual(doc.myInt, undefined);
+ const err = await doc.validate().catch(e => e);
+ assert.ok(err);
+ assert.ok(err.errors['myInt']);
+ assert.equal(err.errors['myInt'].name, 'CastError');
+ assert.equal(
+ err.errors['myInt'].message,
+ 'Cast to Int32 failed for value "2147483648" (type number) at path "myInt"'
+ );
+ });
+ });
+
+ describe('when value below INT32_MIN is provided to an Int32 field', () => {
+ it('throws a CastError upon validation', async() => {
+ const doc = new Test({
+ myInt: INT32_MIN - 1
+ });
+
+ assert.strictEqual(doc.myInt, undefined);
+ const err = await doc.validate().catch(e => e);
+ assert.ok(err);
+ assert.ok(err.errors['myInt']);
+ assert.equal(err.errors['myInt'].name, 'CastError');
+ assert.equal(
+ err.errors['myInt'].message,
+ 'Cast to Int32 failed for value "-2147483649" (type number) at path "myInt"'
+ );
+ });
+ });
+ });
+
+ describe('custom casters', () => {
+ const defaultCast = mongoose.Schema.Types.Int32.cast();
+
+ afterEach(() => {
+ mongoose.Schema.Types.Int32.cast(defaultCast);
+ });
+
+ it('supports cast disabled', async() => {
+ mongoose.Schema.Types.Int32.cast(false);
+ const schema = new Schema({
+ myInt1: {
+ type: Schema.Types.Int32
+ },
+ myInt2: {
+ type: Schema.Types.Int32
+ }
+ });
+ const Test = mongoose.model('Test', schema);
+ const doc = new Test({
+ myInt1: '52',
+ myInt2: 52
+ });
+ assert.strictEqual(doc.myInt1, undefined);
+ assert.strictEqual(doc.myInt2, 52);
+
+ const err = await doc.validate().catch(e => e);
+ assert.ok(err);
+ assert.ok(err.errors['myInt1']);
+ });
+
+ it('supports custom cast', () => {
+ mongoose.Schema.Types.Int32.cast(v => {
+ if (isNaN(v)) {
+ return 0;
+ }
+ return defaultCast(v);
+ });
+ const schema = new Schema({
+ myInt: {
+ type: Schema.Types.Int32
+ }
+ });
+
+ const Test = mongoose.model('Test', schema);
+ const doc = new Test({
+ myInt: NaN
+ });
+ assert.strictEqual(doc.myInt, 0);
+ });
+ });
+
+ describe('mongoDB integration', function() {
+ let db;
+ let Test;
+
+ before(async function() {
+ db = await start();
+
+ const schema = new Schema({
+ myInt: Schema.Types.Int32
+ });
+ db.deleteModel(/Test/);
+ Test = db.model('Test', schema);
+ });
+
+ after(async function() {
+ await db.close();
+ });
+
+ beforeEach(async() => {
+ await Test.deleteMany({});
+ });
+
+ describe('$type compatibility', function() {
+ it('is queryable as a JS number in MongoDB', async function() {
+ await Test.create({ myInt: '42' });
+ const doc = await Test.findOne({ myInt: { $type: 'number' } });
+ assert.ok(doc);
+ assert.strictEqual(doc.myInt, 42);
+ });
+
+ it('is queryable as a BSON Int32 in MongoDB', async function() {
+ await Test.create({ myInt: '42' });
+ const doc = await Test.findOne({ myInt: { $type: 'int' } });
+ assert.ok(doc);
+ assert.strictEqual(doc.myInt, 42);
+ });
+
+ it('is NOT queryable as a BSON Double in MongoDB', async function() {
+ await Test.create({ myInt: '42' });
+ const doc = await Test.findOne({ myInt: { $type: 'double' } });
+ assert.equal(doc, undefined);
+ });
+ });
+
+ it('can query with comparison operators', async function() {
+ await Test.create([
+ { myInt: 1 },
+ { myInt: 2 },
+ { myInt: 3 },
+ { myInt: 4 }
+ ]);
+
+ let docs = await Test.find({ myInt: { $gte: 3 } }).sort({ myInt: 1 });
+ assert.equal(docs.length, 2);
+ assert.deepStrictEqual(docs.map(doc => doc.myInt), [3, 4]);
+
+ docs = await Test.find({ myInt: { $lt: 3 } }).sort({ myInt: -1 });
+ assert.equal(docs.length, 2);
+ assert.deepStrictEqual(docs.map(doc => doc.myInt), [2, 1]);
+ });
+
+ it('supports populate()', async function() {
+ const parentSchema = new Schema({
+ child: {
+ type: Schema.Types.Int32,
+ ref: 'Child'
+ }
+ });
+ const childSchema = new Schema({
+ _id: Schema.Types.Int32,
+ name: String
+ });
+ const Parent = db.model('Parent', parentSchema);
+ const Child = db.model('Child', childSchema);
+
+ const { _id } = await Parent.create({ child: 42 });
+ await Child.create({ _id: 42, name: 'test-int32-populate' });
+
+ const doc = await Parent.findById(_id).populate('child');
+ assert.ok(doc);
+ assert.equal(doc.child.name, 'test-int32-populate');
+ assert.equal(doc.child._id, 42);
+ });
+ });
+});
diff --git a/test/mocha-fixtures.js b/test/mocha-fixtures.js
index bc4190b0d71..8f12be8c1f0 100644
--- a/test/mocha-fixtures.js
+++ b/test/mocha-fixtures.js
@@ -20,13 +20,14 @@ let mongorreplset;
// decide whether to start MMS instances or use the existing URI's
const startMemoryInstance = !process.env.MONGOOSE_TEST_URI;
-const startMemoryReplset = !process.env.MONGOOSE_REPLSET_URI;
+const startMemoryReplset = !process.env.MONGOOSE_REPLSET_URI && !!process.env.START_REPLICA_SET;
module.exports.mochaGlobalSetup = async function mochaGlobalSetup() {
let instanceuri;
let replseturi;
process.env.RUNTIME_DOWNLOAD = '1'; // ensure MMS is able to download binaries in this context
+ process.env.MONGOMS_MD5_CHECK = '1';
// set some options when running in a CI
if (process.env.CI) {
diff --git a/test/model.aggregate.test.js b/test/model.aggregate.test.js
index 7dd31f35fa2..56cdf88fd5b 100644
--- a/test/model.aggregate.test.js
+++ b/test/model.aggregate.test.js
@@ -102,7 +102,7 @@ describe('model aggregate', function() {
const res = await promise;
- assert.ok(promise instanceof mongoose.Promise);
+ assert.ok(promise instanceof Promise);
assert.ok(res);
assert.equal(res.length, 1);
assert.ok('maxAge' in res[0]);
@@ -113,10 +113,6 @@ describe('model aggregate', function() {
assert(A.aggregate([project]) instanceof Aggregate);
});
- it('throws when passing object (gh-6732)', function() {
- assert.throws(() => A.aggregate({}), /disallows passing a spread/);
- });
-
it('can use helper for $out', async function() {
if (!mongo26_or_greater) {
return;
diff --git a/test/model.create.test.js b/test/model.create.test.js
index 455373e8555..d587e70ae16 100644
--- a/test/model.create.test.js
+++ b/test/model.create.test.js
@@ -48,20 +48,14 @@ describe('model', function() {
assert.equal(post2.title, 'bye');
});
- it('fires callback when passed 0 docs', function(done) {
- B.create(function(err, a) {
- assert.ifError(err);
- assert.ok(!a);
- done();
- });
+ it('fires callback when passed 0 docs', async function() {
+ const docs = await B.create();
+ assert.strictEqual(docs, null);
});
- it('fires callback when empty array passed', function(done) {
- B.create([], function(err, a) {
- assert.ifError(err);
- assert.deepEqual(a, []);
- done();
- });
+ it('fires callback when empty array passed', async function() {
+ const a = await B.create([]);
+ assert.deepEqual(a, []);
});
it('supports passing options', async function() {
@@ -73,7 +67,7 @@ describe('model', function() {
it('returns a promise', function() {
const p = B.create({ title: 'returns promise' });
- assert.ok(p instanceof mongoose.Promise);
+ assert.ok(p instanceof Promise);
});
it('creates in parallel', async function() {
@@ -157,12 +151,6 @@ describe('model', function() {
assert(err);
});
- it('if callback is falsy, will ignore it (gh-5061)', async function() {
- const doc = await B.create({ title: 'test' }, null);
-
- assert.equal(doc.title, 'test');
- });
-
it('when passed an empty array, returns an empty array', async function() {
const userSchema = new Schema({ name: String });
const User = db.model('User', userSchema);
@@ -178,6 +166,97 @@ describe('model', function() {
assert.ok(doc._id);
});
});
+
+ it('ignores undefined last arg (gh-13487)', async function() {
+ await B.deleteMany({});
+ await B.create({ title: 'foo' }, void 0);
+ const docs = await B.find();
+ assert.equal(docs.length, 1);
+ assert.equal(docs[0].title, 'foo');
+ });
+ });
+ describe('ordered', function() {
+ it('runs the document insertion in a series when using the ordered option gh-4038', async function() {
+ const countSchema = new Schema({ n: Number });
+ const testSchema = new Schema({ name: { type: String, unique: true }, reference: Number });
+
+ const Count = db.model('gh4038', countSchema);
+
+ testSchema.pre('save', async function(next) {
+ const doc = await Count.findOneAndUpdate({}, { $inc: { n: 1 } }, { new: true, upsert: true });
+ this.reference = doc.n;
+ next();
+ });
+
+ const Test = db.model('gh4038Test', testSchema);
+ const data = [];
+ for (let i = 0; i < 11; i++) {
+ data.push({ name: 'Test' + Math.abs(i - 4) });
+ }
+ await Test.create(data, { ordered: true }).catch(err => err);
+ const docs = await Test.find();
+ assert.equal(docs.length, 5);
+ });
+ it('should throw an error only after all the documents have finished saving gh-4628', async function() {
+ const testSchema = new Schema({ name: { type: String, unique: true } });
+
+
+ const Test = db.model('gh4628Test', testSchema);
+ await Test.init();
+ const data = [];
+ for (let i = 0; i < 11; i++) {
+ data.push({ name: 'Test' + Math.abs(i - 4) });
+ }
+ await Test.create(data, { ordered: false }).catch(err => err);
+ const docs = await Test.find();
+ assert.equal(docs.length, 7); // docs 1,2,3,4 should not go through 11-4 == 7
+ });
+
+ it('should return the first error immediately if "aggregateErrors" is not explicitly set (ordered)', async function() {
+ const testSchema = new Schema({ name: { type: String, required: true } });
+
+ const TestModel = db.model('gh1731-1', testSchema);
+
+ const res = await TestModel.create([{ name: 'test' }, {}, { name: 'another' }], { ordered: true }).then(null).catch(err => err);
+
+ assert.ok(res instanceof mongoose.Error.ValidationError);
+ });
+
+ it('should not return errors immediately if "aggregateErrors" is "true" (ordered)', async function() {
+ const testSchema = new Schema({ name: { type: String, required: true } });
+
+ const TestModel = db.model('gh1731-2', testSchema);
+
+ const res = await TestModel.create([{ name: 'test' }, {}, { name: 'another' }], { ordered: true, aggregateErrors: true });
+
+ assert.equal(res.length, 3);
+ assert.ok(res[0] instanceof mongoose.Document);
+ assert.ok(res[1] instanceof mongoose.Error.ValidationError);
+ assert.ok(res[2] instanceof mongoose.Document);
+ });
+ });
+
+ it('should return the first error immediately if "aggregateErrors" is not explicitly set', async function() {
+ const testSchema = new Schema({ name: { type: String, required: true } });
+
+ const TestModel = db.model('gh1731-3', testSchema);
+
+ const res = await TestModel.create([{ name: 'test' }, {}, { name: 'another' }], {}).then(null).catch(err => err);
+
+ assert.ok(res instanceof mongoose.Error.ValidationError);
+ });
+
+ it('should not return errors immediately if "aggregateErrors" is "true"', async function() {
+ const testSchema = new Schema({ name: { type: String, required: true } });
+
+ const TestModel = db.model('gh1731-4', testSchema);
+
+ const res = await TestModel.create([{ name: 'test' }, {}, { name: 'another' }], { aggregateErrors: true });
+
+ assert.equal(res.length, 3);
+ assert.ok(res[0] instanceof mongoose.Document);
+ assert.ok(res[1] instanceof mongoose.Error.ValidationError);
+ assert.ok(res[2] instanceof mongoose.Document);
});
});
});
diff --git a/test/model.discriminator.querying.test.js b/test/model.discriminator.querying.test.js
index e61c0a9d01f..5c827dc3e4a 100644
--- a/test/model.discriminator.querying.test.js
+++ b/test/model.discriminator.querying.test.js
@@ -210,110 +210,91 @@ describe('model', function() {
});
describe('using "ModelDiscriminator#find"', function() {
- it('to find documents of the appropriate discriminator', function(done) {
+ it('to find documents of the appropriate discriminator', async function() {
const impressionEvent = new ImpressionEvent({ name: 'Impression event' });
const conversionEvent1 = new ConversionEvent({ name: 'Conversion event 1', revenue: 1 });
const conversionEvent2 = new ConversionEvent({ name: 'Conversion event 2', revenue: 2 });
- impressionEvent.save(function(err) {
- assert.ifError(err);
- conversionEvent1.save(function(err) {
- assert.ifError(err);
- conversionEvent2.save(function(err) {
- assert.ifError(err);
- // doesn't find anything since we're querying for an impression id
- const query = ConversionEvent.find({ _id: impressionEvent._id });
- assert.equal(query.op, 'find');
- assert.deepEqual(query._conditions, { _id: impressionEvent._id, __t: 'Conversion' });
- query.exec(function(err, documents) {
- assert.ifError(err);
- assert.equal(documents.length, 0);
-
- // now find one with no criteria given and ensure it gets added to _conditions
- const query = ConversionEvent.find();
- assert.deepEqual(query._conditions, { __t: 'Conversion' });
- assert.equal(query.op, 'find');
- query.exec(function(err, documents) {
- assert.ifError(err);
- assert.equal(documents.length, 2);
-
- assert.ok(documents[0] instanceof ConversionEvent);
- assert.equal(documents[0].__t, 'Conversion');
-
- assert.ok(documents[1] instanceof ConversionEvent);
- assert.equal(documents[1].__t, 'Conversion');
-
- done();
- });
- });
- });
- });
- });
+ await impressionEvent.save();
+ await conversionEvent1.save();
+ await conversionEvent2.save();
+
+ // doesn't find anything since we're querying for an impression id
+ const query = ConversionEvent.find({ _id: impressionEvent._id });
+ assert.equal(query.op, 'find');
+ assert.deepEqual(query._conditions, { _id: impressionEvent._id, __t: 'Conversion' });
+ const documents = await query.exec();
+ assert.equal(documents.length, 0);
+
+ // now find one with no criteria given and ensure it gets added to _conditions
+ const query2 = ConversionEvent.find();
+ assert.deepEqual(query2._conditions, { __t: 'Conversion' });
+ assert.equal(query2.op, 'find');
+ const documents2 = await query2.exec();
+ assert.equal(documents2.length, 2);
+
+ assert.ok(documents2[0] instanceof ConversionEvent);
+ assert.equal(documents2[0].__t, 'Conversion');
+
+ assert.ok(documents2[1] instanceof ConversionEvent);
+ assert.equal(documents2[1].__t, 'Conversion');
});
});
});
- const checkDiscriminatorModelsFindDocumentsOfItsType = function(fields, done) {
+ const checkDiscriminatorModelsFindDocumentsOfItsType = async function(fields) {
const impressionEvent = new ImpressionEvent({ name: 'Impression event' });
const conversionEvent1 = new ConversionEvent({ name: 'Conversion event 1', revenue: 1 });
const conversionEvent2 = new ConversionEvent({ name: 'Conversion event 2', revenue: 2 });
- impressionEvent.save(function(err) {
- assert.ifError(err);
- conversionEvent1.save(function(err) {
- assert.ifError(err);
- conversionEvent2.save(function(err) {
- assert.ifError(err);
- // doesn't find anything since we're querying for an impression id
- const query = ConversionEvent.find({ _id: impressionEvent._id }, fields);
- assert.equal(query.op, 'find');
- assert.deepEqual(query._conditions, { _id: impressionEvent._id, __t: 'Conversion' });
- query.exec(function(err, documents) {
- assert.ifError(err);
- assert.equal(documents.length, 0);
-
- // now find one with no criteria given and ensure it gets added to _conditions
- const query = ConversionEvent.find({}, fields);
- assert.deepEqual(query._conditions, { __t: 'Conversion' });
- assert.equal(query.op, 'find');
- query.exec(function(err, documents) {
- assert.ifError(err);
- assert.equal(documents.length, 2);
-
- assert.ok(documents[0] instanceof ConversionEvent);
- assert.equal(documents[0].__t, 'Conversion');
-
- assert.ok(documents[1] instanceof ConversionEvent);
- assert.equal(documents[1].__t, 'Conversion');
- done();
- });
- });
- });
- });
- });
+ await impressionEvent.save();
+ await conversionEvent1.save();
+ await conversionEvent2.save();
+
+ // doesn't find anything since we're querying for an impression id
+ const query = ConversionEvent.find({ _id: impressionEvent._id }, fields);
+ assert.equal(query.op, 'find');
+ assert.deepEqual(query._conditions, { _id: impressionEvent._id, __t: 'Conversion' });
+ const documents = await query.exec();
+
+ assert.equal(documents.length, 0);
+
+ // now find one with no criteria given and ensure it gets added to _conditions
+ const query2 = ConversionEvent.find({}, fields);
+ assert.deepEqual(query2._conditions, { __t: 'Conversion' });
+ assert.equal(query2.op, 'find');
+ const documents2 = await query2.exec();
+
+ assert.equal(documents2.length, 2);
+
+ assert.ok(documents2[0] instanceof ConversionEvent);
+ assert.equal(documents2[0].__t, 'Conversion');
+
+ assert.ok(documents2[1] instanceof ConversionEvent);
+ assert.equal(documents2[1].__t, 'Conversion');
};
- it('discriminator model only finds documents of its type when fields selection set as string inclusive', function(done) {
- checkDiscriminatorModelsFindDocumentsOfItsType('name', done);
+ it('discriminator model only finds documents of its type when fields selection set as string inclusive', function() {
+ return checkDiscriminatorModelsFindDocumentsOfItsType('name');
});
- it('discriminator model only finds documents of its type when fields selection set as string exclusive', function(done) {
- checkDiscriminatorModelsFindDocumentsOfItsType('-revenue', done);
+ it('discriminator model only finds documents of its type when fields selection set as string exclusive', function() {
+ return checkDiscriminatorModelsFindDocumentsOfItsType('-revenue');
});
- it('discriminator model only finds documents of its type when fields selection set as empty string', function(done) {
- checkDiscriminatorModelsFindDocumentsOfItsType('', done);
+ it('discriminator model only finds documents of its type when fields selection set as empty string', function() {
+ return checkDiscriminatorModelsFindDocumentsOfItsType('');
});
- it('discriminator model only finds documents of its type when fields selection set as object inclusive', function(done) {
- checkDiscriminatorModelsFindDocumentsOfItsType({ name: 1 }, done);
+ it('discriminator model only finds documents of its type when fields selection set as object inclusive', function() {
+ return checkDiscriminatorModelsFindDocumentsOfItsType({ name: 1 });
});
- it('discriminator model only finds documents of its type when fields selection set as object exclusive', function(done) {
- checkDiscriminatorModelsFindDocumentsOfItsType({ revenue: 0 }, done);
+ it('discriminator model only finds documents of its type when fields selection set as object exclusive', function() {
+ return checkDiscriminatorModelsFindDocumentsOfItsType({ revenue: 0 });
});
- it('discriminator model only finds documents of its type when fields selection set as empty object', function(done) {
- checkDiscriminatorModelsFindDocumentsOfItsType({}, done);
+ it('discriminator model only finds documents of its type when fields selection set as empty object', function() {
+ return checkDiscriminatorModelsFindDocumentsOfItsType({});
});
});
@@ -456,174 +437,137 @@ describe('model', function() {
await checkHydratesCorrectModels({});
});
- it('discriminator model only finds a document of its type', function(done) {
+ it('discriminator model only finds a document of its type', async function() {
const impressionEvent = new ImpressionEvent({ name: 'Impression event' });
const conversionEvent = new ConversionEvent({ name: 'Conversion event', revenue: 2 });
- impressionEvent.save(function(err) {
- assert.ifError(err);
- conversionEvent.save(function(err) {
- assert.ifError(err);
- // doesn't find anything since we're querying for an impression id
- const query = ConversionEvent.findOne({ _id: impressionEvent._id });
- assert.equal(query.op, 'findOne');
- assert.deepEqual(query._conditions, { _id: impressionEvent._id, __t: 'Conversion' });
+ await impressionEvent.save();
+ await conversionEvent.save();
- query.exec(function(err, document) {
- assert.ifError(err);
- assert.equal(document, null);
-
- // now find one with no criteria given and ensure it gets added to _conditions
- const query = ConversionEvent.findOne();
- assert.equal(query.op, 'findOne');
- assert.deepEqual(query._conditions, { __t: 'Conversion' });
-
- query.exec(function(err, document) {
- assert.ifError(err);
- assert.ok(document instanceof ConversionEvent);
- assert.equal(document.__t, 'Conversion');
- done();
- });
- });
- });
- });
+ // doesn't find anything since we're querying for an impression id
+ const query = ConversionEvent.findOne({ _id: impressionEvent._id });
+ assert.equal(query.op, 'findOne');
+ assert.deepEqual(query._conditions, { _id: impressionEvent._id, __t: 'Conversion' });
+
+ const document = await query.exec();
+ assert.equal(document, null);
+
+ // now find one with no criteria given and ensure it gets added to _conditions
+ const query2 = ConversionEvent.findOne();
+ assert.equal(query2.op, 'findOne');
+ assert.deepEqual(query2._conditions, { __t: 'Conversion' });
+
+ const document2 = await query2.exec();
+ assert.ok(document2 instanceof ConversionEvent);
+ assert.equal(document2.__t, 'Conversion');
});
- const checkDiscriminatorModelsFindOneDocumentOfItsType = function(fields, done) {
+ const checkDiscriminatorModelsFindOneDocumentOfItsType = async function(fields) {
const impressionEvent = new ImpressionEvent({ name: 'Impression event' });
const conversionEvent = new ConversionEvent({ name: 'Conversion event', revenue: 2 });
- impressionEvent.save(function(err) {
- assert.ifError(err);
- conversionEvent.save(function(err) {
- assert.ifError(err);
- // doesn't find anything since we're querying for an impression id
- const query = ConversionEvent.findOne({ _id: impressionEvent._id }, fields);
- assert.equal(query.op, 'findOne');
- assert.deepEqual(query._conditions, { _id: impressionEvent._id, __t: 'Conversion' });
+ await impressionEvent.save();
+ await conversionEvent.save();
- query.exec(function(err, document) {
- assert.ifError(err);
- assert.equal(document, null);
-
- // now find one with no criteria given and ensure it gets added to _conditions
- const query = ConversionEvent.findOne({}, fields);
- assert.equal(query.op, 'findOne');
- assert.deepEqual(query._conditions, { __t: 'Conversion' });
-
- query.exec(function(err, document) {
- assert.ifError(err);
- assert.ok(document instanceof ConversionEvent);
- assert.equal(document.__t, 'Conversion');
- done();
- });
- });
- });
- });
+ // doesn't find anything since we're querying for an impression id
+ const query = ConversionEvent.findOne({ _id: impressionEvent._id }, fields);
+ assert.equal(query.op, 'findOne');
+ assert.deepEqual(query._conditions, { _id: impressionEvent._id, __t: 'Conversion' });
+
+ const document = await query.exec();
+ assert.equal(document, null);
+
+ // now find one with no criteria given and ensure it gets added to _conditions
+ const query2 = ConversionEvent.findOne({}, fields);
+ assert.equal(query2.op, 'findOne');
+ assert.deepEqual(query2._conditions, { __t: 'Conversion' });
+
+ const document2 = await query2.exec();
+ assert.ok(document2 instanceof ConversionEvent);
+ assert.equal(document2.__t, 'Conversion');
};
- it('discriminator model only finds a document of its type when fields selection set as string inclusive', function(done) {
- checkDiscriminatorModelsFindOneDocumentOfItsType('name', done);
+ it('discriminator model only finds a document of its type when fields selection set as string inclusive', function() {
+ return checkDiscriminatorModelsFindOneDocumentOfItsType('name');
});
- it('discriminator model only finds a document of its type when fields selection set as string exclusive', function(done) {
- checkDiscriminatorModelsFindOneDocumentOfItsType('-revenue', done);
+ it('discriminator model only finds a document of its type when fields selection set as string exclusive', function() {
+ return checkDiscriminatorModelsFindOneDocumentOfItsType('-revenue');
});
- it('discriminator model only finds a document of its type when fields selection set as empty string', function(done) {
- checkDiscriminatorModelsFindOneDocumentOfItsType('', done);
+ it('discriminator model only finds a document of its type when fields selection set as empty string', function() {
+ return checkDiscriminatorModelsFindOneDocumentOfItsType('');
});
- it('discriminator model only finds a document of its type when fields selection set as object inclusive', function(done) {
- checkDiscriminatorModelsFindOneDocumentOfItsType({ name: 1 }, done);
+ it('discriminator model only finds a document of its type when fields selection set as object inclusive', function() {
+ return checkDiscriminatorModelsFindOneDocumentOfItsType({ name: 1 });
});
- it('discriminator model only finds a document of its type when fields selection set as object exclusive', function(done) {
- checkDiscriminatorModelsFindOneDocumentOfItsType({ revenue: 0 }, done);
+ it('discriminator model only finds a document of its type when fields selection set as object exclusive', function() {
+ return checkDiscriminatorModelsFindOneDocumentOfItsType({ revenue: 0 });
});
- it('discriminator model only finds a document of its type when fields selection set as empty object', function(done) {
- checkDiscriminatorModelsFindOneDocumentOfItsType({}, done);
+ it('discriminator model only finds a document of its type when fields selection set as empty object', function() {
+ return checkDiscriminatorModelsFindOneDocumentOfItsType({});
});
});
describe('findOneAndUpdate', function() {
- it('does not update models of other types', function(done) {
+ it('does not update models of other types', async function() {
const baseEvent = new BaseEvent({ name: 'Base event' });
const impressionEvent = new ImpressionEvent({ name: 'Impression event' });
const conversionEvent = new ConversionEvent({ name: 'Conversion event', revenue: 1.337 });
- baseEvent.save(function(err) {
- assert.ifError(err);
- impressionEvent.save(function(err) {
- assert.ifError(err);
- conversionEvent.save(function(err) {
- assert.ifError(err);
- const query = ConversionEvent.findOneAndUpdate({ name: 'Impression event' }, { $set: { name: 'Impression event - updated' } });
- assert.deepEqual(query._conditions, { name: 'Impression event', __t: 'Conversion' });
- query.exec(function(err, document) {
- assert.ifError(err);
- assert.equal(document, null);
- done();
- });
- });
- });
- });
+ await baseEvent.save();
+ await impressionEvent.save();
+ await conversionEvent.save();
+
+ const query = ConversionEvent.findOneAndUpdate({ name: 'Impression event' }, { $set: { name: 'Impression event - updated' } });
+ assert.deepEqual(query._conditions, { name: 'Impression event', __t: 'Conversion' });
+ const document = await query.exec();
+ assert.equal(document, null);
});
- it('updates models of its own type', function(done) {
+
+ it('updates models of its own type', async function() {
const baseEvent = new BaseEvent({ name: 'Base event' });
const impressionEvent = new ImpressionEvent({ name: 'Impression event' });
const conversionEvent = new ConversionEvent({ name: 'Conversion event', revenue: 1.337 });
- baseEvent.save(function(err) {
- assert.ifError(err);
- impressionEvent.save(function(err) {
- assert.ifError(err);
- conversionEvent.save(function(err) {
- assert.ifError(err);
- const query = ConversionEvent.findOneAndUpdate({ name: 'Conversion event' }, { $set: { name: 'Conversion event - updated' } }, { new: true });
- assert.deepEqual(query._conditions, { name: 'Conversion event', __t: 'Conversion' });
- query.exec(function(err, document) {
- assert.ifError(err);
- const expected = conversionEvent.toJSON();
- expected.name = 'Conversion event - updated';
- assert.deepEqual(document.toJSON(), expected);
- done();
- });
- });
- });
- });
+ await baseEvent.save();
+ await impressionEvent.save();
+ await conversionEvent.save();
+
+ const query = ConversionEvent.findOneAndUpdate({ name: 'Conversion event' }, { $set: { name: 'Conversion event - updated' } }, { new: true });
+ assert.deepEqual(query._conditions, { name: 'Conversion event', __t: 'Conversion' });
+
+ const document = await query.exec();
+
+ const expected = conversionEvent.toJSON();
+ expected.name = 'Conversion event - updated';
+ assert.deepEqual(document.toJSON(), expected);
});
- it('base model modifies any event type', function(done) {
+ it('base model modifies any event type', async function() {
const baseEvent = new BaseEvent({ name: 'Base event' });
const impressionEvent = new ImpressionEvent({ name: 'Impression event' });
const conversionEvent = new ConversionEvent({ name: 'Conversion event', revenue: 1.337 });
- baseEvent.save(function(err) {
- assert.ifError(err);
- impressionEvent.save(function(err) {
- assert.ifError(err);
- conversionEvent.save(function(err) {
- assert.ifError(err);
- const query = BaseEvent.findOneAndUpdate({ name: 'Conversion event' }, { $set: { name: 'Conversion event - updated' } }, { new: true });
- assert.deepEqual(query._conditions, { name: 'Conversion event' });
- query.exec(function(err, document) {
- assert.ifError(err);
- const expected = conversionEvent.toJSON();
- expected.name = 'Conversion event - updated';
- assert.deepEqual(document.toJSON(), expected);
- done();
- });
- });
- });
- });
+ await baseEvent.save();
+ await impressionEvent.save();
+ await conversionEvent.save();
+
+ const query = BaseEvent.findOneAndUpdate({ name: 'Conversion event' }, { $set: { name: 'Conversion event - updated' } }, { new: true });
+ assert.deepEqual(query._conditions, { name: 'Conversion event' });
+ const document = await query.exec();
+ const expected = conversionEvent.toJSON();
+ expected.name = 'Conversion event - updated';
+ assert.deepEqual(document.toJSON(), expected);
});
});
describe('population/reference mapping', function() {
- it('populates and hydrates correct models', function(done) {
+ it('populates and hydrates correct models', async function() {
const vehicleSchema = new Schema();
const carSchema = new Schema({ speed: Number });
const busSchema = new Schema({ speed: Number });
@@ -639,50 +583,42 @@ describe('model', function() {
const Bus = Vehicle.discriminator('Bus', busSchema);
const User = db.model('User', userSchema);
- Vehicle.create({}, function(err, vehicle) {
- assert.ifError(err);
- Car.create({ speed: 160 }, function(err, car) {
- Bus.create({ speed: 80 }, function(err, bus) {
- assert.ifError(err);
- User.create({ vehicles: [vehicle._id, car._id, bus._id], favoriteVehicle: car._id, favoriteBus: bus._id }, function(err) {
- assert.ifError(err);
- User.findOne({}).populate('vehicles favoriteVehicle favoriteBus').exec(function(err, user) {
- assert.ifError(err);
-
- const expected = {
- __v: 0,
- _id: user._id,
- vehicles: [
- { _id: vehicle._id, __v: 0 },
- { _id: car._id, speed: 160, __v: 0, __t: 'Car' },
- { _id: bus._id, speed: 80, __v: 0, __t: 'Bus' }
- ],
- favoriteVehicle: { _id: car._id, speed: 160, __v: 0, __t: 'Car' },
- favoriteBus: { _id: bus._id, speed: 80, __v: 0, __t: 'Bus' }
- };
-
- assert.deepEqual(user.toJSON(), expected);
- assert.ok(user.vehicles[0] instanceof Vehicle);
- assert.ok(!(user.vehicles[0] instanceof Car));
- assert.ok(!(user.vehicles[0] instanceof Bus));
-
- assert.ok(user.vehicles[1] instanceof Car);
- assert.ok(!(user.vehicles[1] instanceof Bus));
-
- assert.ok(user.vehicles[2] instanceof Bus);
- assert.ok(!(user.vehicles[2] instanceof Car));
-
- assert.ok(user.favoriteVehicle instanceof Car);
- assert.ok(user.favoriteBus instanceof Bus);
- done();
- });
- });
- });
- });
- });
+ const vehicle = await Vehicle.create({});
+ const car = await Car.create({ speed: 160 });
+ const bus = await Bus.create({ speed: 80 });
+
+ await User.create({ vehicles: [vehicle._id, car._id, bus._id], favoriteVehicle: car._id, favoriteBus: bus._id });
+
+ const user = await User.findOne({}).populate('vehicles favoriteVehicle favoriteBus').exec();
+
+ const expected = {
+ __v: 0,
+ _id: user._id,
+ vehicles: [
+ { _id: vehicle._id, __v: 0 },
+ { _id: car._id, speed: 160, __v: 0, __t: 'Car' },
+ { _id: bus._id, speed: 80, __v: 0, __t: 'Bus' }
+ ],
+ favoriteVehicle: { _id: car._id, speed: 160, __v: 0, __t: 'Car' },
+ favoriteBus: { _id: bus._id, speed: 80, __v: 0, __t: 'Bus' }
+ };
+
+ assert.deepEqual(user.toJSON(), expected);
+ assert.ok(user.vehicles[0] instanceof Vehicle);
+ assert.ok(!(user.vehicles[0] instanceof Car));
+ assert.ok(!(user.vehicles[0] instanceof Bus));
+
+ assert.ok(user.vehicles[1] instanceof Car);
+ assert.ok(!(user.vehicles[1] instanceof Bus));
+
+ assert.ok(user.vehicles[2] instanceof Bus);
+ assert.ok(!(user.vehicles[2] instanceof Car));
+
+ assert.ok(user.favoriteVehicle instanceof Car);
+ assert.ok(user.favoriteBus instanceof Bus);
});
- it('reference in child schemas (gh-2719)', function(done) {
+ it('reference in child schemas (gh-2719)', async function() {
const vehicleSchema = new Schema({});
const carSchema = new Schema({
speed: Number,
@@ -703,27 +639,18 @@ describe('model', function() {
const Bus = Vehicle.discriminator('Bus', busSchema);
const Garage = db.model('Test', garageSchema);
- Garage.create({ name: 'My', num_of_places: 3 }, function(err, garage) {
- assert.ifError(err);
- Car.create({ speed: 160, garage: garage }, function(err) {
- assert.ifError(err);
- Bus.create({ speed: 80, garage: garage }, function(err) {
- assert.ifError(err);
- Vehicle.find({}).populate('garage').exec(function(err, vehicles) {
- assert.ifError(err);
-
- vehicles.forEach(function(v) {
- assert.ok(v.garage instanceof Garage);
- });
-
- done();
- });
- });
- });
+ const garage = await Garage.create({ name: 'My', num_of_places: 3 });
+ await Car.create({ speed: 160, garage });
+ await Bus.create({ speed: 80, garage });
+
+ const vehicles = await Vehicle.find({}).populate('garage');
+
+ vehicles.forEach(function(v) {
+ assert.ok(v.garage instanceof Garage);
});
});
- it('populates parent array reference (gh-4643)', function(done) {
+ it('populates parent array reference (gh-4643)', async function() {
const vehicleSchema = new Schema({
wheels: [{
type: Schema.Types.ObjectId,
@@ -737,25 +664,18 @@ describe('model', function() {
const Bus = Vehicle.discriminator('Bus', busSchema);
const Wheel = db.model('Test', wheelSchema);
- Wheel.create({ brand: 'Rotiform' }, function(err, wheel) {
- assert.ifError(err);
- Bus.create({ speed: 80, wheels: [wheel] }, function(err) {
- assert.ifError(err);
- Bus.findOne({}).populate('wheels').exec(function(err, bus) {
- assert.ifError(err);
-
- assert.ok(bus instanceof Vehicle);
- assert.ok(bus instanceof Bus);
- assert.equal(bus.wheels.length, 1);
- assert.ok(bus.wheels[0] instanceof Wheel);
- assert.equal(bus.wheels[0].brand, 'Rotiform');
- done();
- });
- });
- });
+ const wheel = await Wheel.create({ brand: 'Rotiform' });
+ await Bus.create({ speed: 80, wheels: [wheel] });
+ const bus = await Bus.findOne({}).populate('wheels');
+
+ assert.ok(bus instanceof Vehicle);
+ assert.ok(bus instanceof Bus);
+ assert.equal(bus.wheels.length, 1);
+ assert.ok(bus.wheels[0] instanceof Wheel);
+ assert.equal(bus.wheels[0].brand, 'Rotiform');
});
- it('updating discriminator key (gh-5613)', function(done) {
+ it('updating discriminator key (gh-5613)', async() => {
function BaseSchema() {
Schema.apply(this, arguments);
@@ -772,15 +692,10 @@ describe('model', function() {
const Org = db.model('Test', orgSchema);
Org.discriminator('D', schoolSchema);
- Org.create({ name: 'test' }, function(error, doc) {
- assert.ifError(error);
- assert.ok(!doc.__t);
- Org.findByIdAndUpdate(doc._id, { __t: 'D' }, { new: true, overwriteDiscriminatorKey: true }, function(error, doc) {
- assert.ifError(error);
- assert.equal(doc.__t, 'D');
- done();
- });
- });
+ const doc = await Org.create({ name: 'test' });
+ assert.ok(!doc.__t);
+ const updatedDoc = await Org.findByIdAndUpdate(doc._id, { __t: 'D' }, { new: true, overwriteDiscriminatorKey: true });
+ assert.equal(updatedDoc.__t, 'D');
});
it('disallows updating discriminator key using `$unset` (gh-11456)', async function() {
@@ -1018,46 +933,37 @@ describe('model', function() {
});
describe('using "RootModel#aggregate"', function() {
- it('to aggregate documents of all discriminators', function(done) {
+ it('to aggregate documents of all discriminators', async function() {
const aggregate = BaseEvent.aggregate([
{ $match: { name: 'Test Event' } }
]);
- aggregate.exec(function(err, docs) {
- assert.ifError(err);
- assert.deepEqual(aggregate._pipeline, [
- { $match: { name: 'Test Event' } }
- ]);
- assert.equal(docs.length, 2);
- done();
- });
+ const docs = await aggregate.exec();
+
+ assert.deepEqual(aggregate._pipeline, [
+ { $match: { name: 'Test Event' } }
+ ]);
+ assert.equal(docs.length, 2);
});
});
describe('using "ModelDiscriminator#aggregate"', function() {
- it('only aggregates documents of the appropriate discriminator', function(done) {
+ it('only aggregates documents of the appropriate discriminator', async function() {
const aggregate = ImpressionEvent.aggregate([
{ $group: { _id: '$__t', count: { $sum: 1 } } }
]);
- aggregate.exec(function(err, result) {
- assert.ifError(err);
-
- // Discriminator `$match` pipeline step was added on the
- // `exec` step. The reasoning for this is to not let
- // aggregations with empty pipelines, but that are over
- // discriminators be executed
- assert.deepEqual(aggregate._pipeline, [
- { $match: { __t: 'Impression' } },
- { $group: { _id: '$__t', count: { $sum: 1 } } }
- ]);
-
- assert.equal(result.length, 1);
- assert.deepEqual(result, [
- { _id: 'Impression', count: 2 }
- ]);
- done();
- });
+ const result = await aggregate.exec();
+
+ assert.deepEqual(aggregate._pipeline, [
+ { $match: { __t: 'Impression' } },
+ { $group: { _id: '$__t', count: { $sum: 1 } } }
+ ]);
+
+ assert.equal(result.length, 1);
+ assert.deepEqual(result, [
+ { _id: 'Impression', count: 2 }
+ ]);
});
it('hides fields when discriminated model has select (gh-4991)', function(done) {
@@ -1091,7 +997,7 @@ describe('model', function() {
catch(done);
});
- it('doesnt exclude field if slice (gh-4991)', function(done) {
+ it('doesnt exclude field if slice (gh-4991)', async function() {
const baseSchema = new mongoose.Schema({
propA: { type: String, default: 'default value' },
array: [{ type: String }]
@@ -1104,36 +1010,29 @@ describe('model', function() {
const Discriminator = Base.discriminator('D', discriminatorSchema);
const obj = { propA: 'Hi', propB: 'test', array: ['a', 'b'] };
- Discriminator.create(obj, function(error) {
- assert.ifError(error);
- Base.find().slice('array', 1).exec(function(error, docs) {
- assert.equal(docs.length, 1);
- assert.equal(docs[0].propA, 'Hi');
- done();
- });
- });
+ await Discriminator.create(obj);
+ const docs = await Base.find().slice('array', 1).exec();
+ assert.equal(docs.length, 1);
+ assert.equal(docs[0].propA, 'Hi');
});
- it('merges the first pipeline stages if applicable', function(done) {
+ it('merges the first pipeline stages if applicable', async function() {
const aggregate = ImpressionEvent.aggregate([
{ $match: { name: 'Test Event' } }
]);
- aggregate.exec(function(err, result) {
- assert.ifError(err);
+ const result = await aggregate.exec();
- // Discriminator `$match` pipeline step was added on the
- // `exec` step. The reasoning for this is to not let
- // aggregations with empty pipelines, but that are over
- // discriminators be executed
- assert.deepEqual(aggregate._pipeline, [
- { $match: { __t: 'Impression', name: 'Test Event' } }
- ]);
+ // Discriminator `$match` pipeline step was added on the
+ // `exec` step. The reasoning for this is to not let
+ // aggregations with empty pipelines, but that are over
+ // discriminators be executed
+ assert.deepEqual(aggregate._pipeline, [
+ { $match: { __t: 'Impression', name: 'Test Event' } }
+ ]);
- assert.equal(result.length, 1);
- assert.equal(result[0]._id, impressionEvent.id);
- done();
- });
+ assert.equal(result.length, 1);
+ assert.equal(result[0]._id, impressionEvent.id);
});
});
});
diff --git a/test/model.discriminator.test.js b/test/model.discriminator.test.js
index 89d676bc28b..25e289726eb 100644
--- a/test/model.discriminator.test.js
+++ b/test/model.discriminator.test.js
@@ -7,7 +7,7 @@
const start = require('./common');
const assert = require('assert');
-const clone = require('../lib/utils').clone;
+const clone = require('../lib/helpers/clone');
const random = require('./util').random;
const util = require('util');
@@ -356,15 +356,18 @@ describe('model', function() {
assert.deepEqual(personOptions, employeeOptions);
});
- it('does not allow setting discriminator key (gh-2041)', function(done) {
+ it('does not allow setting discriminator key (gh-2041)', async function() {
const doc = new Employee({ __t: 'fake' });
assert.equal(doc.__t, 'Employee');
- doc.save(function(error) {
+ try {
+ await doc.save();
+
+ throw new Error('Should not get here');
+ } catch (error) {
assert.ok(error);
assert.equal(error.errors['__t'].reason.message,
'Can\'t set discriminator key "__t"');
- done();
- });
+ }
});
it('deduplicates hooks (gh-2945)', function() {
@@ -551,7 +554,7 @@ describe('model', function() {
assert.equal(fromDb.types[1].hello, 'world');
});
- it('supports clone() (gh-4983)', function(done) {
+ it('supports clone() (gh-4983)', async function() {
const childSchema = new Schema({
name: String
});
@@ -591,17 +594,15 @@ describe('model', function() {
};
const doc = new Parent(obj);
- doc.save(function(error, doc) {
- assert.ifError(error);
- assert.equal(doc.name, 'Ned Stark');
- assert.equal(doc.heir.name, 'Robb Stark');
- assert.equal(doc.children.length, 1);
- assert.equal(doc.children[0].name, 'Jon Snow');
- assert.equal(childValidateCalls, 2);
- assert.equal(childCalls, 2);
- assert.equal(parentCalls, 1);
- done();
- });
+ await doc.save();
+
+ assert.equal(doc.name, 'Ned Stark');
+ assert.equal(doc.heir.name, 'Robb Stark');
+ assert.equal(doc.children.length, 1);
+ assert.equal(doc.children[0].name, 'Jon Snow');
+ assert.equal(childValidateCalls, 2);
+ assert.equal(childCalls, 2);
+ assert.equal(parentCalls, 1);
});
it('clone() allows reusing schemas (gh-5098)', function() {
@@ -619,7 +620,24 @@ describe('model', function() {
Person.discriminator('Parent2', parentSchema.clone());
});
- it('clone() allows reusing with different models (gh-5721)', function(done) {
+ it('clone() does not modify original schema `obj` (gh-14821)', function() {
+ const personSchema = new Schema({
+ name: String
+ }, { discriminatorKey: 'kind' });
+
+ const parentSchema = new Schema({
+ child: String
+ });
+
+ const Person = db.model('Person', personSchema);
+ const Parent = Person.discriminator('Parent', parentSchema.clone());
+
+ assert.ok(!Person.schema.obj.child);
+ assert.ok(!personSchema.obj.child);
+ assert.ok(Parent.schema.obj.child);
+ });
+
+ it('clone() allows reusing with different models (gh-5721)', async function() {
const schema = new mongoose.Schema({
name: String
});
@@ -631,16 +649,13 @@ describe('model', function() {
const ModelA = db.model('Test1', schema);
ModelA.discriminator('D1', schemaExt);
- ModelA.findOneAndUpdate({}, { $set: { name: 'test' } }, function(error) {
- assert.ifError(error);
-
- const ModelB = db.model('Test2', schema.clone());
- ModelB.discriminator('D2', schemaExt.clone());
+ await ModelA.findOneAndUpdate({}, { $set: { name: 'test' } });
- done();
- });
+ const ModelB = db.model('Test2', schema.clone());
+ ModelB.discriminator('D2', schemaExt.clone());
});
+
it('incorrect discriminator key throws readable error with create (gh-6434)', async function() {
const settingSchema = new Schema({ name: String }, {
discriminatorKey: 'kind'
@@ -669,7 +684,7 @@ describe('model', function() {
assert.ok(threw);
});
- it('copies query hooks (gh-5147)', function(done) {
+ it('copies query hooks (gh-5147)', async function() {
const options = { discriminatorKey: 'kind' };
const eventSchema = new mongoose.Schema({ time: Date }, options);
@@ -687,13 +702,9 @@ describe('model', function() {
});
const ClickedLinkEvent = Event.discriminator('ClickedLink', clickedEventSchema);
- ClickedLinkEvent.findOneAndUpdate({}, { time: new Date() }, {}).
- exec(function(error) {
- assert.ifError(error);
- assert.equal(eventSchemaCalls, 1);
- assert.equal(clickedEventSchemaCalls, 1);
- done();
- });
+ await ClickedLinkEvent.findOneAndUpdate({}, { time: new Date() }, {}).exec();
+ assert.equal(eventSchemaCalls, 1);
+ assert.equal(clickedEventSchemaCalls, 1);
});
it('reusing schema for discriminators (gh-5684)', function() {
@@ -755,7 +766,7 @@ describe('model', function() {
assert.ifError(d1.validateSync());
});
- it('nested discriminator key with projecting in parent (gh-5775)', function(done) {
+ it('nested discriminator key with projecting in parent (gh-5775)', async() => {
const itemSchema = new Schema({
type: { type: String },
active: { type: Boolean, default: true }
@@ -772,17 +783,12 @@ describe('model', function() {
const doc = {
items: [{ type: 'type1', active: false, count: 3 }]
};
- MyModel.create(doc, function(error) {
- assert.ifError(error);
- MyModel.findOne({}).select('items').exec(function(error, doc) {
- assert.ifError(error);
- assert.equal(doc.items.length, 1);
- assert.equal(doc.items[0].type, 'type1');
- assert.strictEqual(doc.items[0].active, false);
- assert.strictEqual(doc.items[0].count, 3);
- done();
- });
- });
+ await MyModel.create(doc);
+ const result = await MyModel.findOne({}).select('items');
+ assert.equal(result.items.length, 1);
+ assert.equal(result.items[0].type, 'type1');
+ assert.strictEqual(result.items[0].active, false);
+ assert.strictEqual(result.items[0].count, 3);
});
it('with $meta projection (gh-5859)', function() {
@@ -1274,7 +1280,7 @@ describe('model', function() {
});
});
- it('should call the hooks on the embedded document defined by both the parent and discriminated schemas', function(done) {
+ it('should call the hooks on the embedded document defined by both the parent and discriminated schemas', async function() {
const trackSchema = new Schema({
event: eventSchema
});
@@ -1289,18 +1295,15 @@ describe('model', function() {
kind: 'Purchased'
}
});
- doc.save(function(err) {
- assert.ok(!err);
- assert.equal(doc.event.message, 'Test');
- assert.equal(doc.event.kind, 'Purchased');
- Object.keys(counters).forEach(function(i) {
- assert.equal(counters[i], 1, 'Counter ' + i + ' incorrect');
- });
- done();
+ await doc.save();
+ assert.equal(doc.event.message, 'Test');
+ assert.equal(doc.event.kind, 'Purchased');
+ Object.keys(counters).forEach(function(i) {
+ assert.equal(counters[i], 1, 'Counter ' + i + ' incorrect');
});
});
- it('should call the hooks on the embedded document in an embedded array defined by both the parent and discriminated schemas', function(done) {
+ it('should call the hooks on the embedded document in an embedded array defined by both the parent and discriminated schemas', async function() {
const trackSchema = new Schema({
events: [eventSchema]
});
@@ -1321,16 +1324,16 @@ describe('model', function() {
}
]
});
- doc.save(function(err) {
- assert.ok(!err);
- assert.equal(doc.events[0].kind, 'Purchased');
- assert.equal(doc.events[0].message, 'Test');
- assert.equal(doc.events[1].kind, 'Purchased');
- assert.equal(doc.events[1].message, 'TestAgain');
- Object.keys(counters).forEach(function(i) {
- assert.equal(counters[i], 2);
- });
- done();
+
+ await doc.save();
+
+ assert.equal(doc.events[0].kind, 'Purchased');
+ assert.equal(doc.events[0].message, 'Test');
+ assert.equal(doc.events[1].kind, 'Purchased');
+ assert.equal(doc.events[1].message, 'TestAgain');
+
+ Object.keys(counters).forEach(function(i) {
+ assert.equal(counters[i], 2);
});
});
});
@@ -1681,7 +1684,10 @@ describe('model', function() {
it('can use compiled model schema as a discriminator (gh-9238)', function() {
const SmsSchema = new mongoose.Schema({ senderNumber: String });
const EmailSchema = new mongoose.Schema({ fromEmailAddress: String });
- const messageSchema = new mongoose.Schema({ method: String }, { discriminatorKey: 'method' });
+ const messageSchema = new mongoose.Schema(
+ { method: String },
+ { discriminatorKey: 'method' }
+ );
const Message = db.model('Test', messageSchema);
Message.discriminator('email', EmailSchema);
@@ -2101,7 +2107,28 @@ describe('model', function() {
await Square.create({ nested: { test: 'foo' } });
assert.equal(subdocSaveCalls, 1);
});
-
+ it('should not throw an error when the user is not modifying anything involving discriminators gh-12135', function() {
+ const baseSchema = Schema({}, { typeKey: 'foo' });
+ const Base = db.model('Base', baseSchema);
+ const childSchema = new Schema({}, {});
+ const Test = Base.discriminator('model-discriminator-custom', childSchema);
+ assert.equal(Test.schema.options.typeKey, 'foo');
+ });
+ it('should throw an error because of the different typeKeys gh-12135', function() {
+ const baseSchema = Schema({}, { typeKey: 'foo' });
+ const Base = db.model('Base1', baseSchema);
+ const childSchema = new Schema({}, { typeKey: 'bar' });
+ assert.throws(() => {
+ Base.discriminator('model-discriminator-custom1', childSchema);
+ }, { message: 'Can\'t customize discriminator option typeKey (can only modify toJSON, toObject, _id, id, virtuals, methods)' });
+ });
+ it('handles customizable discriminator options gh-12135', function() {
+ const baseSchema = Schema({}, { toJSON: { virtuals: true } });
+ const Base = db.model('Base1', baseSchema);
+ const childSchema = new Schema({}, { toJSON: { virtuals: true, getters: true } });
+ const Test = Base.discriminator('model-discriminator-custom1', childSchema);
+ assert.deepEqual(Test.schema.options.toJSON, { virtuals: true, getters: true });
+ });
it('uses "value" over "name" for multi-dimensonal arrays (gh-13201)', function() {
const buildingSchema = new mongoose.Schema(
{
@@ -2170,4 +2197,183 @@ describe('model', function() {
assert.ok(innerBuildingsPath.schemaOptions.type.discriminators.Garage);
assert.equal(innerBuildingsPath.schemaOptions.type.discriminators.Garage.discriminatorMapping.value, 'G');
});
+
+ it('runs base schema paths validators and setters before child schema validators and setters (gh-13794)', async function() {
+ const baseSchema = new Schema({
+ f1: {
+ type: Number,
+ set() {
+ return 1;
+ }
+ }
+ });
+ const childSchema = new Schema({
+ f2: {
+ type: Number,
+ set() {
+ return this.f1;
+ }
+ }
+ });
+ const Test = db.model('Test', baseSchema);
+ const Child = Test.discriminator('Child', childSchema);
+ const doc = new Child({ f1: 2, f2: 2 });
+ assert.strictEqual(doc.f2, 1);
+ });
+
+ it('should not fail when using a discriminator key multiple times (gh-13906)', async function() {
+ const options = { discriminatorKey: 'type' };
+ const eventSchema = new Schema({ date: Schema.Types.Date }, options);
+ const Event = db.model('gh-13906-event', eventSchema);
+
+
+ const clickedLinkEventSchema = new Schema({ url: String }, options);
+ const ClickedLinkEvent = Event.discriminator('ClickedLinkEvent', clickedLinkEventSchema, 'clickedLinkEvent');
+
+
+ const clickedImageEventSchema = new Schema({ image: String }, options);
+ const ClickedImageEvent = Event.discriminator('ClickedImageEvent', clickedImageEventSchema, 'clickedImageEvent');
+
+ const clickedLinkEvent = new ClickedLinkEvent({ url: 'https://clicked-link.com' });
+ assert.equal(clickedLinkEvent.type, 'clickedLinkEvent');
+ assert.equal(clickedLinkEvent.url, 'https://clicked-link.com');
+
+ const clickedImageEvent = new ClickedImageEvent({ image: 'clicked-image.png' });
+ assert.equal(clickedImageEvent.type, 'clickedImageEvent');
+ assert.equal(clickedImageEvent.image, 'clicked-image.png');
+ const query = {
+ type: 'clickedLinkEvent',
+ $or: [
+ { type: 'clickedImageEvent' }
+ ]
+ };
+ const result = await Event.find(query).exec();
+ assert(result);
+
+ });
+
+ it('correctly gathers subdocs with discriminators (gh-15088)', async function() {
+ const RequestTriggeredWorkflowSchema = new Schema(
+ {
+ workflow: {
+ type: String
+ }
+ },
+ {
+ timestamps: true
+ }
+ );
+
+ const RequestSchema = new Schema(
+ {
+ status: {
+ type: String
+ },
+ summary: {
+ type: String
+ },
+ triggeredWorkflows: [RequestTriggeredWorkflowSchema]
+ },
+ {
+ discriminatorKey: 'source',
+ timestamps: true
+ }
+ );
+
+ const EmailRequestSchema = new Schema({
+ // Any extra properties that specifically apply to 'email' requests
+ });
+ const FormRequestSchema = new Schema({
+ // Any extra properties that specifically apply to 'form' requests
+ });
+
+ const Request = db.model('Request', RequestSchema);
+
+ Request.discriminator('Request:email', EmailRequestSchema, 'email');
+ Request.discriminator('Request:form', FormRequestSchema, 'form');
+
+ const request = await Request.create({
+ status: 'new',
+ source: 'form'
+ });
+
+ let requestAsReadFromDb = await Request.findById(request._id);
+
+ request.status = 'in progress';
+ await request.save();
+
+ assert.equal(requestAsReadFromDb.$getAllSubdocs().length, 0);
+ const now = new Date();
+ request.triggeredWorkflows.push({
+ workflow: '111111111111111111111111'
+ });
+ assert.equal(request.$getAllSubdocs().length, 1);
+ assert.equal(request.$getAllSubdocs()[0], request.triggeredWorkflows[0]);
+ await request.save();
+
+ requestAsReadFromDb = await Request.findById(request._id);
+ assert.equal(requestAsReadFromDb.$getAllSubdocs().length, 1);
+ assert.equal(requestAsReadFromDb.$getAllSubdocs()[0], requestAsReadFromDb.triggeredWorkflows[0]);
+ assert.ok(requestAsReadFromDb.triggeredWorkflows[0].createdAt.valueOf() >= now.valueOf());
+ assert.ok(requestAsReadFromDb.triggeredWorkflows[0].updatedAt.valueOf() >= now.valueOf());
+ });
+
+ it('triggers save hooks on subdocuments (gh-15092)', async function() {
+ const subdocumentSchema = mongoose.Schema({
+ name: String
+ });
+
+ const subdocumentPreSaveHooks = [];
+ subdocumentSchema.pre('save', function(next) {
+ subdocumentPreSaveHooks.push(this);
+ next();
+ });
+
+ const schema = mongoose.Schema({
+ name: String,
+ subdocuments: [subdocumentSchema]
+ }, { discriminatorKey: 'type' });
+
+ const documentPreSaveHooks = [];
+ schema.pre('save', function(next) {
+ documentPreSaveHooks.push(this);
+ next();
+ });
+
+ const Document = db.model('Document', schema);
+
+ const discriminatorSchema = mongoose.Schema({});
+
+ const discriminatorPreSaveHooks = [];
+ discriminatorSchema.pre('save', function(next) {
+ discriminatorPreSaveHooks.push(this);
+ next();
+ });
+
+ const Discriminator = Document.discriminator('Discriminator', discriminatorSchema);
+
+ const document = new Document({ name: 'Document' });
+ document.subdocuments.push({ name: 'Document Subdocument' });
+ await document.save();
+
+ assert.equal(discriminatorPreSaveHooks.length, 0);
+ assert.equal(subdocumentPreSaveHooks.length, 1);
+ assert.equal(subdocumentPreSaveHooks[0], document.subdocuments[0]);
+ assert.equal(documentPreSaveHooks.length, 1);
+ assert.equal(documentPreSaveHooks[0], document);
+
+ subdocumentPreSaveHooks.length = 0;
+ documentPreSaveHooks.length = 0;
+
+ const discriminator = new Discriminator({ name: 'Discriminator', type: 'Discriminator' });
+ discriminator.subdocuments.push({ name: 'Discriminator Subdocument' });
+ await discriminator.save();
+
+ assert.equal(subdocumentPreSaveHooks.length, 1);
+ assert.equal(subdocumentPreSaveHooks[0], discriminator.subdocuments[0]);
+ assert.equal(discriminatorPreSaveHooks.length, 1);
+ assert.equal(discriminatorPreSaveHooks[0], discriminator);
+ assert.equal(documentPreSaveHooks.length, 1);
+ assert.equal(documentPreSaveHooks[0], discriminator);
+ });
});
diff --git a/test/model.field.selection.test.js b/test/model.field.selection.test.js
index 2ae5c97312f..f1cf97c535b 100644
--- a/test/model.field.selection.test.js
+++ b/test/model.field.selection.test.js
@@ -63,7 +63,7 @@ describe('model field selection', function() {
BlogPostB = db.model('BlogPost', BlogPostSchema);
});
- it('excluded fields should be undefined', function(done) {
+ it('excluded fields should be undefined', async function() {
const date = new Date();
const doc = {
@@ -73,178 +73,131 @@ describe('model field selection', function() {
meta: { date: date }
};
- BlogPostB.create(doc, function(err, created) {
- assert.ifError(err);
-
- const id = created.id;
- BlogPostB.findById(id, { title: 0, 'meta.date': 0, owners: 0, 'comments.user': 0 }, function(err, found) {
- assert.ifError(err);
- assert.equal(found._id.toString(), created._id);
- assert.strictEqual(undefined, found.title);
- assert.strictEqual('kandinsky', found.def);
- assert.strictEqual('me', found.author);
- assert.strictEqual(true, Array.isArray(found.numbers));
- assert.equal(found.meta.date, undefined);
- assert.equal(found.numbers.length, 0);
- assert.equal(found.owners, undefined);
- assert.strictEqual(true, Array.isArray(found.comments));
- assert.equal(found.comments.length, 2);
- found.comments.forEach(function(comment) {
- assert.equal(comment.user, undefined);
- });
- done();
- });
+ const created = await BlogPostB.create(doc);
+ const id = created.id;
+
+ const found = await BlogPostB.findById(id, { title: 0, 'meta.date': 0, owners: 0, 'comments.user': 0 });
+ assert.equal(found._id.toString(), created._id);
+ assert.strictEqual(undefined, found.title);
+ assert.strictEqual('kandinsky', found.def);
+ assert.strictEqual('me', found.author);
+ assert.strictEqual(true, Array.isArray(found.numbers));
+ assert.equal(found.meta.date, undefined);
+ assert.equal(found.numbers.length, 0);
+ assert.equal(found.owners, undefined);
+ assert.strictEqual(true, Array.isArray(found.comments));
+ assert.equal(found.comments.length, 2);
+ found.comments.forEach(function(comment) {
+ assert.equal(comment.user, undefined);
});
});
- it('excluded fields should be undefined and defaults applied to other fields', function(done) {
+ it('excluded fields should be undefined and defaults applied to other fields', async function() {
const id = new DocumentObjectId();
const date = new Date();
- BlogPostB.collection.insertOne({ _id: id, title: 'hahaha1', meta: { date: date } }, function(err) {
- assert.ifError(err);
-
- BlogPostB.findById(id, { title: 0 }, function(err, found) {
- assert.ifError(err);
- assert.equal(found._id.toString(), id);
- assert.strictEqual(undefined, found.title);
- assert.strictEqual('kandinsky', found.def);
- assert.strictEqual(undefined, found.author);
- assert.strictEqual(true, Array.isArray(found.comments));
- assert.equal(date.toString(), found.meta.date.toString());
- assert.equal(found.comments.length, 0);
- done();
- });
- });
+ await BlogPostB.collection.insertOne({ _id: id, title: 'hahaha1', meta: { date: date } });
+
+ const found = await BlogPostB.findById(id, { title: 0 });
+ assert.equal(found._id.toString(), id);
+ assert.strictEqual(undefined, found.title);
+ assert.strictEqual('kandinsky', found.def);
+ assert.strictEqual(undefined, found.author);
+ assert.strictEqual(true, Array.isArray(found.comments));
+ assert.equal(date.toString(), found.meta.date.toString());
+ assert.equal(found.comments.length, 0);
});
- it('where subset of fields excludes _id', function(done) {
- BlogPostB.create({ title: 'subset 1' }, function(err) {
- assert.ifError(err);
- BlogPostB.findOne({ title: 'subset 1' }, { title: 1, _id: 0 }, function(err, found) {
- assert.ifError(err);
- assert.strictEqual(undefined, found._id);
- assert.equal(found.title, 'subset 1');
- done();
- });
- });
+ it('where subset of fields excludes _id', async function() {
+ await BlogPostB.create({ title: 'subset 1' });
+ const found = await BlogPostB.findOne({ title: 'subset 1' }, { title: 1, _id: 0 });
+ assert.strictEqual(undefined, found._id);
+ assert.equal(found.title, 'subset 1');
});
- it('works with subset of fields, excluding _id', function(done) {
- BlogPostB.create({ title: 'subset 1', author: 'me' }, function(err) {
- assert.ifError(err);
- BlogPostB.find({ title: 'subset 1' }, { title: 1, _id: 0 }, function(err, found) {
- assert.ifError(err);
- assert.strictEqual(undefined, found[0]._id);
- assert.equal(found[0].title, 'subset 1');
- assert.strictEqual(undefined, found[0].def);
- assert.strictEqual(undefined, found[0].author);
- assert.strictEqual(false, Array.isArray(found[0].comments));
- done();
- });
- });
+ it('works with subset of fields, excluding _id', async function() {
+ await BlogPostB.create({ title: 'subset 1', author: 'me' });
+ const found = await BlogPostB.find({ title: 'subset 1' }, { title: 1, _id: 0 });
+ assert.strictEqual(undefined, found[0]._id);
+ assert.equal(found[0].title, 'subset 1');
+ assert.strictEqual(undefined, found[0].def);
+ assert.strictEqual(undefined, found[0].author);
+ assert.strictEqual(false, Array.isArray(found[0].comments));
});
- it('works with just _id and findOneAndUpdate (gh-3407)', function(done) {
+ it('works with just _id and findOneAndUpdate (gh-3407)', async function() {
const MyModel = db.model('Test', { test: { type: Number, default: 1 } });
-
- MyModel.collection.insertOne({}, function(error) {
- assert.ifError(error);
- MyModel.findOne({}, { _id: 1 }, function(error, doc) {
- assert.ifError(error);
- assert.ok(!doc.test);
- done();
- });
- });
+ await MyModel.collection.insertOne({});
+ const doc = await MyModel.findOne({}, { _id: 1 });
+ assert.ok(!doc.test);
});
- it('works with subset of fields excluding emebedded doc _id (gh-541)', function(done) {
- BlogPostB.create({ title: 'LOTR', comments: [{ title: ':)' }] }, function(err, created) {
- assert.ifError(err);
- BlogPostB.find({ _id: created }, { _id: 0, 'comments._id': 0 }, function(err, found) {
- assert.ifError(err);
- assert.strictEqual(undefined, found[0]._id);
- assert.equal(found[0].title, 'LOTR');
- assert.strictEqual('kandinsky', found[0].def);
- assert.strictEqual(undefined, found[0].author);
- assert.strictEqual(true, Array.isArray(found[0].comments));
- assert.equal(found[0].comments.length, 1);
- assert.equal(found[0].comments[0].title, ':)');
-
- assert.strictEqual(undefined, found[0].comments[0]._id);
- // gh-590
- assert.ok(!found[0].comments[0].id);
- done();
- });
- });
+ it('works with subset of fields excluding emebedded doc _id (gh-541)', async function() {
+ const created = await BlogPostB.create({ title: 'LOTR', comments: [{ title: ':)' }] });
+ const found = await BlogPostB.find({ _id: created }, { _id: 0, 'comments._id': 0 });
+ assert.strictEqual(undefined, found[0]._id);
+ assert.equal(found[0].title, 'LOTR');
+ assert.strictEqual('kandinsky', found[0].def);
+ assert.strictEqual(undefined, found[0].author);
+ assert.strictEqual(true, Array.isArray(found[0].comments));
+ assert.equal(found[0].comments.length, 1);
+ assert.equal(found[0].comments[0].title, ':)');
+
+ assert.strictEqual(undefined, found[0].comments[0]._id);
+ // gh-590
+ assert.ok(!found[0].comments[0].id);
});
- it('included fields should have defaults applied when no value exists in db (gh-870)', function(done) {
+ it('included fields should have defaults applied when no value exists in db (gh-870)', async function() {
const id = new DocumentObjectId();
- BlogPostB.collection.insertOne(
- { _id: id, title: 'issue 870' }, function(err) {
- assert.ifError(err);
-
- BlogPostB.findById(id, 'def comments', function(err, found) {
- assert.ifError(err);
- assert.ok(found);
- assert.equal(found._id.toString(), id);
- assert.strictEqual(undefined, found.title);
- assert.strictEqual('kandinsky', found.def);
- assert.strictEqual(undefined, found.author);
- assert.strictEqual(true, Array.isArray(found.comments));
- assert.equal(found.comments.length, 0);
- done();
- });
- });
+ await BlogPostB.collection.insertOne({ _id: id, title: 'issue 870' });
+
+ const found = await BlogPostB.findById(id, 'def comments');
+ assert.ok(found);
+ assert.equal(found._id.toString(), id);
+ assert.strictEqual(undefined, found.title);
+ assert.strictEqual('kandinsky', found.def);
+ assert.strictEqual(undefined, found.author);
+ assert.strictEqual(true, Array.isArray(found.comments));
+ assert.equal(found.comments.length, 0);
});
- it('including subdoc field excludes other subdoc fields (gh-1027)', function(done) {
- BlogPostB.create({ comments: [{ title: 'a' }, { title: 'b' }] }, function(err, doc) {
- assert.ifError(err);
-
- BlogPostB.findById(doc._id).select('_id comments.title').exec(function(err, found) {
- assert.ifError(err);
- assert.ok(found);
- assert.equal(doc._id.toString(), found._id.toString());
- assert.strictEqual(undefined, found.title);
- assert.strictEqual(true, Array.isArray(found.comments));
- found.comments.forEach(function(comment) {
- assert.equal(comment.body, undefined);
- assert.equal(comment.comments, undefined);
- assert.equal(comment._id, undefined);
- assert.ok(!!comment.title);
- });
- done();
- });
+ it('including subdoc field excludes other subdoc fields (gh-1027)', async function() {
+
+ const doc = await BlogPostB.create({ comments: [{ title: 'a' }, { title: 'b' }] });
+ const found = await BlogPostB.findById(doc._id).select('_id comments.title');
+ assert.ok(found);
+ assert.equal(doc._id.toString(), found._id.toString());
+ assert.strictEqual(undefined, found.title);
+ assert.strictEqual(true, Array.isArray(found.comments));
+ found.comments.forEach(function(comment) {
+ assert.equal(comment.body, undefined);
+ assert.equal(comment.comments, undefined);
+ assert.equal(comment._id, undefined);
+ assert.ok(!!comment.title);
});
});
- it('excluding nested subdoc fields (gh-1027)', function(done) {
- BlogPostB.create({ title: 'top', comments: [{ title: 'a', body: 'body' }, { title: 'b', body: 'body', comments: [{ title: 'c' }] }] }, function(err, doc) {
- assert.ifError(err);
-
- BlogPostB.findById(doc._id).select('-_id -comments.title -comments.comments.comments -numbers').exec(function(err, found) {
- assert.ifError(err);
- assert.ok(found);
- assert.equal(found._id, undefined);
- assert.strictEqual(found.title, 'top');
- assert.equal(found.numbers, undefined);
- assert.strictEqual(true, Array.isArray(found.comments));
- found.comments.forEach(function(comment) {
- assert.equal(comment.title, undefined);
- assert.equal(comment.body, 'body');
- assert.strictEqual(Array.isArray(comment.comments), true);
- assert.ok(comment._id);
- comment.comments.forEach(function(comment) {
- assert.equal(comment.title, 'c');
- assert.equal(comment.body, undefined);
- assert.equal(comment.comments, undefined);
- assert.ok(comment._id);
- });
- });
- done();
+ it('excluding nested subdoc fields (gh-1027)', async function() {
+
+ const doc = await BlogPostB.create({ title: 'top', comments: [{ title: 'a', body: 'body' }, { title: 'b', body: 'body', comments: [{ title: 'c' }] }] });
+ const found = await BlogPostB.findById(doc._id).select('-_id -comments.title -comments.comments.comments -numbers');
+ assert.ok(found);
+ assert.equal(found._id, undefined);
+ assert.strictEqual(found.title, 'top');
+ assert.equal(found.numbers, undefined);
+ assert.strictEqual(true, Array.isArray(found.comments));
+ found.comments.forEach(function(comment) {
+ assert.equal(comment.title, undefined);
+ assert.equal(comment.body, 'body');
+ assert.strictEqual(Array.isArray(comment.comments), true);
+ assert.ok(comment._id);
+ comment.comments.forEach(function(comment) {
+ assert.equal(comment.title, 'c');
+ assert.equal(comment.body, undefined);
+ assert.equal(comment.comments, undefined);
+ assert.ok(comment._id);
});
});
});
@@ -252,7 +205,7 @@ describe('model field selection', function() {
describe('with $elemMatch projection', function() {
// mongodb 2.2 support
- it('casts elemMatch args (gh-1091)', function(done) {
+ it('casts elemMatch args (gh-1091)', async function() {
const postSchema = new Schema({
ids: [{ type: Schema.ObjectId }]
});
@@ -261,36 +214,21 @@ describe('model field selection', function() {
const _id1 = new mongoose.Types.ObjectId();
const _id2 = new mongoose.Types.ObjectId();
- B.create({ ids: [_id1, _id2] }, function(err, doc) {
- assert.ifError(err);
-
- B
- .findById(doc._id)
- .select({ ids: { $elemMatch: { $in: [_id2.toString()] } } })
- .exec(function(err, found) {
- assert.ifError(err);
- assert.ok(found);
- assert.equal(found.id, doc.id);
- assert.equal(found.ids.length, 1);
- assert.equal(found.ids[0].toString(), _id2.toString());
-
- B
- .find({ _id: doc._id })
- .select({ ids: { $elemMatch: { $in: [_id2.toString()] } } })
- .exec(function(err, found) {
- assert.ifError(err);
- assert.ok(found.length);
- found = found[0];
- assert.equal(found.id, doc.id);
- assert.equal(found.ids.length, 1);
- assert.equal(found.ids[0].toString(), _id2.toString());
- done();
- });
- });
- });
+ const doc = await B.create({ ids: [_id1, _id2] });
+ let found = await B.findById(doc._id).select({ ids: { $elemMatch: { $in: [_id2.toString()] } } });
+ assert.ok(found);
+ assert.equal(found.id, doc.id);
+ assert.equal(found.ids.length, 1);
+ assert.equal(found.ids[0].toString(), _id2.toString());
+ found = await B.find({ _id: doc._id }).select({ ids: { $elemMatch: { $in: [_id2.toString()] } } });
+ assert.ok(found.length);
+ found = found[0];
+ assert.equal(found.id, doc.id);
+ assert.equal(found.ids.length, 1);
+ assert.equal(found.ids[0].toString(), _id2.toString());
});
- it('saves modified elemMatch paths (gh-1334)', function(done) {
+ it('saves modified elemMatch paths (gh-1334)', async function() {
const postSchema = new Schema({
ids: [{ type: Schema.ObjectId }],
ids2: [{ type: Schema.ObjectId }]
@@ -300,98 +238,60 @@ describe('model field selection', function() {
const _id1 = new mongoose.Types.ObjectId();
const _id2 = new mongoose.Types.ObjectId();
- B.create({ ids: [_id1, _id2], ids2: [_id2, _id1] }, function(err, doc) {
- assert.ifError(err);
-
- B
- .findById(doc._id)
- .select({ ids2: { $elemMatch: { $in: [_id1.toString()] } } })
- .exec(function(err, found) {
- assert.ifError(err);
- assert.equal(found.ids2.length, 1);
- found.ids2.set(0, _id2);
-
- found.save(function(err) {
- assert.ifError(err);
-
- B
- .findById(doc._id)
- .select({ ids: { $elemMatch: { $in: [_id2.toString()] } } })
- .select('ids2')
- .exec(function(err, found) {
- assert.equal(2, found.ids2.length);
- assert.equal(_id2.toHexString(), found.ids2[0].toHexString());
- assert.equal(_id2.toHexString(), found.ids2[1].toHexString());
-
- found.ids.pull(_id2);
-
- found.save(function(err) {
- assert.ok(err);
-
- done();
- });
- });
- });
- });
- });
+ const doc = await B.create({ ids: [_id1, _id2], ids2: [_id2, _id1] });
+ let found = await B.findById(doc._id).select({ ids2: { $elemMatch: { $in: [_id1.toString()] } } });
+ assert.equal(found.ids2.length, 1);
+ found.ids2.set(0, _id2);
+ await found.save();
+ found = await B.findById(doc._id).select({ ids: { $elemMatch: { $in: [_id2.toString()] } } }).select('ids2');
+ assert.equal(2, found.ids2.length);
+ assert.equal(_id2.toHexString(), found.ids2[0].toHexString());
+ assert.equal(_id2.toHexString(), found.ids2[1].toHexString());
+
+ found.ids.pull(_id2);
+ const err = await found.save().then(() => null, err => err);
+ assert.ok(err);
});
- it('works with $ positional in select (gh-2031)', function(done) {
+ it('works with $ positional in select (gh-2031)', async function() {
const postSchema = new Schema({
- tags: [{ tag: String, count: 0 }]
+ tags: [{ tag: String, count: Number }]
});
const Post = db.model('Test', postSchema);
- Post.create({ tags: [{ tag: 'bacon', count: 2 }, { tag: 'eggs', count: 3 }] }, function(error) {
- assert.ifError(error);
- Post.findOne({ 'tags.tag': 'eggs' }, { 'tags.$': 1 }, function(error, post) {
- assert.ifError(error);
- post.tags[0].count = 1;
- post.save(function(error) {
- assert.ok(error);
- done();
- });
- });
- });
+ await Post.create({ tags: [{ tag: 'bacon', count: 2 }, { tag: 'eggs', count: 3 }] });
+ const post = await Post.findOne({ 'tags.tag': 'eggs' }, { 'tags.$': 1 });
+ post.tags[0].count = 1;
+ const err = await post.save().then(() => null, err => err);
+ assert.ok(err);
});
});
- it('selecting an array of docs applies defaults properly (gh-1108)', function(done) {
+ it('selecting an array of docs applies defaults properly (gh-1108)', async function() {
const M = BlogPostB;
const m = new M({ title: '1108', comments: [{ body: 'yay' }] });
m.comments[0].comments = undefined;
- m.save(function(err, doc) {
- assert.ifError(err);
- M.findById(doc._id).select('comments').exec(function(err, found) {
- assert.ifError(err);
- assert.ok(Array.isArray(found.comments));
- assert.equal(found.comments.length, 1);
- assert.ok(Array.isArray(found.comments[0].comments));
- done();
- });
- });
+ const doc = await m.save();
+ const found = await M.findById(doc._id).select('comments');
+ assert.ok(Array.isArray(found.comments));
+ assert.equal(found.comments.length, 1);
+ assert.ok(Array.isArray(found.comments[0].comments));
});
- it('select properties named length (gh-3903)', function(done) {
+ it('select properties named length (gh-3903)', async function() {
const schema = new mongoose.Schema({
length: Number,
name: String
});
const MyModel = db.model('Test', schema);
-
- MyModel.create({ name: 'val', length: 3 }, function(error) {
- assert.ifError(error);
- MyModel.findOne({}).select({ length: 1 }).exec(function(error, doc) {
- assert.ifError(error);
- assert.ok(!doc.name);
- done();
- });
- });
+ await MyModel.create({ name: 'val', length: 3 });
+ const doc = await MyModel.findOne({}).select({ length: 1 });
+ assert.ok(!doc.name);
});
- it('appropriately filters subdocuments based on properties (gh-1280)', function(done) {
+ it('appropriately filters subdocuments based on properties (gh-1280)', async function() {
const RouteSchema = new Schema({
stations: {
start: {
@@ -426,23 +326,13 @@ describe('model field selection', function() {
points: [{ name: 'rawr' }]
}
};
-
- Route.create(item, function(err, i) {
- assert.ifError(err);
-
- Route.findById(i.id).select('-stations').exec(function(err, res) {
- assert.ifError(err);
- assert.equal(res.stations.toString(), 'MongooseDocument { undefined }');
-
- Route.findById(i.id).select('-stations.start -stations.end').exec(function(err, res) {
- assert.ifError(err);
- assert.equal(res.stations.start.toString(), 'MongooseDocument { undefined }');
- assert.equal(res.stations.end.toString(), 'MongooseDocument { undefined }');
- assert.ok(Array.isArray(res.stations.points));
- done();
- });
- });
- });
+ const i = await Route.create(item);
+ let res = await Route.findById(i.id).select('-stations');
+ assert.equal(res.stations.toString(), 'MongooseDocument { undefined }');
+ res = await Route.findById(i.id).select('-stations.start -stations.end');
+ assert.equal(res.stations.start.toString(), 'MongooseDocument { undefined }');
+ assert.equal(res.stations.end.toString(), 'MongooseDocument { undefined }');
+ assert.ok(Array.isArray(res.stations.points));
});
it('sets defaults correctly in child docs with projection (gh-7159)', async function() {
diff --git a/test/model.findByIdAndUpdate.test.js b/test/model.findByIdAndUpdate.test.js
new file mode 100644
index 00000000000..9db1b39d228
--- /dev/null
+++ b/test/model.findByIdAndUpdate.test.js
@@ -0,0 +1,96 @@
+'use strict';
+
+/**
+ * Test dependencies.
+ */
+
+const start = require('./common');
+
+const assert = require('assert');
+const mongoose = start.mongoose;
+const Schema = mongoose.Schema;
+const util = require('./util');
+
+describe('model: findByIdAndUpdate:', function() {
+ let db;
+
+ before(function() {
+ db = start();
+ });
+
+ after(async function() {
+ await db.close();
+ });
+
+ beforeEach(() => db.deleteModel(/.*/));
+ afterEach(() => util.clearTestData(db));
+ afterEach(() => require('./util').stopRemainingOps(db));
+
+ it('returns the edited document with previous and target discriminators types defined', async function() {
+ const shapeSchema = Schema({ name: String }, { discriminatorKey: 'kind' });
+ const schema = Schema({ shape: shapeSchema });
+
+ schema.path('shape').discriminator('gh8378_Circle',
+ Schema({ radius: String, color: String }));
+ schema.path('shape').discriminator('gh8378_Square',
+ Schema({ side: Number, color: String }));
+
+ const MyModel = db.model('Test', schema);
+
+
+ let doc = await MyModel.create({
+ shape: {
+ kind: 'gh8378_Circle',
+ name: 'before',
+ radius: 5,
+ color: 'red'
+ }
+ });
+
+ doc = await MyModel.findByIdAndUpdate(doc._id, {
+ 'shape.kind': 'gh8378_Square',
+ 'shape.name': 'after',
+ 'shape.side': 4,
+ 'shape.color': 'white'
+ }, { new: true });
+
+ assert.equal(doc.shape.kind, 'gh8378_Square');
+ assert.equal(doc.shape.name, 'after');
+ assert.equal(doc.shape.side, 4);
+ assert.equal(doc.shape.color, 'white');
+ });
+
+ it('returns the edited document with only previous discriminator type defined', async function() {
+ const shapeSchema = Schema({ name: String }, { discriminatorKey: 'kind' });
+ const schema = Schema({ shape: shapeSchema });
+
+ schema.path('shape').discriminator('gh8378_Circle',
+ Schema({ radius: String, color: String }));
+ schema.path('shape').discriminator('gh8378_Square',
+ Schema({ side: Number, color: String }));
+
+ const MyModel = db.model('Test', schema);
+
+
+ let doc = await MyModel.create({
+ shape: {
+ kind: 'gh8378_Circle',
+ name: 'before',
+ radius: 5,
+ color: 'red'
+ }
+ });
+
+ doc = await MyModel.findByIdAndUpdate(doc._id, {
+ 'shape.kind': 'gh8378_Square',
+ 'shape.name': 'after',
+ 'shape.side': 4,
+ 'shape.color': 'white'
+ }, { new: true, overwriteDiscriminatorKey: true });
+
+ assert.equal(doc.shape.kind, 'gh8378_Square');
+ assert.equal(doc.shape.name, 'after');
+ assert.equal(doc.shape.side, 4);
+ assert.equal(doc.shape.color, 'white');
+ });
+});
diff --git a/test/model.findOneAndDelete.test.js b/test/model.findOneAndDelete.test.js
index 12ad1618278..de77c5ceb05 100644
--- a/test/model.findOneAndDelete.test.js
+++ b/test/model.findOneAndDelete.test.js
@@ -128,66 +128,6 @@ describe('model: findOneAndDelete:', function() {
assert.equal(query._conditions.author, undefined);
});
- it('executes when a callback is passed', function(done) {
- const M = BlogPost;
- let pending = 5;
-
- M.findOneAndDelete({ name: 'aaron1' }, { select: 'name' }, cb);
- M.findOneAndDelete({ name: 'aaron1' }, cb);
- M.where().findOneAndDelete({ name: 'aaron1' }, { select: 'name' }, cb);
- M.where().findOneAndDelete({ name: 'aaron1' }, cb);
- M.where('name', 'aaron1').findOneAndDelete(cb);
-
- function cb(err, doc) {
- assert.ifError(err);
- assert.equal(doc, null); // no previously existing doc
- if (--pending) return;
- done();
- }
- });
-
- it('executed with only a callback throws', function() {
- const M = BlogPost;
- let err;
-
- try {
- M.findOneAndDelete(function() {});
- } catch (e) {
- err = e;
- }
-
- assert.ok(/First argument must not be a function/.test(err));
- });
-
- it('executed with only a callback throws', function() {
- const M = BlogPost;
- let err;
-
- try {
- M.findByIdAndDelete(function() {});
- } catch (e) {
- err = e;
- }
-
- assert.ok(/First argument must not be a function/.test(err));
- });
-
- it('executes when a callback is passed', function(done) {
- const M = BlogPost;
- const _id = new DocumentObjectId();
- let pending = 2;
-
- M.findByIdAndDelete(_id, { select: 'name' }, cb);
- M.findByIdAndDelete(_id, cb);
-
- function cb(err, doc) {
- assert.ifError(err);
- assert.equal(doc, null); // no previously existing doc
- if (--pending) return;
- done();
- }
- });
-
it('returns the original document', async function() {
const M = BlogPost;
const title = 'remove muah pleez';
@@ -231,10 +171,12 @@ describe('model: findOneAndDelete:', function() {
let query;
query = M.findByIdAndDelete(_id, { select: 'author -title' });
+ query._applyPaths();
assert.strictEqual(1, query._fields.author);
assert.strictEqual(0, query._fields.title);
query = M.findOneAndDelete({}, { select: 'author -title' });
+ query._applyPaths();
assert.strictEqual(1, query._fields.author);
assert.strictEqual(0, query._fields.title);
});
@@ -296,7 +238,6 @@ describe('model: findOneAndDelete:', function() {
const b = await N.create({ a: a._id, i: 10 });
-
const doc = await N.findOneAndDelete({ _id: b._id }, { select: 'a -_id' })
.populate('a')
.exec();
@@ -394,4 +335,26 @@ describe('model: findOneAndDelete:', function() {
assert.equal(postCount, 1);
});
});
+
+ it('supports the `includeResultMetadata` option (gh-13539)', async function() {
+ const testSchema = new mongoose.Schema({
+ name: String
+ });
+ const Test = db.model('Test', testSchema);
+ await Test.create({ name: 'Test' });
+ const doc = await Test.findOneAndDelete(
+ { name: 'Test' },
+ { includeResultMetadata: false }
+ );
+ assert.equal(doc.ok, undefined);
+ assert.equal(doc.name, 'Test');
+
+ await Test.create({ name: 'Test' });
+ const data = await Test.findOneAndDelete(
+ { name: 'Test' },
+ { includeResultMetadata: true }
+ );
+ assert(data.ok);
+ assert.equal(data.value.name, 'Test');
+ });
});
diff --git a/test/model.findOneAndRemove.test.js b/test/model.findOneAndRemove.test.js
deleted file mode 100644
index c966de4d669..00000000000
--- a/test/model.findOneAndRemove.test.js
+++ /dev/null
@@ -1,431 +0,0 @@
-'use strict';
-
-/**
- * Test dependencies.
- */
-
-const start = require('./common');
-
-const assert = require('assert');
-
-const mongoose = start.mongoose;
-const Schema = mongoose.Schema;
-const ObjectId = Schema.Types.ObjectId;
-const DocumentObjectId = mongoose.Types.ObjectId;
-
-describe('model: findOneAndRemove:', async function() {
- let Comments;
- let BlogPost;
- let db;
-
- before(function() {
- db = start();
- });
-
- after(async function() {
- await db.close();
- });
-
- beforeEach(() => db.deleteModel(/.*/));
- afterEach(() => require('./util').clearTestData(db));
- afterEach(() => require('./util').stopRemainingOps(db));
-
- beforeEach(function() {
- Comments = new Schema();
-
- Comments.add({
- title: String,
- date: Date,
- body: String,
- comments: [Comments]
- });
-
- BlogPost = new Schema({
- title: String,
- author: String,
- slug: String,
- date: Date,
- meta: {
- date: Date,
- visitors: Number
- },
- published: Boolean,
- mixed: {},
- numbers: [Number],
- owners: [ObjectId],
- comments: [Comments]
- });
-
- BlogPost.virtual('titleWithAuthor')
- .get(function() {
- return this.get('title') + ' by ' + this.get('author');
- })
- .set(function(val) {
- const split = val.split(' by ');
- this.set('title', split[0]);
- this.set('author', split[1]);
- });
-
- BlogPost.method('cool', function() {
- return this;
- });
-
- BlogPost.static('woot', function() {
- return this;
- });
-
- BlogPost = db.model('BlogPost', BlogPost);
- });
-
- it('returns the original document', async function() {
- const M = BlogPost;
- const title = 'remove muah';
-
- const post = new M({ title: title });
-
- await post.save();
-
- const doc = await M.findOneAndRemove({ title: title });
-
- assert.equal(post.id, doc.id);
-
- const gone = await M.findById(post.id);
- assert.equal(gone, null);
- });
-
- it('options/conditions/doc are merged when no callback is passed', function(done) {
- const M = BlogPost;
-
- const now = new Date();
- let query;
-
- // Model.findOneAndRemove
- query = M.findOneAndRemove({ author: 'aaron' }, { select: 'author' });
- assert.equal(query._fields.author, 1);
- assert.equal(query._conditions.author, 'aaron');
-
- query = M.findOneAndRemove({ author: 'aaron' });
- assert.equal(query._fields, undefined);
- assert.equal(query._conditions.author, 'aaron');
-
- query = M.findOneAndRemove();
- assert.equal(query.options.new, undefined);
- assert.equal(query._fields, undefined);
- assert.equal(query._conditions.author, undefined);
-
- // Query.findOneAndRemove
- query = M.where('author', 'aaron').findOneAndRemove({ date: now });
- assert.equal(query._fields, undefined);
- assert.equal(query._conditions.date, now);
- assert.equal(query._conditions.author, 'aaron');
-
- query = M.find().findOneAndRemove({ author: 'aaron' }, { select: 'author' });
- assert.equal(query._fields.author, 1);
- assert.equal(query._conditions.author, 'aaron');
-
- query = M.find().findOneAndRemove();
- assert.equal(query._fields, undefined);
- assert.equal(query._conditions.author, undefined);
- done();
- });
-
- it('executes when a callback is passed', function(done) {
- const M = BlogPost;
- let pending = 5;
-
- M.findOneAndRemove({ name: 'aaron1' }, { select: 'name' }, cb);
- M.findOneAndRemove({ name: 'aaron1' }, cb);
- M.where().findOneAndRemove({ name: 'aaron1' }, { select: 'name' }, cb);
- M.where().findOneAndRemove({ name: 'aaron1' }, cb);
- M.where('name', 'aaron1').findOneAndRemove(cb);
-
- function cb(err, doc) {
- assert.ifError(err);
- assert.equal(doc, null); // no previously existing doc
- if (--pending) return;
- done();
- }
- });
-
- it('executed with only a callback throws', function(done) {
- const M = BlogPost;
- let err;
-
- try {
- M.findOneAndRemove(function() {});
- } catch (e) {
- err = e;
- }
-
- assert.ok(/First argument must not be a function/.test(err));
- done();
- });
-
- it('executed with only a callback throws', function(done) {
- const M = BlogPost;
- let err;
-
- try {
- M.findByIdAndRemove(function() {});
- } catch (e) {
- err = e;
- }
-
- assert.ok(/First argument must not be a function/.test(err));
- done();
- });
-
- it('executes when a callback is passed', function(done) {
- const M = BlogPost;
- const _id = new DocumentObjectId();
- let pending = 2;
-
- M.findByIdAndRemove(_id, { select: 'name' }, cb);
- M.findByIdAndRemove(_id, cb);
-
- function cb(err, doc) {
- assert.ifError(err);
- assert.equal(doc, null); // no previously existing doc
- if (--pending) return;
- done();
- }
- });
-
- it('returns the original document', function(done) {
- const M = BlogPost;
- const title = 'remove muah pleez';
-
- const post = new M({ title: title });
- post.save(function(err) {
- assert.ifError(err);
- M.findByIdAndRemove(post.id, function(err, doc) {
- assert.ifError(err);
- assert.equal(post.id, doc.id);
- M.findById(post.id, function(err, gone) {
- assert.ifError(err);
- assert.equal(gone, null);
- done();
- });
- });
- });
- });
-
- it('options/conditions/doc are merged when no callback is passed', function(done) {
- const M = BlogPost;
- const _id = new DocumentObjectId();
-
- let query;
-
- // Model.findByIdAndRemove
- query = M.findByIdAndRemove(_id, { select: 'author' });
- assert.equal(query._fields.author, 1);
- assert.equal(query._conditions._id.toString(), _id.toString());
-
- query = M.findByIdAndRemove(_id.toString());
- assert.equal(query._fields, undefined);
- assert.equal(query._conditions._id, _id.toString());
-
- query = M.findByIdAndRemove();
- assert.equal(query.options.new, undefined);
- assert.equal(query._fields, undefined);
- assert.equal(query._conditions._id, undefined);
- done();
- });
-
- it('supports v3 select string syntax', function(done) {
- const M = BlogPost;
- const _id = new DocumentObjectId();
-
- let query;
-
- query = M.findByIdAndRemove(_id, { select: 'author -title' });
- assert.strictEqual(1, query._fields.author);
- assert.strictEqual(0, query._fields.title);
-
- query = M.findOneAndRemove({}, { select: 'author -title' });
- assert.strictEqual(1, query._fields.author);
- assert.strictEqual(0, query._fields.title);
- done();
- });
-
- it('supports v3 select object syntax', function(done) {
- const M = BlogPost;
- const _id = new DocumentObjectId();
-
- let query;
-
- query = M.findByIdAndRemove(_id, { select: { author: 1, title: 0 } });
- assert.strictEqual(1, query._fields.author);
- assert.strictEqual(0, query._fields.title);
-
- query = M.findOneAndRemove({}, { select: { author: 1, title: 0 } });
- assert.strictEqual(1, query._fields.author);
- assert.strictEqual(0, query._fields.title);
- done();
- });
-
- it('supports v3 sort string syntax', function(done) {
- const M = BlogPost;
- const _id = new DocumentObjectId();
-
- let query;
-
- query = M.findByIdAndRemove(_id, { sort: 'author -title' });
- assert.equal(Object.keys(query.options.sort).length, 2);
- assert.equal(query.options.sort.author, 1);
- assert.equal(query.options.sort.title, -1);
-
- query = M.findOneAndRemove({}, { sort: 'author -title' });
- assert.equal(Object.keys(query.options.sort).length, 2);
- assert.equal(query.options.sort.author, 1);
- assert.equal(query.options.sort.title, -1);
- done();
- });
-
- it('supports v3 sort object syntax', function(done) {
- const M = BlogPost;
- const _id = new DocumentObjectId();
-
- let query;
-
- query = M.findByIdAndRemove(_id, { sort: { author: 1, title: -1 } });
- assert.equal(Object.keys(query.options.sort).length, 2);
- assert.equal(query.options.sort.author, 1);
- assert.equal(query.options.sort.title, -1);
-
- query = M.findOneAndRemove(_id, { sort: { author: 1, title: -1 } });
- assert.equal(Object.keys(query.options.sort).length, 2);
- assert.equal(query.options.sort.author, 1);
- assert.equal(query.options.sort.title, -1);
- done();
- });
-
- it('supports population (gh-1395)', function(done) {
- const M = db.model('Test1', { name: String });
- const N = db.model('Test2', { a: { type: Schema.ObjectId, ref: 'Test1' }, i: Number });
-
- M.create({ name: 'i am an A' }, function(err, a) {
- if (err) return done(err);
- N.create({ a: a._id, i: 10 }, function(err, b) {
- if (err) return done(err);
-
- N.findOneAndRemove({ _id: b._id }, { select: 'a -_id' })
- .populate('a')
- .exec(function(err, doc) {
- if (err) return done(err);
- assert.ok(doc);
- assert.equal(doc._id, undefined);
- assert.ok(doc.a);
- assert.equal('i am an A', doc.a.name);
- done();
- });
- });
- });
- });
-
- it('only calls setters once (gh-6203)', async function() {
-
- const calls = [];
- const userSchema = new mongoose.Schema({
- name: String,
- foo: {
- type: String,
- set: function(val) {
- calls.push(val);
- return val + val;
- }
- }
- });
- const Model = db.model('Test', userSchema);
-
- await Model.findOneAndRemove({ foo: '123' }, { name: 'bar' });
-
- assert.deepEqual(calls, ['123']);
- });
-
- it('with orFail() (gh-9381)', function() {
- const User = db.model('User', Schema({ name: String }));
-
- return User.findOneAndRemove({ name: 'not found' }).orFail().
- then(() => null, err => err).
- then(err => {
- assert.ok(err);
- assert.equal(err.name, 'DocumentNotFoundError');
- });
- });
-
- describe('middleware', function() {
- it('works', function(done) {
- const s = new Schema({
- topping: { type: String, default: 'bacon' },
- base: String
- });
-
- let preCount = 0;
- s.pre('findOneAndRemove', function() {
- ++preCount;
- });
-
- let postCount = 0;
- s.post('findOneAndRemove', function() {
- ++postCount;
- });
-
- const Breakfast = db.model('Test', s);
- const breakfast = new Breakfast({
- base: 'eggs'
- });
-
- breakfast.save(function(error) {
- assert.ifError(error);
-
- Breakfast.findOneAndRemove(
- { base: 'eggs' },
- {},
- function(error, breakfast) {
- assert.ifError(error);
- assert.equal(breakfast.base, 'eggs');
- assert.equal(preCount, 1);
- assert.equal(postCount, 1);
- done();
- });
- });
- });
-
- it('works with exec() (gh-439)', function(done) {
- const s = new Schema({
- topping: { type: String, default: 'bacon' },
- base: String
- });
-
- let preCount = 0;
- s.pre('findOneAndRemove', function() {
- ++preCount;
- });
-
- let postCount = 0;
- s.post('findOneAndRemove', function() {
- ++postCount;
- });
-
- const Breakfast = db.model('Test', s);
- const breakfast = new Breakfast({
- base: 'eggs'
- });
-
- breakfast.save(function(error) {
- assert.ifError(error);
-
- Breakfast.
- findOneAndRemove({ base: 'eggs' }, {}).
- exec(function(error, breakfast) {
- assert.ifError(error);
- assert.equal(breakfast.base, 'eggs');
- assert.equal(preCount, 1);
- assert.equal(postCount, 1);
- done();
- });
- });
- });
- });
-});
diff --git a/test/model.findOneAndReplace.test.js b/test/model.findOneAndReplace.test.js
index f8188663f42..e8fa3764ca4 100644
--- a/test/model.findOneAndReplace.test.js
+++ b/test/model.findOneAndReplace.test.js
@@ -4,6 +4,7 @@
* Test dependencies.
*/
+const sinon = require('sinon');
const start = require('./common');
const assert = require('assert');
@@ -86,7 +87,6 @@ describe('model: findOneAndReplace:', function() {
await post.save();
const doc = await M.findOneAndReplace({ title: title });
-
assert.equal(post.id, doc.id);
});
@@ -125,68 +125,6 @@ describe('model: findOneAndReplace:', function() {
assert.equal(query._conditions.author, undefined);
});
- it('executes when a callback is passed', function(done) {
- const M = BlogPost;
- let pending = 5;
-
- M.findOneAndReplace({ name: 'aaron1' }, { select: 'name' }, cb);
- M.findOneAndReplace({ name: 'aaron1' }, cb);
- M.where().findOneAndReplace({ name: 'aaron1' }, { select: 'name' }, cb);
- M.where().findOneAndReplace({ name: 'aaron1' }, cb);
- M.where('name', 'aaron1').findOneAndReplace(cb);
-
- function cb(err, doc) {
- assert.ifError(err);
- assert.equal(doc, null); // no previously existing doc
- if (--pending) {
- return;
- }
- done();
- }
- });
-
- it('executed with only a callback throws', function() {
- const M = BlogPost;
- let err;
-
- try {
- M.findOneAndReplace(function() {});
- } catch (e) {
- err = e;
- }
-
- assert.ok(/First argument must not be a function/.test(err));
- });
-
- it('executed with only a callback throws', function() {
- const M = BlogPost;
- let err;
-
- try {
- M.findByIdAndDelete(function() {});
- } catch (e) {
- err = e;
- }
-
- assert.ok(/First argument must not be a function/.test(err));
- });
-
- it('executes when a callback is passed', function(done) {
- const M = BlogPost;
- const _id = new DocumentObjectId();
- let pending = 2;
-
- M.findByIdAndDelete(_id, { select: 'name' }, cb);
- M.findByIdAndDelete(_id, cb);
-
- function cb(err, doc) {
- assert.ifError(err);
- assert.equal(doc, null); // no previously existing doc
- if (--pending) return;
- done();
- }
- });
-
it('returns the original document', async function() {
const M = BlogPost;
const title = 'remove muah pleez';
@@ -226,6 +164,7 @@ describe('model: findOneAndReplace:', function() {
const M = BlogPost;
const query = M.findOneAndReplace({}, {}, { select: 'author -title' });
+ query._applyPaths();
assert.strictEqual(1, query._fields.author);
assert.strictEqual(0, query._fields.title);
});
@@ -415,7 +354,7 @@ describe('model: findOneAndReplace:', function() {
const testSchema = new Schema({
name: {
type: String,
- required: true // you had a typo here
+ required: true
}
});
const Test = db.model('Test', testSchema);
@@ -432,4 +371,109 @@ describe('model: findOneAndReplace:', function() {
const doc = await Test.findById(entry);
assert.strictEqual(doc.name, undefined);
});
+
+ it('respects query-level strict option (gh-13507)', async function() {
+ const testSchema = new Schema({
+ name: {
+ type: String,
+ required: true
+ }
+ });
+ const Test = db.model('Test', testSchema);
+
+ let err = await Test.findOneAndReplace(
+ { name: 'Test' },
+ { name: 'Bar', notInSchema: 'foo' },
+ { strict: 'throw' }
+ ).then(() => null, err => err);
+
+ assert.ok(err);
+ assert.ok(err.errors['notInSchema']);
+ assert.equal(err.errors['notInSchema'].name, 'StrictModeError');
+
+ err = await Test.findOneAndReplace(
+ { name: 'Test' },
+ { name: 'Bar', notInSchema: 'foo' },
+ { strict: 'throw', runValidators: true }
+ ).then(() => null, err => err);
+
+ assert.ok(err);
+ assert.ok(err.errors['notInSchema']);
+ assert.equal(err.errors['notInSchema'].name, 'StrictModeError');
+ });
+
+ it('respects schema-level strict option (gh-13507)', async function() {
+ const testSchema = new Schema({
+ name: {
+ type: String,
+ required: true
+ }
+ }, { strict: 'throw' });
+ const Test = db.model('Test', testSchema);
+
+ let err = await Test.findOneAndReplace(
+ { name: 'Test' },
+ { name: 'Bar', notInSchema: 'foo' },
+ {}
+ ).then(() => null, err => err);
+
+ assert.ok(err);
+ assert.ok(err.errors['notInSchema']);
+ assert.equal(err.errors['notInSchema'].name, 'StrictModeError');
+
+ err = await Test.findOneAndReplace(
+ { name: 'Test' },
+ { name: 'Bar', notInSchema: 'foo' },
+ { runValidators: true }
+ ).then(() => null, err => err);
+
+ assert.ok(err);
+ assert.ok(err.errors['notInSchema']);
+ assert.equal(err.errors['notInSchema'].name, 'StrictModeError');
+ });
+
+ it('does not send overwrite or timestamps option to MongoDB', async function() {
+ const testSchema = new Schema({
+ name: String
+ });
+ const Test = db.model('Test', testSchema);
+
+ sinon.stub(Test.collection, 'findOneAndReplace').callsFake(() => Promise.resolve({}));
+
+ await Test.findOneAndReplace(
+ { name: 'Test' },
+ {},
+ { timestamps: true }
+ );
+
+ assert.ok(Test.collection.findOneAndReplace.calledOnce);
+ const opts = Test.collection.findOneAndReplace.getCalls()[0].args[2];
+ assert.ok(!Object.keys(opts).includes('overwrite'));
+ assert.ok(!Object.keys(opts).includes('timestamps'));
+ });
+
+ it('supports the `includeResultMetadata` option (gh-13539)', async function() {
+ const testSchema = new mongoose.Schema({
+ name: String
+ });
+ const Test = db.model('Test', testSchema);
+ await Test.create({
+ name: 'Test'
+ });
+ const doc = await Test.findOneAndReplace(
+ { name: 'Test' },
+ { name: 'Test Testerson' },
+ { new: true, upsert: true, includeResultMetadata: false }
+ );
+ assert.equal(doc.ok, undefined);
+ assert.equal(doc.name, 'Test Testerson');
+
+ const data = await Test.findOneAndReplace(
+ { name: 'Test Testerson' },
+ { name: 'Test' },
+ { new: true, upsert: true, includeResultMetadata: true }
+ );
+ assert(data.ok);
+ assert.equal(data.value.name, 'Test');
+ });
});
diff --git a/test/model.findOneAndUpdate.test.js b/test/model.findOneAndUpdate.test.js
index 91b6f13ad72..55a1f48ac8a 100644
--- a/test/model.findOneAndUpdate.test.js
+++ b/test/model.findOneAndUpdate.test.js
@@ -83,7 +83,7 @@ describe('model: findOneAndUpdate:', function() {
BlogPost = db.model('BlogPost', BlogPost);
});
- it('returns the edited document', function(done) {
+ it('returns the edited document', async function() {
const M = BlogPost;
const title = 'Tobi ' + random();
const author = 'Brian ' + random();
@@ -102,62 +102,57 @@ describe('model: findOneAndUpdate:', function() {
post.owners = [id0, id1];
post.comments = [{ body: 'been there' }, { body: 'done that' }];
- post.save(function(err) {
- assert.ifError(err);
- M.findById(post._id, function(err, cf) {
- assert.ifError(err);
- assert.equal(cf.title, title);
- assert.equal(cf.author, author);
- assert.equal(cf.meta.visitors.valueOf(), 0);
- assert.equal(cf.date, post.date.toString());
- assert.equal(cf.published, true);
- assert.equal(cf.mixed.x, 'ex');
- assert.deepEqual([4, 5, 6, 7], cf.numbers.toObject());
- assert.equal(cf.owners.length, 2);
- assert.equal(cf.owners[0].toString(), id0.toString());
- assert.equal(cf.owners[1].toString(), id1.toString());
- assert.equal(cf.comments.length, 2);
- assert.equal(cf.comments[0].body, 'been there');
- assert.equal(cf.comments[1].body, 'done that');
- assert.ok(cf.comments[0]._id);
- assert.ok(cf.comments[1]._id);
- assert.ok(cf.comments[0]._id instanceof DocumentObjectId);
- assert.ok(cf.comments[1]._id instanceof DocumentObjectId);
-
- const update = {
- title: newTitle, // becomes $set
- $inc: { 'meta.visitors': 2 },
- $set: { date: new Date() },
- published: false, // becomes $set
- mixed: { x: 'ECKS', y: 'why' }, // $set
- $pullAll: { numbers: [4, 6] },
- $pull: { owners: id0 },
- 'comments.1.body': 8 // $set
- };
-
- M.findOneAndUpdate({ title: title }, update, { new: true }, function(err, up) {
- assert.equal(err && err.stack, err, null);
-
- assert.equal(up.title, newTitle);
- assert.equal(up.author, author);
- assert.equal(up.meta.visitors.valueOf(), 2);
- assert.equal(up.date.toString(), update.$set.date.toString());
- assert.equal(up.published, false);
- assert.equal(up.mixed.x, 'ECKS');
- assert.equal(up.mixed.y, 'why');
- assert.deepEqual([5, 7], up.numbers.toObject());
- assert.equal(up.owners.length, 1);
- assert.equal(up.owners[0].toString(), id1.toString());
- assert.equal(up.comments[0].body, 'been there');
- assert.equal(up.comments[1].body, '8');
- assert.ok(up.comments[0]._id);
- assert.ok(up.comments[1]._id);
- assert.ok(up.comments[0]._id instanceof DocumentObjectId);
- assert.ok(up.comments[1]._id instanceof DocumentObjectId);
- done();
- });
- });
- });
+ await post.save();
+
+ const cf = await M.findById(post._id);
+
+ assert.equal(cf.title, title);
+ assert.equal(cf.author, author);
+ assert.equal(cf.meta.visitors.valueOf(), 0);
+ assert.equal(cf.date, post.date.toString());
+ assert.equal(cf.published, true);
+ assert.equal(cf.mixed.x, 'ex');
+ assert.deepEqual([4, 5, 6, 7], cf.numbers.toObject());
+ assert.equal(cf.owners.length, 2);
+ assert.equal(cf.owners[0].toString(), id0.toString());
+ assert.equal(cf.owners[1].toString(), id1.toString());
+ assert.equal(cf.comments.length, 2);
+ assert.equal(cf.comments[0].body, 'been there');
+ assert.equal(cf.comments[1].body, 'done that');
+ assert.ok(cf.comments[0]._id);
+ assert.ok(cf.comments[1]._id);
+ assert.ok(cf.comments[0]._id instanceof DocumentObjectId);
+ assert.ok(cf.comments[1]._id instanceof DocumentObjectId);
+
+ const update = {
+ title: newTitle, // becomes $set
+ $inc: { 'meta.visitors': 2 },
+ $set: { date: new Date() },
+ published: false, // becomes $set
+ mixed: { x: 'ECKS', y: 'why' }, // $set
+ $pullAll: { numbers: [4, 6] },
+ $pull: { owners: id0 },
+ 'comments.1.body': 8 // $set
+ };
+
+ const up = await M.findOneAndUpdate({ title: title }, update, { new: true });
+
+ assert.equal(up.title, newTitle);
+ assert.equal(up.author, author);
+ assert.equal(up.meta.visitors.valueOf(), 2);
+ assert.equal(up.date.toString(), update.$set.date.toString());
+ assert.equal(up.published, false);
+ assert.equal(up.mixed.x, 'ECKS');
+ assert.equal(up.mixed.y, 'why');
+ assert.deepEqual([5, 7], up.numbers.toObject());
+ assert.equal(up.owners.length, 1);
+ assert.equal(up.owners[0].toString(), id1.toString());
+ assert.equal(up.comments[0].body, 'been there');
+ assert.equal(up.comments[1].body, '8');
+ assert.ok(up.comments[0]._id);
+ assert.ok(up.comments[1]._id);
+ assert.ok(up.comments[0]._id instanceof DocumentObjectId);
+ assert.ok(up.comments[1]._id instanceof DocumentObjectId);
});
describe('will correctly', function() {
@@ -183,7 +178,7 @@ describe('model: findOneAndUpdate:', function() {
ItemChildModel = db.model('Test2', itemSpec);
});
- it('update subdocument in array item', function(done) {
+ it('update subdocument in array item', async function() {
const item1 = new ItemChildModel({
address: {
street: 'times square',
@@ -203,27 +198,23 @@ describe('model: findOneAndUpdate:', function() {
}
});
const itemParent = new ItemParentModel({ items: [item1, item2, item3] });
- itemParent.save(function(err) {
- assert.ifError(err);
- ItemParentModel.findOneAndUpdate(
- { _id: itemParent._id, 'items.item_id': item1.item_id },
- { $set: { 'items.$.address': {} } },
- { new: true },
- function(err, updatedDoc) {
- assert.ifError(err);
- assert.ok(updatedDoc.items);
- assert.ok(updatedDoc.items instanceof Array);
- assert.ok(updatedDoc.items.length, 3);
- assert.ok(Utils.isObject(updatedDoc.items[0].address));
- assert.ok(Object.keys(updatedDoc.items[0].address).length, 0);
- done();
- }
- );
- });
+ await itemParent.save();
+
+ const updatedDoc = await ItemParentModel.findOneAndUpdate(
+ { _id: itemParent._id, 'items.item_id': item1.item_id },
+ { $set: { 'items.$.address': {} } },
+ { new: true }
+ );
+
+ assert.ok(updatedDoc.items);
+ assert.ok(updatedDoc.items instanceof Array);
+ assert.ok(updatedDoc.items.length, 3);
+ assert.ok(Utils.isObject(updatedDoc.items[0].address));
+ assert.ok(Object.keys(updatedDoc.items[0].address).length, 0);
});
});
- it('returns the original document', function(done) {
+ it('returns the original document', async function() {
const M = BlogPost;
const title = 'Tobi ' + random();
const author = 'Brian ' + random();
@@ -242,48 +233,40 @@ describe('model: findOneAndUpdate:', function() {
post.owners = [id0, id1];
post.comments = [{ body: 'been there' }, { body: 'done that' }];
- post.save(function(err) {
- assert.ifError(err);
- M.findById(post._id, function(err) {
- assert.ifError(err);
-
- const update = {
- title: newTitle, // becomes $set
- $inc: { 'meta.visitors': 2 },
- $set: { date: new Date() },
- published: false, // becomes $set
- mixed: { x: 'ECKS', y: 'why' }, // $set
- $pullAll: { numbers: [4, 6] },
- $pull: { owners: id0 },
- 'comments.1.body': 8 // $set
- };
-
- M.findOneAndUpdate({ title: title }, update, { new: false }, function(err, up) {
- assert.ifError(err);
-
- assert.equal(up.title, post.title);
- assert.equal(up.author, post.author);
- assert.equal(up.meta.visitors.valueOf(), post.meta.visitors);
- assert.equal(post.date.toString(), up.date.toString());
- assert.equal(post.published, up.published);
- assert.equal(post.mixed.x, up.mixed.x);
- assert.equal(post.mixed.y, up.mixed.y);
- assert.deepEqual(up.numbers.toObject(), post.numbers.toObject());
- assert.equal(post.owners.length, up.owners.length);
- assert.equal(post.owners[0].toString(), up.owners[0].toString());
- assert.equal(post.comments[0].body, up.comments[0].body);
- assert.equal(post.comments[1].body, up.comments[1].body);
- assert.ok(up.comments[0]._id);
- assert.ok(up.comments[1]._id);
- assert.ok(up.comments[0]._id instanceof DocumentObjectId);
- assert.ok(up.comments[1]._id instanceof DocumentObjectId);
- done();
- });
- });
- });
+ await post.save();
+
+ const update = {
+ title: newTitle, // becomes $set
+ $inc: { 'meta.visitors': 2 },
+ $set: { date: new Date() },
+ published: false, // becomes $set
+ mixed: { x: 'ECKS', y: 'why' }, // $set
+ $pullAll: { numbers: [4, 6] },
+ $pull: { owners: id0 },
+ 'comments.1.body': 8 // $set
+ };
+
+ const up = await M.findOneAndUpdate({ title: title }, update, { new: false });
+
+ assert.equal(up.title, post.title);
+ assert.equal(up.author, post.author);
+ assert.equal(up.meta.visitors.valueOf(), post.meta.visitors);
+ assert.equal(post.date.toString(), up.date.toString());
+ assert.equal(post.published, up.published);
+ assert.equal(post.mixed.x, up.mixed.x);
+ assert.equal(post.mixed.y, up.mixed.y);
+ assert.deepEqual(up.numbers.toObject(), post.numbers.toObject());
+ assert.equal(post.owners.length, up.owners.length);
+ assert.equal(post.owners[0].toString(), up.owners[0].toString());
+ assert.equal(post.comments[0].body, up.comments[0].body);
+ assert.equal(post.comments[1].body, up.comments[1].body);
+ assert.ok(up.comments[0]._id);
+ assert.ok(up.comments[1]._id);
+ assert.ok(up.comments[0]._id instanceof DocumentObjectId);
+ assert.ok(up.comments[1]._id instanceof DocumentObjectId);
});
- it('allows upserting', function(done) {
+ it('allows upserting', async function() {
const M = BlogPost;
const title = 'Tobi ' + random();
const author = 'Brian ' + random();
@@ -312,21 +295,18 @@ describe('model: findOneAndUpdate:', function() {
$pull: { owners: id0 }
};
- M.findOneAndUpdate({ title: title }, update, { upsert: true, new: true }, function(err, up) {
- assert.ifError(err);
-
- assert.equal(up.title, newTitle);
- assert.equal(up.meta.visitors.valueOf(), 2);
- assert.equal(update.$set.date.toString(), up.date.toString());
- assert.equal(up.published, update.published);
- assert.deepEqual(update.mixed.x, up.mixed.x);
- assert.strictEqual(up.mixed.y, update.mixed.y);
- assert.ok(Array.isArray(up.numbers));
- assert.ok(Array.isArray(up.owners));
- assert.strictEqual(0, up.numbers.length);
- assert.strictEqual(0, up.owners.length);
- done();
- });
+ const up = await M.findOneAndUpdate({ title: title }, update, { upsert: true, new: true });
+
+ assert.equal(up.title, newTitle);
+ assert.equal(up.meta.visitors.valueOf(), 2);
+ assert.equal(update.$set.date.toString(), up.date.toString());
+ assert.equal(up.published, update.published);
+ assert.deepEqual(update.mixed.x, up.mixed.x);
+ assert.strictEqual(up.mixed.y, update.mixed.y);
+ assert.ok(Array.isArray(up.numbers));
+ assert.ok(Array.isArray(up.owners));
+ assert.strictEqual(0, up.numbers.length);
+ assert.strictEqual(0, up.owners.length);
});
it('options/conditions/doc are merged when no callback is passed', function(done) {
@@ -379,90 +359,18 @@ describe('model: findOneAndUpdate:', function() {
done();
});
- it('executes when a callback is passed', function(done) {
- const M = BlogPost;
- let pending = 6;
-
- M.findOneAndUpdate({ name: 'aaron' }, { $set: { name: 'Aaron6' } }, { new: false }, cb);
- M.findOneAndUpdate({ name: 'aaron' }, { $set: { name: 'Aaron4' } }, cb);
- M.where().findOneAndUpdate({ name: 'aaron' }, { $set: { name: 'Aaron1' } }, { new: false }, cb);
- M.where().findOneAndUpdate({ name: 'aaron' }, { $set: { name: 'Aaron2' } }, cb);
- M.where().findOneAndUpdate({ $set: { name: 'Aaron6' } }, cb);
- M.where('name', 'aaron').findOneAndUpdate({ $set: { name: 'Aaron' } }).findOneAndUpdate(cb);
-
- function cb(err, doc) {
- assert.ifError(err);
- assert.strictEqual(null, doc); // not an upsert, no previously existing doc
- if (--pending) {
- return;
- }
- done();
- }
- });
-
- it('executes when a callback is passed to a succeeding function', function(done) {
- const M = BlogPost;
- let pending = 6;
-
- M.findOneAndUpdate({ name: 'aaron' }, { $set: { name: 'Aaron' } }, { new: false }).exec(cb);
- M.findOneAndUpdate({ name: 'aaron' }, { $set: { name: 'Aaron' } }).exec(cb);
- M.where().findOneAndUpdate({ name: 'aaron' }, { $set: { name: 'Aaron' } }, { new: false }).exec(cb);
- M.where().findOneAndUpdate({ name: 'aaron' }, { $set: { name: 'Aaron' } }).exec(cb);
- M.where().findOneAndUpdate({ $set: { name: 'Aaron' } }).exec(cb);
- M.where('name', 'aaron').findOneAndUpdate({ $set: { name: 'Aaron' } }).exec(cb);
-
- function cb(err, doc) {
- assert.ifError(err);
- assert.strictEqual(null, doc); // not an upsert, no previously existing doc
- if (--pending) {
- return;
- }
- done();
- }
- });
-
- it('executing with only a callback throws', function(done) {
- const M = BlogPost;
- let err;
-
- try {
- M.findOneAndUpdate(function() {
- });
- } catch (e) {
- err = e;
- }
-
- assert.ok(/First argument must not be a function/.test(err));
- done();
- });
-
- it('updates numbers atomically', function(done) {
- let totalDocs = 4;
-
+ it('updates numbers atomically', async function() {
const post = new BlogPost();
post.set('meta.visitors', 5);
- post.save(function(err) {
- assert.ifError(err);
-
- function callback(err) {
- assert.ifError(err);
- --totalDocs || complete();
- }
+ await post.save();
- for (let i = 0; i < 4; ++i) {
- BlogPost
- .findOneAndUpdate({ _id: post._id }, { $inc: { 'meta.visitors': 1 } }, callback);
- }
+ await Promise.all(Array(4).fill(null).map(async() => {
+ await BlogPost.findOneAndUpdate({ _id: post._id }, { $inc: { 'meta.visitors': 1 } });
+ }));
- function complete() {
- BlogPost.findOne({ _id: post.get('_id') }, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.get('meta.visitors'), 9);
- done();
- });
- }
- });
+ const doc = await BlogPost.findOne({ _id: post.get('_id') });
+ assert.equal(doc.get('meta.visitors'), 9);
});
it('honors strict schemas', async function() {
@@ -511,55 +419,6 @@ describe('model: findOneAndUpdate:', function() {
assert.ok(/not in schema/.test(err2));
});
- it('executing with just a callback throws', function() {
- const M = BlogPost;
- let err;
-
- try {
- M.findByIdAndUpdate(function() {});
- } catch (e) {
- err = e;
- }
-
- assert.ok(/First argument must not be a function/.test(err));
- });
-
- it('executes when a callback is passed', function(done) {
- const M = BlogPost;
- const _id = new DocumentObjectId();
- let pending = 2;
-
- M.findByIdAndUpdate(_id, { $set: { name: 'Aaron' } }, { new: false }, cb);
- M.findByIdAndUpdate(_id, { $set: { name: 'changed' } }, cb);
-
- function cb(err, doc) {
- assert.ifError(err);
- assert.strictEqual(null, doc); // no previously existing doc
- if (--pending) {
- return;
- }
- done();
- }
- });
-
- it('executes when a callback is passed to a succeeding function', function(done) {
- const M = BlogPost;
- const _id = new DocumentObjectId();
- let pending = 2;
-
- M.findByIdAndUpdate(_id, { $set: { name: 'Aaron' } }, { new: false }).exec(cb);
- M.findByIdAndUpdate(_id, { $set: { name: 'changed' } }).exec(cb);
-
- function cb(err, doc) {
- assert.ifError(err);
- assert.strictEqual(null, doc); // no previously existing doc
- if (--pending) {
- return;
- }
- done();
- }
- });
-
it('returns the original document', async function() {
const M = BlogPost;
const title = 'Tobi ' + random();
@@ -652,10 +511,12 @@ describe('model: findOneAndUpdate:', function() {
let query;
query = M.findByIdAndUpdate(_id, { $set: { date: now } }, { select: 'author -title' });
+ query._applyPaths();
assert.strictEqual(1, query._fields.author);
assert.strictEqual(0, query._fields.title);
query = M.findOneAndUpdate({}, { $set: { date: now } }, { select: 'author -title' });
+ query._applyPaths();
assert.strictEqual(1, query._fields.author);
assert.strictEqual(0, query._fields.title);
});
@@ -726,7 +587,7 @@ describe('model: findOneAndUpdate:', function() {
done();
});
- it('supports $elemMatch with $in (gh-1091 gh-1100)', function(done) {
+ it('supports $elemMatch with $in (gh-1091 gh-1100)', async function() {
const postSchema = new Schema({
ids: [{ type: Schema.ObjectId }],
title: String
@@ -737,52 +598,37 @@ describe('model: findOneAndUpdate:', function() {
const _id1 = new mongoose.Types.ObjectId();
const _id2 = new mongoose.Types.ObjectId();
- B.create({ ids: [_id1, _id2] }, function(err, doc) {
- assert.ifError(err);
-
- B
- .findByIdAndUpdate(doc._id, { title: 'woot' }, { new: true })
- .select({ title: 1, ids: { $elemMatch: { $in: [_id2.toString()] } } })
- .exec(function(err, found) {
- assert.ifError(err);
- assert.ok(found);
- assert.equal(doc.id, found.id);
- assert.equal(found.title, 'woot');
- assert.equal(found.ids.length, 1);
- assert.equal(found.ids[0].toString(), _id2.toString());
- done();
- });
- });
+ const doc = await B.create({ ids: [_id1, _id2] });
+
+ const found = await B
+ .findByIdAndUpdate(doc._id, { title: 'woot' }, { new: true })
+ .select({ title: 1, ids: { $elemMatch: { $in: [_id2.toString()] } } })
+ .exec();
+
+ assert.ok(found);
+ assert.equal(doc.id, found.id);
+ assert.equal(found.title, 'woot');
+ assert.equal(found.ids.length, 1);
+ assert.equal(found.ids[0].toString(), _id2.toString());
});
- it('supports population (gh-1395)', function(done) {
+ it('supports population (gh-1395)', async function() {
const M = db.model('Test1', { name: String });
const N = db.model('Test2', { a: { type: Schema.ObjectId, ref: 'Test1' }, i: Number });
- M.create({ name: 'i am an A' }, function(err, a) {
- if (err) {
- return done(err);
- }
- N.create({ a: a._id, i: 10 }, function(err, b) {
- if (err) {
- return done(err);
- }
+ const a = await M.create({ name: 'i am an A' });
+ const b = await N.create({ a: a._id, i: 10 });
- N.findOneAndUpdate({ _id: b._id }, { $inc: { i: 1 } })
- .populate('a')
- .exec(function(err, doc) {
- if (err) {
- return done(err);
- }
- assert.ok(doc);
- assert.ok(doc.a);
- assert.equal('i am an A', doc.a.name);
- done();
- });
- });
- });
+ const doc = await N.findOneAndUpdate({ _id: b._id }, { $inc: { i: 1 } })
+ .populate('a')
+ .exec();
+
+ assert.ok(doc);
+ assert.ok(doc.a);
+ assert.equal('i am an A', doc.a.name);
});
- it('returns null when doing an upsert & new=false gh-1533', function(done) {
+
+ it('returns null when doing an upsert & new=false gh-1533', async function() {
const thingSchema = new Schema({
_id: String,
flag: {
@@ -794,17 +640,14 @@ describe('model: findOneAndUpdate:', function() {
const Thing = db.model('Test', thingSchema);
const key = 'some-id';
- Thing.findOneAndUpdate({ _id: key }, { $set: { flag: false } }, { upsert: true, new: false }).exec(function(err, thing) {
- assert.ifError(err);
- assert.equal(thing, null);
- Thing.findOneAndUpdate({ _id: key }, { $set: { flag: false } }, { upsert: true, new: false }).exec(function(err, thing2) {
- assert.ifError(err);
- assert.equal(thing2.id, key);
- assert.equal(thing2.flag, false);
- done();
- });
- });
+ const thing = await Thing.findOneAndUpdate({ _id: key }, { $set: { flag: false } }, { upsert: true, new: false }).exec();
+ assert.equal(thing, null);
+
+ const thing2 = await Thing.findOneAndUpdate({ _id: key }, { $set: { flag: false } }, { upsert: true, new: false }).exec();
+ assert.equal(thing2.id, key);
+ assert.equal(thing2.flag, false);
});
+
it('return hydrated document (gh-7734 gh-7735)', async function() {
const fruitSchema = new Schema({
name: { type: String }
@@ -822,7 +665,8 @@ describe('model: findOneAndUpdate:', function() {
{ new: true });
assert.ok(fruit instanceof mongoose.Document);
});
- it('return rawResult when doing an upsert & new=false gh-7770', function(done) {
+
+ it('return includeResultMetadata when doing an upsert & new=false gh-7770', async function() {
const thingSchema = new Schema({
_id: String,
flag: {
@@ -834,60 +678,30 @@ describe('model: findOneAndUpdate:', function() {
const Thing = db.model('Test', thingSchema);
const key = 'some-new-id';
- Thing.findOneAndUpdate({ _id: key }, { $set: { flag: false } }, { upsert: true, new: false, rawResult: true }).exec(function(err, rawResult) {
- assert.ifError(err);
- assert.equal(rawResult.lastErrorObject.updatedExisting, false);
- Thing.findOneAndUpdate({ _id: key }, { $set: { flag: true } }, { upsert: true, new: false, rawResult: true }).exec(function(err, rawResult2) {
- assert.ifError(err);
- assert.equal(rawResult2.lastErrorObject.updatedExisting, true);
- assert.equal(rawResult2.value._id, key);
- assert.equal(rawResult2.value.flag, false);
- done();
- });
- });
+ const res1 = await Thing.findOneAndUpdate({ _id: key }, { $set: { flag: false } }, { upsert: true, new: false, includeResultMetadata: true }).exec();
+ assert.equal(res1.lastErrorObject.updatedExisting, false);
+
+ const res2 = await Thing.findOneAndUpdate({ _id: key }, { $set: { flag: true } }, { upsert: true, new: false, includeResultMetadata: true }).exec();
+ assert.equal(res2.lastErrorObject.updatedExisting, true);
+ assert.equal(res2.value._id, key);
+ assert.equal(res2.value.flag, false);
});
- it('allows properties to be set to null gh-1643', function(done) {
+
+ it('allows properties to be set to null gh-1643', async function() {
const testSchema = new Schema({
name: [String]
});
const Test = db.model('Test', testSchema);
- Test.create({ name: ['Test'] }, function(err, thing) {
- if (err) {
- return done(err);
- }
- Test.findOneAndUpdate({ _id: thing._id }, { name: null }, { new: true })
- .exec(function(err, doc) {
- if (err) {
- return done(err);
- }
- assert.ok(doc);
- assert.equal(null, doc.name);
- done();
- });
- });
- });
-
- it('honors the overwrite option (gh-1809)', function(done) {
- const M = db.model('Test', { name: String, change: Boolean });
- M.create({ name: 'first' }, function(err, doc) {
- if (err) {
- return done(err);
- }
- M.findByIdAndUpdate(doc._id, { change: true }, { overwrite: true, new: true }, function(err, doc) {
- if (err) {
- return done(err);
- }
- assert.ok(doc.change);
- assert.equal(doc.name, undefined);
- done();
- });
- });
+ const thing = await Test.create({ name: ['Test'] });
+ const doc = await Test.findOneAndUpdate({ _id: thing._id }, { name: null }, { new: true });
+ assert.ok(doc);
+ assert.equal(null, doc.name);
});
- it('can do various deep equal checks (lodash.isEqual, lodash.isEqualWith, assert.deepEqual, utils.deepEqual) on object id after findOneAndUpdate (gh-2070)', function(done) {
+ it('can do various deep equal checks (lodash.isEqual, lodash.isEqualWith, assert.deepEqual, utils.deepEqual) on object id after findOneAndUpdate (gh-2070)', async function() {
const userSchema = new Schema({
name: String,
contacts: [{
@@ -901,35 +715,29 @@ describe('model: findOneAndUpdate:', function() {
const a1 = new User({ name: 'parent' });
const a2 = new User({ name: 'child' });
- a1.save(function(error) {
- assert.ifError(error);
- a2.save(function(error, a2) {
- assert.ifError(error);
- User.findOneAndUpdate(
- { name: 'parent' },
- { $push: { contacts: { account: a2._id, name: 'child' } } },
- { new: true },
- function(error, doc) {
- assert.ifError(error);
- assert.deepEqual(doc.contacts[0].account, a2._id);
- assert.ok(Utils.deepEqual(doc.contacts[0].account, a2._id));
- assert.ok(isEqualWith(doc.contacts[0].account, a2._id, compareBuffers));
- // Re: commends on https://github.com/mongodb/js-bson/commit/aa0b54597a0af28cce3530d2144af708e4b66bf0
- // Deep equality checks no longer work as expected with node 0.10.
- // Please file an issue if this is a problem for you
- assert.ok(isEqual(doc.contacts[0].account, a2._id));
-
- User.findOne({ name: 'parent' }, function(error, doc) {
- assert.ifError(error);
- assert.deepEqual(doc.contacts[0].account, a2._id);
- assert.ok(Utils.deepEqual(doc.contacts[0].account, a2._id));
- assert.ok(isEqualWith(doc.contacts[0].account, a2._id, compareBuffers));
- assert.ok(isEqual(doc.contacts[0].account, a2._id));
- done();
- });
- });
- });
- });
+ await a1.save();
+ await a2.save();
+
+ const doc = await User.findOneAndUpdate(
+ { name: 'parent' },
+ { $push: { contacts: { account: a2._id, name: 'child' } } },
+ { new: true }
+ );
+
+ assert.deepEqual(doc.contacts[0].account, a2._id);
+ assert.ok(Utils.deepEqual(doc.contacts[0].account, a2._id));
+ assert.ok(isEqualWith(doc.contacts[0].account, a2._id, compareBuffers));
+ // Re: commends on https://github.com/mongodb/js-bson/commit/aa0b54597a0af28cce3530d2144af708e4b66bf0
+ // Deep equality checks no longer work as expected with node 0.10.
+ // Please file an issue if this is a problem for you
+ assert.ok(isEqual(doc.contacts[0].account, a2._id));
+
+ const doc2 = await User.findOne({ name: 'parent' });
+
+ assert.deepEqual(doc2.contacts[0].account, a2._id);
+ assert.ok(Utils.deepEqual(doc2.contacts[0].account, a2._id));
+ assert.ok(isEqualWith(doc2.contacts[0].account, a2._id, compareBuffers));
+ assert.ok(isEqual(doc2.contacts[0].account, a2._id));
function compareBuffers(a, b) {
if (Buffer.isBuffer(a) && Buffer.isBuffer(b)) {
@@ -938,29 +746,26 @@ describe('model: findOneAndUpdate:', function() {
}
});
- it('adds __v on upsert (gh-2122) (gh-4505)', function(done) {
+ it('adds __v on upsert (gh-2122) (gh-4505)', async function() {
const accountSchema = new Schema({
name: String
});
const Account = db.model('Test', accountSchema);
- Account.findOneAndUpdate(
+ const doc = await Account.findOneAndUpdate(
{ name: 'account' },
{ name: 'test' },
- { upsert: true, new: true },
- function(error, doc) {
- assert.ifError(error);
- assert.equal(doc.__v, 0);
- Account.replaceOne({ name: 'test' }, { name: 'test' }, { upsert: true }, function(error) {
- assert.ifError(error);
- Account.findOne({ name: 'test' }, function(error, doc) {
- assert.ifError(error);
- assert.equal(doc.__v, 0);
- done();
- });
- });
- });
+ { upsert: true, new: true }
+ );
+
+ assert.equal(doc.__v, 0);
+
+ await Account.replaceOne({ name: 'test' }, { name: 'test' }, { upsert: true });
+
+ const doc2 = await Account.findOne({ name: 'test' });
+
+ assert.equal(doc2.__v, 0);
});
it('doesn\'t add __v on upsert if `$set` (gh-4505) (gh-5973)', function() {
@@ -991,28 +796,20 @@ describe('model: findOneAndUpdate:', function() {
then(doc => assert.strictEqual(doc.__v, 1));
});
- it('works with nested schemas and $pull+$or (gh-1932)', function(done) {
+ it('works with nested schemas and $pull+$or (gh-1932)', async function() {
const TickSchema = new Schema({ name: String });
const TestSchema = new Schema({ a: Number, b: Number, ticks: [TickSchema] });
const TestModel = db.model('Test', TestSchema);
- TestModel.create({ a: 1, b: 0, ticks: [{ name: 'eggs' }, { name: 'bacon' }, { name: 'coffee' }] }, function(error) {
- assert.ifError(error);
- TestModel.findOneAndUpdate({ a: 1 }, { $pull: { ticks: { $or: [{ name: 'eggs' }, { name: 'bacon' }] } } },
- function(error) {
- assert.ifError(error);
- TestModel.findOne({}, function(error, doc) {
- assert.ifError(error);
- assert.equal(doc.ticks.length, 1);
- assert.equal(doc.ticks[0].name, 'coffee');
- done();
- });
- });
- });
+ await TestModel.create({ a: 1, b: 0, ticks: [{ name: 'eggs' }, { name: 'bacon' }, { name: 'coffee' }] });
+ await TestModel.findOneAndUpdate({ a: 1 }, { $pull: { ticks: { $or: [{ name: 'eggs' }, { name: 'bacon' }] } } });
+ const doc = await TestModel.findOne({});
+ assert.equal(doc.ticks.length, 1);
+ assert.equal(doc.ticks[0].name, 'coffee');
});
- it('accepts undefined', function(done) {
+ it('accepts undefined', async function() {
const s = new Schema({
time: Date,
base: String
@@ -1020,48 +817,43 @@ describe('model: findOneAndUpdate:', function() {
const Breakfast = db.model('Test', s);
- Breakfast.
- findOneAndUpdate({}, { time: undefined, base: undefined }, {}).
- exec(function(error) {
- assert.ifError(error);
- done();
- });
+ await Breakfast.findOneAndUpdate({}, { time: undefined, base: undefined }, {});
});
- it('cast errors for empty objects as object ids (gh-2732)', function(done) {
+ it('cast errors for empty objects as object ids (gh-2732)', async function() {
const s = new Schema({
base: ObjectId
});
const Breakfast = db.model('Test', s);
- Breakfast.
+ const err = await Breakfast.
findOneAndUpdate({}, { base: {} }, {}).
- exec(function(error) {
- assert.ok(error);
- done();
- });
+ exec().
+ then(() => null, err => err);
+ assert.ok(err);
+ assert.equal(err.name, 'CastError');
});
- it('strict mode with objects (gh-2947)', function(done) {
+ it('cast errors for empty objects as object ids (gh-2732)', async function() {
const s = new Schema({
- test: String
- }, { strict: true });
+ base: ObjectId
+ });
const Breakfast = db.model('Test', s);
- const q = Breakfast.findOneAndUpdate({},
- { notInSchema: { a: 1 }, test: 'abc' },
- { new: true, strict: true, upsert: true });
- q.lean();
- q.exec(function(error, doc) {
- assert.ok(!doc.notInSchema);
- done();
- });
+ try {
+ await Breakfast.findOneAndUpdate({}, { base: {} }, {});
+
+ assert.ok(false);
+ } catch (error) {
+ assert.ok(error);
+ assert.equal(error.name, 'CastError');
+ }
});
describe('middleware', function() {
- it('works', function(done) {
+ it('works', async function() {
const s = new Schema({
topping: { type: String, default: 'bacon' },
base: String
@@ -1079,19 +871,12 @@ describe('model: findOneAndUpdate:', function() {
const Breakfast = db.model('Test', s);
- Breakfast.findOneAndUpdate(
- {},
- { base: 'eggs' },
- {},
- function(error) {
- assert.ifError(error);
- assert.equal(preCount, 1);
- assert.equal(postCount, 1);
- done();
- });
+ await Breakfast.findOneAndUpdate({}, { base: 'eggs' }, {});
+ assert.equal(preCount, 1);
+ assert.equal(postCount, 1);
});
- it('works with exec()', function(done) {
+ it('works with exec()', async function() {
const s = new Schema({
topping: { type: String, default: 'bacon' },
base: String
@@ -1109,19 +894,14 @@ describe('model: findOneAndUpdate:', function() {
const Breakfast = db.model('Test', s);
- Breakfast.
- findOneAndUpdate({}, { base: 'eggs' }, {}).
- exec(function(error) {
- assert.ifError(error);
- assert.equal(preCount, 1);
- assert.equal(postCount, 1);
- done();
- });
+ await Breakfast.findOneAndUpdate({}, { base: 'eggs' }, {}).exec();
+ assert.equal(preCount, 1);
+ assert.equal(postCount, 1);
});
});
describe('validators (gh-860)', function() {
- it('applies defaults on upsert', function(done) {
+ it('applies defaults on upsert', async function() {
const s = new Schema({
topping: { type: String, default: 'bacon' },
base: String
@@ -1129,23 +909,19 @@ describe('model: findOneAndUpdate:', function() {
const Breakfast = db.model('Test', s);
const updateOptions = { upsert: true, new: true };
- Breakfast.findOneAndUpdate(
+ const breakfast = await Breakfast.findOneAndUpdate(
{},
{ base: 'eggs' },
- updateOptions,
- function(error, breakfast) {
- assert.ifError(error);
- assert.equal(breakfast.base, 'eggs');
- assert.equal(breakfast.topping, 'bacon');
- Breakfast.countDocuments({ topping: 'bacon' }, function(error, count) {
- assert.ifError(error);
- assert.equal(1, count);
- done();
- });
- });
+ updateOptions
+ );
+ assert.equal(breakfast.base, 'eggs');
+ assert.equal(breakfast.topping, 'bacon');
+
+ const count = await Breakfast.countDocuments({ topping: 'bacon' });
+ assert.equal(1, count);
});
- it('doesnt set default on upsert if query sets it', function(done) {
+ it('doesnt set default on upsert if query sets it', async function() {
const s = new Schema({
topping: { type: String, default: 'bacon' },
numEggs: { type: Number, default: 3 },
@@ -1154,20 +930,17 @@ describe('model: findOneAndUpdate:', function() {
const Breakfast = db.model('Test', s);
const updateOptions = { upsert: true, new: true };
- Breakfast.findOneAndUpdate(
+ const breakfast = await Breakfast.findOneAndUpdate(
{ topping: 'sausage', numEggs: 4 },
{ base: 'eggs' },
- updateOptions,
- function(error, breakfast) {
- assert.ifError(error);
- assert.equal(breakfast.base, 'eggs');
- assert.equal(breakfast.topping, 'sausage');
- assert.equal(breakfast.numEggs, 4);
- done();
- });
+ updateOptions
+ );
+ assert.equal(breakfast.base, 'eggs');
+ assert.equal(breakfast.topping, 'sausage');
+ assert.equal(breakfast.numEggs, 4);
});
- it('properly sets default on upsert if query wont set it', function(done) {
+ it('properly sets default on upsert if query wont set it', async function() {
const s = new Schema({
topping: { type: String, default: 'bacon' },
base: String
@@ -1175,20 +948,16 @@ describe('model: findOneAndUpdate:', function() {
const Breakfast = db.model('Test', s);
const updateOptions = { upsert: true, new: true };
- Breakfast.findOneAndUpdate(
+ const breakfast = await Breakfast.findOneAndUpdate(
{ topping: { $ne: 'sausage' } },
{ base: 'eggs' },
- updateOptions,
- function(error, breakfast) {
- assert.ifError(error);
- assert.equal(breakfast.base, 'eggs');
- assert.equal(breakfast.topping, 'bacon');
- Breakfast.countDocuments({ topping: 'bacon' }, function(error, count) {
- assert.ifError(error);
- assert.equal(1, count);
- done();
- });
- });
+ updateOptions
+ );
+ assert.equal(breakfast.base, 'eggs');
+ assert.equal(breakfast.topping, 'bacon');
+
+ const count = await Breakfast.countDocuments({ topping: 'bacon' });
+ assert.equal(1, count);
});
it('skips setting defaults within maps (gh-7909)', async function() {
@@ -1210,7 +979,7 @@ describe('model: findOneAndUpdate:', function() {
assert.equal(doc.socialMediaHandles, undefined);
});
- it('runs validators if theyre set', function(done) {
+ it('runs validators if theyre set', async function() {
const s = new Schema({
topping: {
type: String,
@@ -1232,23 +1001,19 @@ describe('model: findOneAndUpdate:', function() {
runValidators: true,
new: true
};
- Breakfast.findOneAndUpdate(
+ const error = await Breakfast.findOneAndUpdate(
{},
{ topping: 'bacon', base: 'eggs' },
- updateOptions,
- function(error, breakfast) {
- assert.ok(!!error);
- assert.ok(!breakfast);
- assert.equal(Object.keys(error.errors).length, 1);
- assert.equal(Object.keys(error.errors)[0], 'topping');
- assert.equal(error.errors.topping.message, 'Validator failed for path `topping` with value `bacon`');
-
- assert.ok(!breakfast);
- done();
- });
+ updateOptions
+ ).then(() => null, err => err);
+
+ assert.ok(error);
+ assert.equal(Object.keys(error.errors).length, 1);
+ assert.equal(Object.keys(error.errors)[0], 'topping');
+ assert.equal(error.errors.topping.message, 'Validator failed for path `topping` with value `bacon`');
});
- it('validators handle $unset and $setOnInsert', function(done) {
+ it('validators handle $unset and $setOnInsert', async function() {
const s = new Schema({
steak: { type: String, required: true },
eggs: {
@@ -1260,23 +1025,21 @@ describe('model: findOneAndUpdate:', function() {
const Breakfast = db.model('Test', s);
const updateOptions = { runValidators: true, new: true };
- Breakfast.findOneAndUpdate(
+ const error = await Breakfast.findOneAndUpdate(
{},
{ $unset: { steak: '' }, $setOnInsert: { eggs: 'softboiled' } },
- updateOptions,
- function(error, breakfast) {
- assert.ok(!!error);
- assert.ok(!breakfast);
- assert.equal(Object.keys(error.errors).length, 2);
- assert.ok(Object.keys(error.errors).indexOf('eggs') !== -1);
- assert.ok(Object.keys(error.errors).indexOf('steak') !== -1);
- assert.equal(error.errors.eggs.message, 'Validator failed for path `eggs` with value `softboiled`');
- assert.equal(error.errors.steak.message, 'Path `steak` is required.');
- done();
- });
+ updateOptions
+ ).then(() => null, err => err);
+
+ assert.ok(error);
+ assert.equal(Object.keys(error.errors).length, 2);
+ assert.ok(Object.keys(error.errors).indexOf('eggs') !== -1);
+ assert.ok(Object.keys(error.errors).indexOf('steak') !== -1);
+ assert.equal(error.errors.eggs.message, 'Validator failed for path `eggs` with value `softboiled`');
+ assert.equal(error.errors.steak.message, 'Path `steak` is required.');
});
- it('min/max, enum, and regex built-in validators work', function(done) {
+ it('min/max, enum, and regex built-in validators work', async function() {
const s = new Schema({
steak: { type: String, enum: ['ribeye', 'sirloin'] },
eggs: { type: Number, min: 4, max: 6 },
@@ -1285,43 +1048,36 @@ describe('model: findOneAndUpdate:', function() {
const Breakfast = db.model('Test', s);
const updateOptions = { runValidators: true, new: true };
- Breakfast.findOneAndUpdate(
+ let error = await Breakfast.findOneAndUpdate(
{},
{ $set: { steak: 'ribeye', eggs: 3, bacon: '3 strips' } },
- updateOptions,
- function(error) {
- assert.ok(!!error);
- assert.equal(Object.keys(error.errors).length, 1);
- assert.equal(Object.keys(error.errors)[0], 'eggs');
- assert.equal(error.errors.eggs.message, 'Path `eggs` (3) is less than minimum allowed value (4).');
-
- Breakfast.findOneAndUpdate(
- {},
- { $set: { steak: 'tofu', eggs: 5, bacon: '3 strips' } },
- updateOptions,
- function(error) {
- assert.ok(!!error);
- assert.equal(Object.keys(error.errors).length, 1);
- assert.equal(Object.keys(error.errors)[0], 'steak');
- assert.equal(error.errors.steak, '`tofu` is not a valid enum value for path `steak`.');
-
- Breakfast.findOneAndUpdate(
- {},
- { $set: { steak: 'sirloin', eggs: 6, bacon: 'none' } },
- updateOptions,
- function(error) {
- assert.ok(!!error);
- assert.equal(Object.keys(error.errors).length, 1);
- assert.equal(Object.keys(error.errors)[0], 'bacon');
- assert.equal(error.errors.bacon.message, 'Path `bacon` is invalid (none).');
-
- done();
- });
- });
- });
+ updateOptions
+ ).then(() => null, err => err);
+ assert.equal(Object.keys(error.errors).length, 1);
+ assert.equal(Object.keys(error.errors)[0], 'eggs');
+ assert.equal(error.errors.eggs.message, 'Path `eggs` (3) is less than minimum allowed value (4).');
+
+ error = await Breakfast.findOneAndUpdate(
+ {},
+ { $set: { steak: 'tofu', eggs: 5, bacon: '3 strips' } },
+ updateOptions
+ ).then(() => null, err => err);
+
+ assert.equal(Object.keys(error.errors).length, 1);
+ assert.equal(Object.keys(error.errors)[0], 'steak');
+ assert.equal(error.errors.steak, '`tofu` is not a valid enum value for path `steak`.');
+
+ error = await Breakfast.findOneAndUpdate(
+ {},
+ { $set: { steak: 'sirloin', eggs: 6, bacon: 'none' } },
+ updateOptions
+ ).then(() => null, err => err);
+ assert.equal(Object.keys(error.errors).length, 1);
+ assert.equal(Object.keys(error.errors)[0], 'bacon');
+ assert.equal(error.errors.bacon.message, 'Path `bacon` is invalid (none).');
});
- it('multiple validation errors', function(done) {
+ it('multiple validation errors', async function() {
const s = new Schema({
steak: { type: String, enum: ['ribeye', 'sirloin'] },
eggs: { type: Number, min: 4, max: 6 },
@@ -1330,21 +1086,18 @@ describe('model: findOneAndUpdate:', function() {
const Breakfast = db.model('Test', s);
const updateOptions = { runValidators: true, new: true };
- Breakfast.findOneAndUpdate(
+ const error = await Breakfast.findOneAndUpdate(
{},
{ $set: { steak: 'tofu', eggs: 2, bacon: '3 strips' } },
- updateOptions,
- function(error, breakfast) {
- assert.ok(!!error);
- assert.equal(Object.keys(error.errors).length, 2);
- assert.ok(Object.keys(error.errors).indexOf('steak') !== -1);
- assert.ok(Object.keys(error.errors).indexOf('eggs') !== -1);
- assert.ok(!breakfast);
- done();
- });
+ updateOptions
+ ).then(() => null, err => err);
+ assert.ok(!!error);
+ assert.equal(Object.keys(error.errors).length, 2);
+ assert.ok(Object.keys(error.errors).indexOf('steak') !== -1);
+ assert.ok(Object.keys(error.errors).indexOf('eggs') !== -1);
});
- it('validators ignore $inc', function(done) {
+ it('validators ignore $inc', async function() {
const s = new Schema({
steak: { type: String, required: true },
eggs: { type: Number, min: 4 }
@@ -1352,16 +1105,12 @@ describe('model: findOneAndUpdate:', function() {
const Breakfast = db.model('Test', s);
const updateOptions = { runValidators: true, upsert: true, new: true };
- Breakfast.findOneAndUpdate(
+ const breakfast = await Breakfast.findOneAndUpdate(
{},
{ $inc: { eggs: 1 } },
- updateOptions,
- function(error, breakfast) {
- assert.ifError(error);
- assert.ok(!!breakfast);
- assert.equal(breakfast.eggs, 1);
- done();
- });
+ updateOptions
+ );
+ assert.equal(breakfast.eggs, 1);
});
it('validators ignore paths underneath mixed (gh-8659)', function() {
@@ -1379,7 +1128,7 @@ describe('model: findOneAndUpdate:', function() {
then(() => assert.equal(called, 0));
});
- it('should work with arrays (gh-3035)', function(done) {
+ it('should work with arrays (gh-3035)', async function() {
const testSchema = new mongoose.Schema({
id: String,
name: String,
@@ -1391,17 +1140,11 @@ describe('model: findOneAndUpdate:', function() {
});
const TestModel = db.model('Test', testSchema);
- TestModel.create({ id: '1' }, function(error) {
- assert.ifError(error);
- TestModel.findOneAndUpdate({ id: '1' }, { $set: { name: 'Joe' } }, { upsert: true },
- function(error) {
- assert.ifError(error);
- done();
- });
- });
+ await TestModel.create({ id: '1' });
+ await TestModel.findOneAndUpdate({ id: '1' }, { $set: { name: 'Joe' } }, { upsert: true });
});
- it('should allow null values in query (gh-3135)', function(done) {
+ it('should allow null values in query (gh-3135)', async function() {
const testSchema = new mongoose.Schema({
id: String,
blob: ObjectId,
@@ -1409,17 +1152,11 @@ describe('model: findOneAndUpdate:', function() {
});
const TestModel = db.model('Test', testSchema);
- TestModel.create({ blob: null, status: 'active' }, function(error) {
- assert.ifError(error);
- TestModel.findOneAndUpdate({ id: '1', blob: null }, { $set: { status: 'inactive' } }, { upsert: true },
- function(error) {
- assert.ifError(error);
- done();
- });
- });
+ await TestModel.create({ blob: null, status: 'active' });
+ await TestModel.findOneAndUpdate({ id: '1', blob: null }, { $set: { status: 'inactive' } }, { upsert: true });
});
- it('should work with array documents (gh-3034)', function(done) {
+ it('should work with array documents (gh-3034)', async function() {
const testSchema = new mongoose.Schema({
id: String,
name: String,
@@ -1433,17 +1170,11 @@ describe('model: findOneAndUpdate:', function() {
});
const TestModel = db.model('Test', testSchema);
- TestModel.create({ id: '1' }, function(error) {
- assert.ifError(error);
- TestModel.findOneAndUpdate({ id: '1' }, { $set: { name: 'Joe' } }, { upsert: true },
- function(error) {
- assert.ifError(error);
- done();
- });
- });
+ await TestModel.create({ id: '1' });
+ await TestModel.findOneAndUpdate({ id: '1' }, { $set: { name: 'Joe' } }, { upsert: true });
});
- it('handles setting array (gh-3107)', function(done) {
+ it('handles setting array (gh-3107)', async function() {
const testSchema = new mongoose.Schema({
name: String,
a: [{
@@ -1455,20 +1186,14 @@ describe('model: findOneAndUpdate:', function() {
const TestModel = db.model('Test', testSchema);
const update = { $setOnInsert: { a: [{ foo: 'bar' }], b: [2] } };
const opts = { upsert: true, new: true };
- TestModel
- .findOneAndUpdate({ name: 'abc' }, update, opts,
- function(error, doc) {
- assert.ifError(error);
- assert.equal(doc.a.length, 1);
- assert.equal(doc.a[0].foo, 'bar');
- assert.equal(doc.b.length, 1);
- assert.equal(doc.b[0], 2);
- done();
- });
+ const doc = await TestModel.findOneAndUpdate({ name: 'abc' }, update, opts);
+ assert.equal(doc.a.length, 1);
+ assert.equal(doc.a[0].foo, 'bar');
+ assert.equal(doc.b.length, 1);
+ assert.equal(doc.b[0], 2);
});
-
- it('handles nested cast errors (gh-3468)', function(done) {
+ it('handles nested cast errors (gh-3468)', async function() {
const recordSchema = new mongoose.Schema({
kind: String,
amount: Number
@@ -1483,91 +1208,77 @@ describe('model: findOneAndUpdate:', function() {
const Shift = db.model('Test', shiftSchema);
- Shift.create({
+ await Shift.create({
userId: 'tom',
records: []
- }, function(error) {
- assert.ifError(error);
- Shift.findOneAndUpdate({ userId: 'tom' }, {
+ });
+
+ try {
+ await Shift.findOneAndUpdate({ userId: 'tom' }, {
records: [{ kind: 'kind1', amount: NaN }]
}, {
new: true
- }, function(error) {
- assert.ok(error);
- assert.ok(error instanceof CastError);
- done();
});
- });
+ } catch (error) {
+ assert.ok(error);
+ assert.ok(error instanceof CastError);
+ }
});
- it('cast errors with nested schemas (gh-3580)', function(done) {
+ it('cast errors with nested schemas (gh-3580)', async function() {
const nested = new Schema({ num: Number });
const s = new Schema({ nested: nested });
const MyModel = db.model('Test', s);
const update = { nested: { num: 'Not a Number' } };
- MyModel.findOneAndUpdate({}, update, function(error) {
- assert.ok(error);
- done();
- });
+ const error = await MyModel.findOneAndUpdate({}, update).then(() => null, err => err);
+ assert.ok(error);
});
- it('pull with nested schemas (gh-3616)', function(done) {
+ it('pull with nested schemas (gh-3616)', async function() {
const nested = new Schema({ arr: [{ num: Number }] });
const s = new Schema({ nested: nested });
const MyModel = db.model('Test', s);
- MyModel.create({ nested: { arr: [{ num: 5 }] } }, function(error) {
- assert.ifError(error);
- const update = { $pull: { 'nested.arr': { num: 5 } } };
- const options = { new: true };
- MyModel.findOneAndUpdate({}, update, options, function(error, doc) {
- assert.ifError(error);
- assert.equal(doc.nested.arr.length, 0);
- done();
- });
- });
+ await MyModel.create({ nested: { arr: [{ num: 5 }] } });
+ const update = { $pull: { 'nested.arr': { num: 5 } } };
+ const options = { new: true };
+ const doc = await MyModel.findOneAndUpdate({}, update, options);
+ assert.equal(doc.nested.arr.length, 0);
});
- it('setting nested schema (gh-3889)', function(done) {
+ it('setting nested schema (gh-3889)', async function() {
const nested = new Schema({ test: String });
const s = new Schema({ nested: nested });
const MyModel = db.model('Test', s);
- MyModel.findOneAndUpdate(
+ await MyModel.findOneAndUpdate(
{},
- { $set: { nested: { test: 'abc' } } },
- function(error) {
- assert.ifError(error);
- done();
- });
+ { $set: { nested: { test: 'abc' } } }
+ );
});
});
describe('bug fixes', function() {
- it('passes raw result if rawResult specified (gh-4925)', function(done) {
+ it('passes raw result if includeResultMetadata specified (gh-4925)', async function() {
const testSchema = new mongoose.Schema({
test: String
});
const TestModel = db.model('Test', testSchema);
- const options = { upsert: true, new: true, rawResult: true };
+ const options = { upsert: true, new: true, includeResultMetadata: true };
const update = { $set: { test: 'abc' } };
- TestModel.findOneAndUpdate({}, update, options).
- exec(function(error, res) {
- assert.ifError(error);
- assert.ok(res);
- assert.ok(res.ok);
- assert.equal(res.value.test, 'abc');
- assert.ok(res.value.id);
- assert.equal(res.lastErrorObject.n, 1);
- done();
- });
+ const res = await TestModel.findOneAndUpdate({}, update, options).exec();
+ assert.ok(res);
+ assert.ok(res.ok);
+ assert.equal(res.value.test, 'abc');
+ assert.ok(res.value.id);
+ assert.equal(res.lastErrorObject.n, 1);
});
- it('handles setting single embedded docs to null (gh-4281)', function(done) {
+ it('handles setting single embedded docs to null (gh-4281)', async function() {
const foodSchema = new mongoose.Schema({
name: { type: String, default: 'Bacon' }
});
@@ -1581,17 +1292,13 @@ describe('model: findOneAndUpdate:', function() {
const options = { upsert: true, new: true };
const update = { $set: { main: null, for: 'Val' } };
- TestModel.findOneAndUpdate({}, update, options).
- exec(function(error, doc) {
- assert.ifError(error);
- assert.ok(doc);
- assert.equal(doc.main, null);
+ const doc = await TestModel.findOneAndUpdate({}, update, options).exec();
- done();
- });
+ assert.ok(doc);
+ assert.equal(doc.main, null);
});
- it('custom validator on mixed field (gh-4305)', function(done) {
+ it('custom validator on mixed field (gh-4305)', async function() {
let called = 0;
const boardSchema = new Schema({
@@ -1627,20 +1334,16 @@ describe('model: findOneAndUpdate:', function() {
const opts = {
new: true,
upsert: false,
- passRawResult: false,
overwrite: false,
runValidators: true
};
- Board.
- findOneAndUpdate({}, update, opts).
- exec(function(error) {
- assert.ifError(error);
- assert.equal(called, 1);
- done();
- });
+
+ await Board.findOneAndUpdate({}, update, opts).exec();
+
+ assert.equal(called, 1);
});
- it('single nested doc cast errors (gh-3602)', function(done) {
+ it('single nested doc cast errors (gh-3602)', async() => {
const AddressSchema = new Schema({
street: {
type: Number
@@ -1654,15 +1357,13 @@ describe('model: findOneAndUpdate:', function() {
const Person = db.model('Person', PersonSchema);
const update = { $push: { addresses: { street: 'not a num' } } };
- Person.findOneAndUpdate({}, update, function(error) {
- assert.ok(error.message.indexOf('street') !== -1);
- assert.equal(error.reason.message,
- 'Cast to Number failed for value "not a num" (type string) at path "street"');
- done();
- });
+ const error = await Person.findOneAndUpdate({}, update).then(() => null, err => err);
+ assert.ok(error.message.indexOf('street') !== -1);
+ assert.equal(error.reason.message,
+ 'Cast to Number failed for value "not a num" (type string) at path "street"');
});
- it('projection option as alias for fields (gh-4315)', function(done) {
+ it('projection option as alias for fields (gh-4315)', async function() {
const TestSchema = new Schema({
test1: String,
test2: String
@@ -1670,42 +1371,38 @@ describe('model: findOneAndUpdate:', function() {
const Test = db.model('Test', TestSchema);
const update = { $set: { test1: 'a', test2: 'b' } };
const options = { projection: { test2: 0 }, new: true, upsert: true };
- Test.findOneAndUpdate({}, update, options, function(error, doc) {
- assert.ifError(error);
- assert.ok(!doc.test2);
- assert.equal(doc.test1, 'a');
- done();
- });
+ const doc = await Test.findOneAndUpdate({}, update, options);
+ assert.ok(!doc.test2);
+ assert.equal(doc.test1, 'a');
});
- it('handles upserting a non-existing field (gh-4757)', function(done) {
+ it('handles upserting a non-existing field (gh-4757)', async function() {
const modelSchema = new Schema({ field: Number }, { strict: 'throw' });
const Model = db.model('Test', modelSchema);
- Model.findOneAndUpdate({ nonexistingField: 1 }, { field: 2 }, {
- upsert: true,
- new: true
- }).exec(function(error) {
+ try {
+ await Model.findOneAndUpdate({ nonexistingField: 1 }, { field: 2 }, {
+ upsert: true,
+ new: true
+ }).exec();
+ assert.ok(false);
+ } catch (error) {
assert.ok(error);
assert.equal(error.name, 'StrictModeError');
- done();
- });
+ }
});
- it('strict option (gh-5108)', function(done) {
+ it('strict option (gh-5108)', async function() {
const modelSchema = new Schema({ field: Number }, { strict: 'throw' });
const Model = db.model('Test', modelSchema);
- Model.findOneAndUpdate({}, { field: 2, otherField: 3 }, {
+ const doc = await Model.findOneAndUpdate({}, { field: 2, otherField: 3 }, {
upsert: true,
strict: false,
new: true
- }).exec(function(error, doc) {
- assert.ifError(error);
- assert.equal(doc.field, 2);
- assert.equal(doc.get('otherField'), 3);
- done();
});
+ assert.equal(doc.field, 2);
+ assert.equal(doc.get('otherField'), 3);
});
it('correct key order (gh-6484)', function() {
@@ -1756,49 +1453,26 @@ describe('model: findOneAndUpdate:', function() {
});
});
- it('overwrite doc with update validators (gh-3556)', function(done) {
- const testSchema = new Schema({
- name: {
- type: String,
- required: true
- },
- otherName: String
- });
- const Test = db.model('Test', testSchema);
-
- const opts = { overwrite: true, runValidators: true };
- Test.findOneAndUpdate({}, { otherName: 'test' }, opts, function(error) {
- assert.ok(error);
- assert.ok(error.errors['name']);
- Test.findOneAndUpdate({}, { $set: { otherName: 'test' } }, opts, function(error) {
- assert.ifError(error);
- done();
- });
- });
- });
-
- it('update using $ (gh-5628)', function(done) {
+ it('update using $ (gh-5628)', async function() {
const schema = new mongoose.Schema({
elems: [String]
});
const Model = db.model('Test', schema);
- Model.create({ elems: ['a', 'b'] }, function(error, doc) {
- assert.ifError(error);
- const query = { _id: doc._id, elems: 'a' };
- const update = { $set: { 'elems.$': 'c' } };
- Model.findOneAndUpdate(query, update, { new: true }, function(error) {
- assert.ifError(error);
- Model.collection.findOne({ _id: doc._id }, function(error, doc) {
- assert.ifError(error);
- assert.deepEqual(doc.elems, ['c', 'b']);
- done();
- });
- });
- });
+
+ const doc = await Model.create({ elems: ['a', 'b'] });
+
+ const query = { _id: doc._id, elems: 'a' };
+ const update = { $set: { 'elems.$': 'c' } };
+
+ await Model.findOneAndUpdate(query, update, { new: true });
+
+ const updatedDoc = await Model.collection.findOne({ _id: doc._id });
+
+ assert.deepEqual(updatedDoc.elems, ['c', 'b']);
});
- it('projection with $elemMatch (gh-5661)', function(done) {
+ it('projection with $elemMatch (gh-5661)', async function() {
const schema = new mongoose.Schema({
name: { type: String, default: 'test' },
arr: [{ tag: String }]
@@ -1806,25 +1480,20 @@ describe('model: findOneAndUpdate:', function() {
const Model = db.model('Test', schema);
const doc = { arr: [{ tag: 't1' }, { tag: 't2' }] };
- Model.create(doc, function(error) {
- assert.ifError(error);
- const query = {};
- const update = { $set: { name: 'test2' } };
- const opts = {
- new: true,
- fields: { arr: { $elemMatch: { tag: 't1' } } }
- };
- Model.findOneAndUpdate(query, update, opts, function(error, doc) {
- assert.ifError(error);
- assert.ok(!doc.name);
- assert.equal(doc.arr.length, 1);
- assert.equal(doc.arr[0].tag, 't1');
- done();
- });
- });
+ await Model.create(doc);
+ const query = {};
+ const update = { $set: { name: 'test2' } };
+ const opts = {
+ new: true,
+ fields: { arr: { $elemMatch: { tag: 't1' } } }
+ };
+ const doc2 = await Model.findOneAndUpdate(query, update, opts);
+ assert.ok(!doc2.name);
+ assert.equal(doc2.arr.length, 1);
+ assert.equal(doc2.arr[0].tag, 't1');
});
- it('multi cast error (gh-5609)', function(done) {
+ it('multi cast error (gh-5609)', async function() {
const schema = new mongoose.Schema({
num1: Number,
num2: Number
@@ -1833,18 +1502,16 @@ describe('model: findOneAndUpdate:', function() {
const Model = db.model('Test', schema);
const opts = { multipleCastError: true };
- Model.findOneAndUpdate({}, { num1: 'fail', num2: 'fail' }, opts, function(error) {
- assert.ok(error);
- assert.equal(error.name, 'ValidationError');
- assert.ok(error.errors['num1']);
- assert.equal(error.errors['num1'].name, 'CastError');
- assert.ok(error.errors['num2']);
- assert.equal(error.errors['num2'].name, 'CastError');
- done();
- });
+ const error = await Model.findOneAndUpdate({}, { num1: 'fail', num2: 'fail' }, opts).then(() => null, err => err);
+ assert.ok(error);
+ assert.equal(error.name, 'ValidationError');
+ assert.ok(error.errors['num1']);
+ assert.equal(error.errors['num1'].name, 'CastError');
+ assert.ok(error.errors['num2']);
+ assert.equal(error.errors['num2'].name, 'CastError');
});
- it('update validators with pushing null (gh-5710)', function(done) {
+ it('update validators with pushing null (gh-5710)', async function() {
const schema = new mongoose.Schema({
arr: [String]
});
@@ -1853,10 +1520,7 @@ describe('model: findOneAndUpdate:', function() {
const update = { $addToSet: { arr: null } };
const options = { runValidators: true };
- Model.findOneAndUpdate({}, update, options, function(error) {
- assert.ifError(error);
- done();
- });
+ await Model.findOneAndUpdate({}, update, options);
});
it('only calls setters once (gh-6203)', async function() {
@@ -1955,7 +1619,7 @@ describe('model: findOneAndUpdate:', function() {
assert.equal(res.highlights.length, 0);
});
- it('avoids edge case with middleware cloning buffers (gh-5702)', function(done) {
+ it('avoids edge case with middleware cloning buffers (gh-5702)', async function() {
function toUUID(string) {
if (!string) {
return null;
@@ -1989,7 +1653,7 @@ describe('model: findOneAndUpdate:', function() {
}, { collection: 'users' });
UserSchema.pre('findOneAndUpdate', function() {
- this.update({}, { $set: { lastUpdate: new Date() } });
+ this.updateOne({}, { $set: { lastUpdate: new Date() } });
});
const User = db.model('User', UserSchema);
@@ -2000,19 +1664,17 @@ describe('model: findOneAndUpdate:', function() {
friends: [{ status: 'New', id: friendId }]
};
- User.create(user, function(error, user) {
- assert.ifError(error);
+ const createdUser = await User.create(user);
- const q = { _id: user._id, 'friends.id': friendId };
- const upd = { 'friends.$.status': 'Active' };
- User.findOneAndUpdate(q, upd, { new: true }).lean().exec(function(error) {
- assert.ifError(error);
- done();
- });
- });
+ const q = { _id: createdUser._id, 'friends.id': friendId };
+ const upd = { 'friends.$.status': 'Active' };
+
+ const updatedUser = await User.findOneAndUpdate(q, upd, { new: true }).lean().exec();
+
+ assert.equal(updatedUser.friends[0].status, 'Active');
});
- it('setting subtype when saving (gh-5551)', function(done) {
+ it('setting subtype when saving (gh-5551)', async function() {
const uuid = require('uuid');
function toUUID(string) {
if (!string) {
@@ -2040,14 +1702,12 @@ describe('model: findOneAndUpdate:', function() {
upsert: true,
new: true
};
- User.findOneAndUpdate({}, user, opts).exec(function(error, doc) {
- assert.ifError(error);
- User.collection.findOne({ _id: doc._id }, function(error, doc) {
- assert.ifError(error);
- assert.equal(doc.foo.sub_type, 4);
- done();
- });
- });
+
+ const doc = await User.findOneAndUpdate({}, user, opts).exec();
+
+ const result = await User.collection.findOne({ _id: doc._id });
+
+ assert.equal(result.foo.sub_type, 4);
});
it('properly handles casting nested objects in update (gh-4724)', function(done) {
@@ -2088,7 +1748,7 @@ describe('model: findOneAndUpdate:', function() {
catch(done);
});
- it('doesnt do double validation on document arrays during updates (gh-4440)', function(done) {
+ it('doesnt do double validation on document arrays during updates (gh-4440)', async function() {
const A = new Schema({ str: String });
let B = new Schema({ a: [A] });
let validateCalls = 0;
@@ -2100,16 +1760,13 @@ describe('model: findOneAndUpdate:', function() {
B = db.model('Test', B);
- B.findOneAndUpdate(
+ await B.findOneAndUpdate(
{ foo: 'bar' },
{ $set: { a: [{ str: 'asdf' }] } },
- { runValidators: true },
- function(err) {
- assert.ifError(err);
- assert.equal(validateCalls, 1); // Assertion error: 1 == 2
- done();
- }
+ { runValidators: true }
);
+
+ assert.equal(validateCalls, 1);
});
it('runs setters on array elements (gh-7679)', function() {
@@ -2404,6 +2061,26 @@ describe('model: findOneAndUpdate:', function() {
assert.ifError(err);
});
+ it('casts array filters (gh-13219)', async function() {
+ const MyModel = db.model('Test', new Schema({
+ _id: Number,
+ grades: [Number]
+ }));
+
+ await MyModel.create([
+ { _id: 1, grades: [95, 102, 90] }
+ ]);
+
+ await MyModel.findOneAndUpdate(
+ {},
+ { $set: { 'grades.$[element]': 100 } },
+ { arrayFilters: [{ element: { $gt: '100' } }] }
+ );
+
+ const doc = await MyModel.findOne();
+ assert.deepEqual(doc.toObject().grades, [95, 100, 90]);
+ });
+
it('throws error if filter is not an object (gh-13264)', async function() {
const schema = new Schema({ name: String });
const Model = db.model('Test', schema);
@@ -2413,4 +2090,143 @@ describe('model: findOneAndUpdate:', function() {
assert.equal(err.name, 'ObjectParameterError');
});
+ it('handles plus path in projection (gh-13413)', async function() {
+ const testSchema = new mongoose.Schema({
+ name: String,
+ nickName: {
+ type: String,
+ select: false
+ }
+ });
+ const Test = db.model('Test', testSchema);
+
+ const entry = await Test.create({
+ name: 'Test Testerson',
+ nickName: 'Quiz'
+ });
+
+ let res = await Test.findOneAndUpdate(
+ { _id: entry._id },
+ { $set: { name: 'Test' } },
+ { projection: '+nickName', returnDocument: 'after' }
+ );
+ assert.equal(res.name, 'Test');
+ assert.equal(res.nickName, 'Quiz');
+
+ res = await Test.findOneAndDelete(
+ { _id: entry._id },
+ { projection: '+nickName', returnDocument: 'before' }
+ );
+ assert.equal(res.name, 'Test');
+ assert.equal(res.nickName, 'Quiz');
+ });
+
+ it('allows setting paths with dots in non-strict paths (gh-13434) (gh-10200)', async function() {
+ const testSchema = new mongoose.Schema({
+ name: String,
+ info: Object
+ }, { strict: false });
+ const Test = db.model('Test', testSchema);
+
+ const doc = await Test.findOneAndUpdate(
+ {},
+ {
+ name: 'Test Testerson',
+ info: { 'second.name': 'Quiz' },
+ info2: { 'second.name': 'Quiz' }
+ },
+ { new: true, upsert: true }
+ ).lean();
+
+ assert.ok(doc);
+ assert.equal(doc.info['second.name'], 'Quiz');
+ assert.equal(doc.info2['second.name'], 'Quiz');
+ });
+ it('supports the `includeResultMetadata` option (gh-13539)', async function() {
+ const testSchema = new mongoose.Schema({
+ name: String
+ });
+ const Test = db.model('Test', testSchema);
+ await Test.create({
+ name: 'Test'
+ });
+ const doc = await Test.findOneAndUpdate(
+ { name: 'Test' },
+ { name: 'Test Testerson' },
+ { new: true, upsert: true, includeResultMetadata: false }
+ );
+ assert.equal(doc.ok, undefined);
+ assert.equal(doc.name, 'Test Testerson');
+
+ const data = await Test.findOneAndUpdate(
+ { name: 'Test Testerson' },
+ { name: 'Test' },
+ { new: true, upsert: true, includeResultMetadata: true }
+ );
+ assert(data.ok);
+ assert.equal(data.value.name, 'Test');
+ });
+
+ it('successfully runs findOneAndUpdate with no update and versionKey set to false (gh-13783)', async function() {
+ const exampleSchema = new mongoose.Schema({
+ name: String
+ }, { versionKey: false });
+
+ const ExampleModel = db.model('Example', exampleSchema);
+
+ const document = await ExampleModel.findOneAndUpdate(
+ { name: 'test' },
+ {},
+ { upsert: true, returnDocument: 'after', returnOriginal: false }
+ );
+ assert.ok(document);
+ assert.equal(document.name, 'test');
+ });
+
+ it('skips adding defaults to filter when passing empty update (gh-13962)', async function() {
+ const schema = new Schema({
+ myField: Number,
+ defaultField: { type: String, default: 'default' }
+ }, { versionKey: false });
+ const Test = db.model('Test', schema);
+
+ await Test.create({ myField: 1, defaultField: 'some non-default value' });
+
+ const updated = await Test.findOneAndUpdate(
+ { myField: 1 },
+ {},
+ { upsert: true, returnDocument: 'after' }
+ );
+ assert.equal(updated.defaultField, 'some non-default value');
+ });
+
+ it('sets CastError path to full path (gh-14114)', async function() {
+ const testSchema = new mongoose.Schema({
+ id: mongoose.Schema.Types.ObjectId,
+ name: String,
+ accessories: [
+ {
+ isEnabled: Boolean,
+ additionals: [
+ {
+ k: String,
+ v: Number
+ }
+ ]
+ }
+ ]
+ });
+ const Test = db.model('Test', testSchema);
+ const err = await Test.findOneAndUpdate(
+ {},
+ {
+ $set: {
+ 'accessories.0.additionals.0.k': ['test']
+ }
+ }
+ ).then(() => null, err => err);
+ assert.ok(err);
+ assert.equal(err.name, 'CastError');
+ assert.equal(err.path, 'accessories.0.additionals.0.k');
+ });
});
diff --git a/test/model.hydrate.test.js b/test/model.hydrate.test.js
index 6cdc333ce8d..447cc2be85b 100644
--- a/test/model.hydrate.test.js
+++ b/test/model.hydrate.test.js
@@ -99,5 +99,104 @@ describe('model', function() {
assert.equal(hydrated.test, 'test');
assert.deepEqual(hydrated.schema.tree, C.schema.tree);
});
+ it('should deeply hydrate the document with the `hydratedPopulatedDocs` option (gh-4727)', async function() {
+ const userSchema = new Schema({
+ name: String
+ });
+ const companySchema = new Schema({
+ name: String,
+ users: [{ ref: 'User', type: Schema.Types.ObjectId }]
+ });
+
+ db.deleteModel(/User/);
+ db.deleteModel(/Company/);
+ db.model('User', userSchema);
+ const Company = db.model('Company', companySchema);
+
+ const users = [{ _id: new mongoose.Types.ObjectId(), name: 'Val' }];
+ const company = { _id: new mongoose.Types.ObjectId(), name: 'Booster', users: [users[0]] };
+
+ const C = Company.hydrate(company, null, { hydratedPopulatedDocs: true });
+ assert.equal(C.users[0].name, 'Val');
+ });
+ it('should hydrate documents in virtual populate (gh-14503)', async function() {
+ const StorySchema = new Schema({
+ userId: {
+ type: Schema.Types.ObjectId,
+ ref: 'User'
+ },
+ title: {
+ type: String
+ }
+ }, { timestamps: true });
+
+ const UserSchema = new Schema({
+ name: String
+ }, { timestamps: true });
+
+ UserSchema.virtual('stories', {
+ ref: 'Story',
+ localField: '_id',
+ foreignField: 'userId'
+ });
+ UserSchema.virtual('storiesCount', {
+ ref: 'Story',
+ localField: '_id',
+ foreignField: 'userId',
+ count: true
+ });
+
+ db.deleteModel(/User/);
+ const User = db.model('User', UserSchema);
+ const Story = db.model('Story', StorySchema);
+
+ const user = await User.create({ name: 'Alex' });
+ const story1 = await Story.create({ title: 'Ticket 1', userId: user._id });
+ const story2 = await Story.create({ title: 'Ticket 2', userId: user._id });
+
+ const populated = await User.findOne({ name: 'Alex' }).populate(['stories', 'storiesCount']).lean();
+ const hydrated = User.hydrate(
+ JSON.parse(JSON.stringify(populated)),
+ null,
+ { hydratedPopulatedDocs: true }
+ );
+
+ assert.equal(hydrated.stories[0]._id.toString(), story1._id.toString());
+ assert(typeof hydrated.stories[0]._id == 'object', typeof hydrated.stories[0]._id);
+ assert(hydrated.stories[0]._id instanceof mongoose.Types.ObjectId);
+ assert(typeof hydrated.stories[0].createdAt == 'object');
+ assert(hydrated.stories[0].createdAt instanceof Date);
+
+ assert.equal(hydrated.stories[1]._id.toString(), story2._id.toString());
+ assert(typeof hydrated.stories[1]._id == 'object');
+
+ assert(hydrated.stories[1]._id instanceof mongoose.Types.ObjectId);
+ assert(typeof hydrated.stories[1].createdAt == 'object');
+ assert(hydrated.stories[1].createdAt instanceof Date);
+
+ assert.strictEqual(hydrated.storiesCount, 2);
+ });
+
+ it('sets hydrated docs as populated (gh-15048)', async function() {
+ const userSchema = new Schema({
+ name: String
+ });
+ const companySchema = new Schema({
+ name: String,
+ users: [{ ref: 'User', type: Schema.Types.ObjectId }]
+ });
+
+ db.deleteModel(/User/);
+ db.deleteModel(/Company/);
+ const User = db.model('User', userSchema);
+ const Company = db.model('Company', companySchema);
+
+ const users = [{ _id: new mongoose.Types.ObjectId(), name: 'Val' }];
+ const company = { _id: new mongoose.Types.ObjectId(), name: 'Acme', users: [users[0]] };
+
+ const c = Company.hydrate(company, null, { hydratedPopulatedDocs: true });
+ assert.ok(c.populated('users'));
+ assert.ok(c.users[0] instanceof User);
+ });
});
});
diff --git a/test/model.indexes.test.js b/test/model.indexes.test.js
index 92138a53791..d94935ebff1 100644
--- a/test/model.indexes.test.js
+++ b/test/model.indexes.test.js
@@ -256,19 +256,23 @@ describe('model', function() {
it('error should emit on the model', async function() {
const schema = new Schema({ name: { type: String } });
- const Test = db.model('Test', schema);
-
+ const Test = db.model('Test', schema, 'Test');
+ await Test.init();
await Test.create({ name: 'hi' }, { name: 'hi' });
- Test.schema.index({ name: 1 }, { unique: true });
- Test.schema.index({ other: 1 });
-
- const err = await Test.ensureIndexes().then(() => null, err => err);
+ const Test2 = db.model(
+ 'Test2',
+ new Schema({
+ name: {
+ type: String,
+ unique: true
+ }
+ }),
+ 'Test'
+ );
+ const err = await Test2.init().then(() => null, err => err);
assert.ok(/E11000 duplicate key error/.test(err.message), err);
-
- delete Test.$init;
- await Test.init().catch(() => {});
});
it('when one index creation errors', async function() {
@@ -391,7 +395,7 @@ describe('model', function() {
const schema = mongoose.Schema({ x: 'string' });
const Test = mongoose.createConnection().model('ensureIndexes-' + random, schema);
const p = Test.ensureIndexes();
- assert.ok(p instanceof mongoose.Promise);
+ assert.ok(p instanceof Promise);
});
it('creates indexes', async function() {
@@ -625,7 +629,7 @@ describe('model', function() {
});
});
- it('should not find a diff when calling diffIndexes after syncIndexes involving a text and non-text compound index (gh-13136)', function(done) {
+ it('should not find a diff when calling diffIndexes after syncIndexes involving a text and non-text compound index (gh-13136)', async function() {
const Test = new Schema({
title: {
type: String
@@ -647,16 +651,14 @@ describe('model', function() {
});
const TestModel = db.model('Test', Test);
+ await TestModel.init();
- TestModel.diffIndexes().then((diff) => {
- assert.deepEqual(diff, { toCreate: [{ age: 1, title: 'text', description: 'text' }], toDrop: [] });
- TestModel.syncIndexes().then(() => {
- TestModel.diffIndexes().then((diff2) => {
- assert.deepEqual(diff2, { toCreate: [], toDrop: [] });
- done();
- });
- });
- });
+ const diff = await TestModel.diffIndexes();
+ assert.deepEqual(diff, { toCreate: [{ age: 1, title: 'text', description: 'text' }], toDrop: [] });
+ await TestModel.syncIndexes();
+
+ const diff2 = await TestModel.diffIndexes();
+ assert.deepEqual(diff2, { toCreate: [], toDrop: [] });
});
it('cleanIndexes (gh-6676)', async function() {
@@ -712,5 +714,15 @@ describe('model', function() {
assert.deepStrictEqual(result.toDrop, ['age_1', 'weight_1']);
assert.deepStrictEqual(result.toCreate, [{ password: 1 }, { email: 1 }]);
});
+
+ it('running diffIndexes with a non-existent collection should not throw an error (gh-14010)', async function() {
+ const testSchema = new mongoose.Schema({
+ name: String
+ });
+
+ const Test = db.model('gh14010', testSchema);
+ const res = await Test.diffIndexes();
+ assert.ok(res);
+ });
});
});
diff --git a/test/model.insertMany.test.js b/test/model.insertMany.test.js
new file mode 100644
index 00000000000..db8c96b535c
--- /dev/null
+++ b/test/model.insertMany.test.js
@@ -0,0 +1,649 @@
+'use strict';
+
+const assert = require('assert');
+const start = require('./common');
+const util = require('./util');
+const mongoose = start.mongoose;
+const Schema = mongoose.Schema;
+
+describe('insertMany()', function() {
+ let db;
+
+ beforeEach(() => db.deleteModel(/.*/));
+
+ before(function() {
+ db = start();
+ });
+
+ after(async function() {
+ await db.close();
+ });
+
+ afterEach(() => util.clearTestData(db));
+ afterEach(() => require('./util').stopRemainingOps(db));
+ it('with timestamps (gh-723)', function() {
+ const schema = new Schema({ name: String }, { timestamps: true });
+ const Movie = db.model('Movie', schema);
+ const start = Date.now();
+
+ return Movie.insertMany([{ name: 'Star Wars' }, { name: 'The Empire Strikes Back' }]).
+ then(docs => {
+ assert.equal(docs.length, 2);
+ assert.ok(!docs[0].isNew);
+ assert.ok(!docs[1].isNew);
+ assert.ok(docs[0].createdAt.valueOf() >= start);
+ assert.ok(docs[1].createdAt.valueOf() >= start);
+ }).
+ then(() => Movie.find()).
+ then(docs => {
+ assert.equal(docs.length, 2);
+ assert.ok(docs[0].createdAt.valueOf() >= start);
+ assert.ok(docs[1].createdAt.valueOf() >= start);
+ });
+ });
+
+ it('timestamps respect $timestamps() (gh-12117)', async function() {
+ const schema = new Schema({ name: String }, { timestamps: true });
+ const Movie = db.model('Movie', schema);
+ const start = Date.now();
+
+ const arr = [
+ new Movie({ name: 'Star Wars' }),
+ new Movie({ name: 'The Empire Strikes Back' })
+ ];
+ arr[1].$timestamps(false);
+
+ await Movie.insertMany(arr);
+ const docs = await Movie.find().sort({ name: 1 });
+ assert.ok(docs[0].createdAt.valueOf() >= start);
+ assert.ok(!docs[1].createdAt);
+ });
+
+ it('insertMany() with nested timestamps (gh-12060)', async function() {
+ const childSchema = new Schema({ name: { type: String } }, {
+ _id: false,
+ timestamps: true
+ });
+
+ const parentSchema = new Schema({ child: childSchema }, {
+ timestamps: true
+ });
+
+ const Test = db.model('Test', parentSchema);
+
+ await Test.insertMany([{ child: { name: 'test' } }]);
+ let docs = await Test.find();
+
+ assert.equal(docs.length, 1);
+ assert.equal(docs[0].child.name, 'test');
+ assert.ok(docs[0].child.createdAt);
+ assert.ok(docs[0].child.updatedAt);
+
+ await Test.insertMany([{ child: { name: 'test2' } }], { timestamps: false });
+ docs = await Test.find({ 'child.name': 'test2' });
+ assert.equal(docs.length, 1);
+ assert.equal(docs[0].child.name, 'test2');
+ assert.ok(!docs[0].child.createdAt);
+ assert.ok(!docs[0].child.updatedAt);
+ });
+ it('insertMany() (gh-723)', async function() {
+ const schema = new Schema({
+ name: String
+ }, { timestamps: true });
+ const Movie = db.model('Movie', schema);
+
+ let docs = await Movie.insertMany([{ name: 'Star Wars' }, { name: 'The Empire Strikes Back' }]);
+ assert.equal(docs.length, 2);
+ assert.ok(!docs[0].isNew);
+ assert.ok(!docs[1].isNew);
+ assert.ok(docs[0].createdAt);
+ assert.ok(docs[1].createdAt);
+ assert.strictEqual(docs[0].__v, 0);
+ assert.strictEqual(docs[1].__v, 0);
+ docs = await Movie.find({});
+ assert.equal(docs.length, 2);
+ assert.ok(docs[0].createdAt);
+ assert.ok(docs[1].createdAt);
+ });
+
+ it('insertMany() ordered option for constraint errors (gh-3893)', async function() {
+ const version = await start.mongodVersion();
+
+ const mongo34 = version[0] > 3 || (version[0] === 3 && version[1] >= 4);
+ if (!mongo34) {
+ return;
+ }
+
+ const schema = new Schema({
+ name: { type: String, unique: true }
+ });
+ const Movie = db.model('Movie', schema);
+
+ const arr = [
+ { name: 'Star Wars' },
+ { name: 'Star Wars' },
+ { name: 'The Empire Strikes Back' }
+ ];
+ await Movie.init();
+
+ const error = await Movie.insertMany(arr, { ordered: false }).then(() => null, err => err);
+
+ assert.equal(error.message.indexOf('E11000'), 0);
+ assert.equal(error.results.length, 3);
+ assert.equal(error.results[0].name, 'Star Wars');
+ assert.ok(error.results[1].err);
+ assert.ok(error.results[1].err.errmsg.includes('E11000'));
+ assert.equal(error.results[2].name, 'The Empire Strikes Back');
+ const docs = await Movie.find({}).sort({ name: 1 }).exec();
+
+ assert.equal(docs.length, 2);
+ assert.equal(docs[0].name, 'Star Wars');
+ assert.equal(docs[1].name, 'The Empire Strikes Back');
+ await Movie.collection.drop();
+ });
+
+ describe('insertMany() lean option to bypass validation (gh-8234)', () => {
+ const gh8234Schema = new Schema({
+ name: String,
+ age: { type: Number, required: true }
+ });
+ const arrGh8234 = [{ name: 'Rigas' }, { name: 'Tonis', age: 9 }];
+ let Gh8234;
+ before('init model', () => {
+ Gh8234 = db.model('Test8234', gh8234Schema);
+
+ return Gh8234.deleteMany({});
+ });
+ afterEach('delete inserted data', function() {
+ return Gh8234.deleteMany({});
+ });
+
+ it('insertMany() should bypass validation if lean option set to `true`', async function() {
+ let docs = await Gh8234.insertMany(arrGh8234, { lean: true });
+ assert.equal(docs.length, 2);
+ docs = await Gh8234.find({});
+ assert.equal(docs.length, 2);
+ assert.equal(arrGh8234[0].age, undefined);
+ assert.equal(arrGh8234[1].age, 9);
+ });
+
+ it('insertMany() should validate if lean option not set', async function() {
+ const error = await Gh8234.insertMany(arrGh8234).then(() => null, err => err);
+ assert.ok(error);
+ assert.equal(error.name, 'ValidationError');
+ assert.equal(error.errors.age.kind, 'required');
+ });
+
+ it('insertMany() should validate if lean option set to `false`', async function() {
+ const error = await Gh8234.insertMany(arrGh8234, { lean: false }).then(() => null, err => err);
+ assert.ok(error);
+ assert.equal(error.name, 'ValidationError');
+ assert.equal(error.errors.age.kind, 'required');
+ });
+ });
+
+ it('insertMany() ordered option for validation errors (gh-5068)', async function() {
+ const version = await start.mongodVersion();
+
+ const mongo34 = version[0] > 3 || (version[0] === 3 && version[1] >= 4);
+ if (!mongo34) {
+ return;
+ }
+
+ const schema = new Schema({
+ name: { type: String, required: true }
+ });
+ const Movie = db.model('Movie', schema);
+
+ const arr = [
+ { name: 'Star Wars' },
+ { foo: 'Star Wars' },
+ { name: 'The Empire Strikes Back' }
+ ];
+ await Movie.insertMany(arr, { ordered: false });
+
+ const docs = await Movie.find({}).sort({ name: 1 }).exec();
+ assert.equal(docs.length, 2);
+ assert.equal(docs[0].name, 'Star Wars');
+ assert.equal(docs[1].name, 'The Empire Strikes Back');
+ });
+
+ it('insertMany() `writeErrors` if only one error (gh-8938)', async function() {
+ const QuestionType = new mongoose.Schema({
+ code: { type: String, required: true, unique: true },
+ text: String
+ });
+ const Question = db.model('Test', QuestionType);
+
+
+ await Question.init();
+
+ await Question.create({ code: 'MEDIUM', text: '123' });
+ const data = [
+ { code: 'MEDIUM', text: '1111' },
+ { code: 'test', text: '222' },
+ { code: 'HARD', text: '2222' }
+ ];
+ const opts = { ordered: false, rawResult: true };
+ let err = await Question.insertMany(data, opts).catch(err => err);
+ assert.ok(Array.isArray(err.writeErrors));
+ assert.equal(err.writeErrors.length, 1);
+ assert.equal(err.insertedDocs.length, 2);
+ assert.equal(err.insertedDocs[0].code, 'test');
+ assert.equal(err.insertedDocs[1].code, 'HARD');
+
+ assert.equal(err.results.length, 3);
+ assert.ok(err.results[0].err.errmsg.includes('E11000'));
+ assert.equal(err.results[1].code, 'test');
+ assert.equal(err.results[2].code, 'HARD');
+
+ await Question.deleteMany({});
+ await Question.create({ code: 'MEDIUM', text: '123' });
+ await Question.create({ code: 'HARD', text: '123' });
+
+ err = await Question.insertMany(data, opts).catch(err => err);
+ assert.ok(Array.isArray(err.writeErrors));
+ assert.equal(err.writeErrors.length, 2);
+ assert.equal(err.insertedDocs.length, 1);
+ assert.equal(err.insertedDocs[0].code, 'test');
+
+ });
+
+ it('insertMany() ordered option for single validation error', async function() {
+ const version = start.mongodVersion();
+
+ const mongo34 = version[0] > 3 || (version[0] === 3 && version[1] >= 4);
+ if (!mongo34) {
+ return;
+ }
+
+ const schema = new Schema({
+ name: { type: String, required: true }
+ });
+ const Movie = db.model('Movie', schema);
+
+ const arr = [
+ { foo: 'Star Wars' },
+ { foo: 'The Fast and the Furious' }
+ ];
+ await Movie.insertMany(arr, { ordered: false });
+
+ const docs = await Movie.find({}).sort({ name: 1 }).exec();
+
+ assert.equal(docs.length, 0);
+ });
+
+ it('insertMany() hooks (gh-3846)', async function() {
+ const schema = new Schema({
+ name: String
+ });
+ let calledPre = 0;
+ let calledPost = 0;
+ schema.pre('insertMany', function(next, docs) {
+ assert.equal(docs.length, 2);
+ assert.equal(docs[0].name, 'Star Wars');
+ ++calledPre;
+ next();
+ });
+ schema.pre('insertMany', function(next, docs) {
+ assert.equal(docs.length, 2);
+ assert.equal(docs[0].name, 'Star Wars');
+ docs[0].name = 'A New Hope';
+ ++calledPre;
+ next();
+ });
+ schema.post('insertMany', function() {
+ ++calledPost;
+ });
+ const Movie = db.model('Movie', schema);
+
+ let docs = await Movie.insertMany([{ name: 'Star Wars' }, { name: 'The Empire Strikes Back' }]);
+ assert.equal(docs.length, 2);
+ assert.equal(calledPre, 2);
+ assert.equal(calledPost, 1);
+ docs = await Movie.find({}).sort({ name: 1 });
+ assert.equal(docs[0].name, 'A New Hope');
+ assert.equal(docs[1].name, 'The Empire Strikes Back');
+ });
+
+ it('returns empty array if no documents (gh-8130)', function() {
+ const Movie = db.model('Movie', Schema({ name: String }));
+ return Movie.insertMany([]).then(docs => assert.deepEqual(docs, []));
+ });
+
+ it('insertMany() multi validation error with ordered false (gh-5337)', async function() {
+ const schema = new Schema({
+ name: { type: String, required: true }
+ });
+ const Movie = db.model('Movie', schema);
+
+ const arr = [
+ { foo: 'The Phantom Menace' },
+ { name: 'Star Wars' },
+ { name: 'The Empire Strikes Back' },
+ { foobar: 'The Force Awakens' }
+ ];
+ const opts = { ordered: false, rawResult: true };
+ const res = await Movie.insertMany(arr, opts);
+ assert.equal(res.mongoose.validationErrors.length, 2);
+ assert.equal(res.mongoose.validationErrors[0].name, 'ValidationError');
+ assert.equal(res.mongoose.validationErrors[1].name, 'ValidationError');
+ });
+
+ it('insertMany() validation error with ordered true when all documents are invalid', async function() {
+ const schema = new Schema({
+ name: { type: String, required: true }
+ });
+ const Movie = db.model('Movie', schema);
+
+ const arr = [
+ { foo: 'The Phantom Menace' },
+ { foobar: 'The Force Awakens' }
+ ];
+ const opts = { ordered: true };
+ const error = await Movie.insertMany(arr, opts).then(() => null, err => err);
+ assert.ok(error);
+ assert.equal(error.name, 'ValidationError');
+ });
+
+ it('insertMany() validation error with ordered false when all documents are invalid', async function() {
+ const schema = new Schema({
+ name: { type: String, required: true }
+ });
+ const Movie = db.model('Movie', schema);
+
+ const arr = [
+ { foo: 'The Phantom Menace' },
+ { foobar: 'The Force Awakens' }
+ ];
+ const opts = { ordered: false };
+ const res = await Movie.insertMany(arr, opts);
+ assert.equal(res.length, 0);
+ });
+
+ it('insertMany() validation error with ordered false and rawResult for checking which documents failed (gh-12791)', async function() {
+ const schema = new Schema({
+ name: { type: String, required: true },
+ year: { type: Number, required: true }
+ });
+ const Movie = db.model('Movie', schema);
+
+ const id1 = new mongoose.Types.ObjectId();
+ const id2 = new mongoose.Types.ObjectId();
+ const id3 = new mongoose.Types.ObjectId();
+ const arr = [
+ { _id: id1, foo: 'The Phantom Menace', year: 1999 },
+ { _id: id2, name: 'The Force Awakens', bar: 2015 },
+ { _id: id3, name: 'The Empire Strikes Back', year: 1980 }
+ ];
+ const opts = { ordered: false, rawResult: true };
+ const res = await Movie.insertMany(arr, opts);
+ // {
+ // acknowledged: true,
+ // insertedCount: 1,
+ // insertedIds: { '0': new ObjectId("63b34b062cfe38622738e510") },
+ // mongoose: { validationErrors: [ [Error], [Error] ] }
+ // }
+
+ assert.equal(res.insertedCount, 1);
+ assert.equal(res.insertedIds[0].toHexString(), id3.toHexString());
+ assert.equal(res.mongoose.validationErrors.length, 2);
+ assert.ok(res.mongoose.validationErrors[0].errors['name']);
+ assert.ok(!res.mongoose.validationErrors[0].errors['year']);
+ assert.ok(res.mongoose.validationErrors[1].errors['year']);
+ assert.ok(!res.mongoose.validationErrors[1].errors['name']);
+
+ assert.equal(res.mongoose.results.length, 3);
+ assert.ok(res.mongoose.results[0].errors['name']);
+ assert.ok(res.mongoose.results[1].errors['year']);
+ assert.ok(res.mongoose.results[2].$__);
+ assert.equal(res.mongoose.results[2].name, 'The Empire Strikes Back');
+ });
+
+ it('insertMany() validation error with ordered false and rawResult for mixed write and validation error (gh-12791)', async function() {
+ const schema = new Schema({
+ name: { type: String, required: true, unique: true },
+ year: { type: Number, required: true }
+ });
+ const Movie = db.model('Movie', schema);
+ await Movie.init();
+
+ const arr = [
+ { foo: 'The Phantom Menace', year: 1999 },
+ { name: 'The Force Awakens', bar: 2015 },
+ { name: 'The Empire Strikes Back', year: 1980 },
+ { name: 'The Empire Strikes Back', year: 1980 }
+ ];
+ const opts = { ordered: false, rawResult: true };
+ const err = await Movie.insertMany(arr, opts).then(() => null, err => err);
+
+ assert.ok(err);
+ assert.equal(err.insertedDocs.length, 1);
+ assert.equal(err.insertedDocs[0].name, 'The Empire Strikes Back');
+ assert.equal(err.writeErrors.length, 1);
+ assert.equal(err.writeErrors[0].index, 3);
+ assert.equal(err.mongoose.validationErrors.length, 2);
+ assert.ok(err.mongoose.validationErrors[0].errors['name']);
+ assert.ok(!err.mongoose.validationErrors[0].errors['year']);
+ assert.ok(err.mongoose.validationErrors[1].errors['year']);
+ assert.ok(!err.mongoose.validationErrors[1].errors['name']);
+ });
+
+ it('insertMany() populate option (gh-9720)', async function() {
+ const schema = new Schema({
+ name: { type: String, required: true }
+ });
+ const Movie = db.model('Movie', schema);
+ const Person = db.model('Person', Schema({
+ name: String,
+ favoriteMovie: {
+ type: 'ObjectId',
+ ref: 'Movie'
+ }
+ }));
+
+
+ const movies = await Movie.create([
+ { name: 'The Empire Strikes Back' },
+ { name: 'Jingle All The Way' }
+ ]);
+
+ const people = await Person.insertMany([
+ { name: 'Test1', favoriteMovie: movies[1]._id },
+ { name: 'Test2', favoriteMovie: movies[0]._id }
+ ], { populate: 'favoriteMovie' });
+
+ assert.equal(people.length, 2);
+ assert.equal(people[0].favoriteMovie.name, 'Jingle All The Way');
+ assert.equal(people[1].favoriteMovie.name, 'The Empire Strikes Back');
+
+ });
+
+ it('insertMany() sets `isNew` for inserted documents with `ordered = false` (gh-9677)', async function() {
+ const schema = new Schema({
+ title: { type: String, required: true, unique: true }
+ });
+ const Movie = db.model('Movie', schema);
+
+ const arr = [{ title: 'The Phantom Menace' }, { title: 'The Phantom Menace' }];
+ const opts = { ordered: false };
+
+ await Movie.init();
+ const err = await Movie.insertMany(arr, opts).then(() => err, err => err);
+ assert.ok(err);
+ assert.ok(err.insertedDocs);
+
+ assert.equal(err.insertedDocs.length, 1);
+ assert.strictEqual(err.insertedDocs[0].isNew, false);
+
+ });
+
+ it('insertMany() returns only inserted docs with `ordered = true`', async function() {
+ const schema = new Schema({
+ name: { type: String, required: true, unique: true }
+ });
+ const Movie = db.model('Movie', schema);
+
+ const arr = [
+ { name: 'The Phantom Menace' },
+ { name: 'The Empire Strikes Back' },
+ { name: 'The Phantom Menace' },
+ { name: 'Jingle All The Way' }
+ ];
+ const opts = { ordered: true };
+
+ await Movie.init();
+ const err = await Movie.insertMany(arr, opts).then(() => err, err => err);
+ assert.ok(err);
+ assert.ok(err.insertedDocs);
+
+ assert.equal(err.insertedDocs.length, 2);
+ assert.strictEqual(err.insertedDocs[0].name, 'The Phantom Menace');
+ assert.strictEqual(err.insertedDocs[1].name, 'The Empire Strikes Back');
+
+ });
+
+ it('insertMany() validation error with ordered true and rawResult true when all documents are invalid', async function() {
+ const schema = new Schema({
+ name: { type: String, required: true }
+ });
+ const Movie = db.model('Movie', schema);
+
+ const arr = [
+ { foo: 'The Phantom Menace' },
+ { foobar: 'The Force Awakens' }
+ ];
+ const opts = { ordered: true, rawResult: true };
+ const error = await Movie.insertMany(arr, opts).then(() => null, err => err);
+ assert.ok(error);
+ assert.equal(error.name, 'ValidationError');
+ });
+
+ it('insertMany() validation error with ordered false and rawResult true when all documents are invalid', async function() {
+ const schema = new Schema({
+ name: { type: String, required: true }
+ });
+ const Movie = db.model('Movie', schema);
+
+ const arr = [
+ { foo: 'The Phantom Menace' },
+ { foobar: 'The Force Awakens' }
+ ];
+ const opts = { ordered: false, rawResult: true };
+ const res = await Movie.insertMany(arr, opts);
+ assert.equal(res.mongoose.validationErrors.length, 2);
+ assert.equal(res.mongoose.validationErrors[0].name, 'ValidationError');
+ assert.equal(res.mongoose.validationErrors[1].name, 'ValidationError');
+ });
+
+ it('insertMany() depopulate (gh-4590)', async function() {
+ const personSchema = new Schema({
+ name: String
+ });
+ const movieSchema = new Schema({
+ name: String,
+ leadActor: {
+ type: Schema.Types.ObjectId,
+ ref: 'gh4590'
+ }
+ });
+
+ const Person = db.model('Person', personSchema);
+ const Movie = db.model('Movie', movieSchema);
+
+ const arnold = new Person({ name: 'Arnold Schwarzenegger' });
+ const movies = [{ name: 'Predator', leadActor: arnold }];
+ const docs = await Movie.insertMany(movies);
+ assert.equal(docs.length, 1);
+ const doc = await Movie.findOne({ name: 'Predator' });
+ assert.equal(doc.leadActor.toHexString(), arnold._id.toHexString());
+ });
+
+ it('insertMany() with error handlers (gh-6228)', async function() {
+ const schema = new Schema({
+ name: { type: String, unique: true }
+ }, { autoIndex: false });
+
+ let postCalled = 0;
+ let postErrorCalled = 0;
+ schema.post('insertMany', (doc, next) => {
+ ++postCalled;
+ next();
+ });
+
+ schema.post('insertMany', (err, doc, next) => {
+ ++postErrorCalled;
+ next(err);
+ });
+
+ const Movie = db.model('Movie', schema);
+
+
+ await Movie.createIndexes();
+
+ let threw = false;
+ try {
+ await Movie.insertMany([
+ { name: 'Star Wars' },
+ { name: 'Star Wars' }
+ ]);
+ } catch (error) {
+ assert.ok(error);
+ threw = true;
+ }
+
+ assert.ok(threw);
+ assert.equal(postCalled, 0);
+ assert.equal(postErrorCalled, 1);
+
+ await Movie.collection.drop();
+
+ });
+
+ it('insertMany() with non object array error can be catched (gh-8363)', function() {
+ const schema = mongoose.Schema({
+ _id: mongoose.Schema.Types.ObjectId,
+ url: { type: String }
+ });
+ const Image = db.model('Test', schema);
+ return Image.insertMany(['a', 'b', 'c']).then(
+ () => assert.ok(false),
+ (error) => {
+ assert.equal(error.name, 'ObjectParameterError');
+ }
+ );
+ });
+
+ it('insertMany() return docs with empty modifiedPaths (gh-7852)', async function() {
+ const schema = new Schema({
+ name: { type: String }
+ });
+
+ const Food = db.model('Test', schema);
+
+
+ const foods = await Food.insertMany([
+ { name: 'Rice dumplings' },
+ { name: 'Beef noodle' }
+ ]);
+ assert.equal(foods[0].modifiedPaths().length, 0);
+ assert.equal(foods[1].modifiedPaths().length, 0);
+
+ });
+
+ it('insertMany with Decimal (gh-5190)', async function() {
+ const version = start.mongodVersion();
+
+ const mongo34 = version[0] > 3 || (version[0] === 3 && version[1] >= 4);
+ if (!mongo34) {
+ return;
+ }
+
+ const schema = new mongoose.Schema({
+ amount: mongoose.Schema.Types.Decimal
+ });
+ const Money = db.model('Test', schema);
+
+ await Money.insertMany([{ amount: '123.45' }]);
+ });
+});
diff --git a/test/model.mapreduce.test.js b/test/model.mapreduce.test.js
deleted file mode 100644
index 937a59270d0..00000000000
--- a/test/model.mapreduce.test.js
+++ /dev/null
@@ -1,301 +0,0 @@
-/* global emit */
-/**
- * Test dependencies.
- */
-
-'use strict';
-
-const start = require('./common');
-
-const assert = require('assert');
-const random = require('./util').random;
-
-const mongoose = start.mongoose;
-const Schema = mongoose.Schema;
-const ObjectId = Schema.Types.ObjectId;
-
-describe('model: mapreduce:', function() {
- let Comments;
- let BlogPost;
- let db;
-
- before(function() {
- Comments = new Schema();
-
- Comments.add({
- title: String,
- date: Date,
- body: String,
- comments: [Comments]
- });
-
- BlogPost = new Schema({
- title: String,
- author: String,
- slug: String,
- date: Date,
- meta: {
- date: Date,
- visitors: Number
- },
- published: Boolean,
- mixed: {},
- numbers: [Number],
- owners: [ObjectId],
- comments: [Comments]
- });
-
- db = start();
- });
-
- after(async function() {
- await db.close();
- });
-
- beforeEach(function() {
- db.deleteModel(/.*/);
- });
-
- it('works', async function() {
- const MR = db.model('MapReduce', BlogPost);
-
- const id = new mongoose.Types.ObjectId();
- const authors = 'aaron guillermo brian nathan'.split(' ');
- const num = 10;
- const docs = [];
- for (let i = 0; i < num; ++i) {
- docs.push({ author: authors[i % authors.length], owners: [id], published: true });
- }
-
- const insertedDocs = await MR.create(docs);
-
- const magicID = insertedDocs[1]._id;
-
- const o1 = {
- map: 'function() { emit(this.author, 1); }',
- reduce: function(k, vals) {
- return vals.length;
- }
- };
-
- const { results: ret, stats } = await MR.mapReduce(o1);
-
-
- assert.ok(Array.isArray(ret));
- assert.ok(stats);
- ret.forEach(function(res) {
- if (res._id === 'aaron') {
- assert.equal(res.value, 3);
- }
- if (res._id === 'guillermo') {
- assert.equal(res.value, 3);
- }
- if (res._id === 'brian') {
- assert.equal(res.value, 2);
- }
- if (res._id === 'nathan') {
- assert.equal(res.value, 2);
- }
- });
-
- const o2 = {
- map: function() {
- emit(this.author, 1);
- },
- reduce: function(k, vals) {
- return vals.length;
- },
- query: { author: 'aaron', published: 1, owners: id }
- };
-
- const res2 = await MR.mapReduce(o2);
-
- const ret2 = res2.results;
- const stats2 = res2.stats;
-
- assert.ok(Array.isArray(ret2));
- assert.equal(ret2.length, 1);
- assert.equal(ret2[0]._id, 'aaron');
- assert.equal(ret2[0].value, 3);
- assert.ok(stats2);
-
- const o3 = {
- map: function() {
- emit(this.author, { own: magicID });
- },
- scope: { magicID: magicID },
- reduce: function(k, vals) {
- return { own: vals[0].own, count: vals.length };
- },
- out: { replace: '_mapreduce_test_' + random() }
- };
-
- const res3 = await MR.mapReduce(o3);
-
- const model = res3.model;
-
- // ret is a model
- assert.equal(typeof model.findOne, 'function');
- assert.equal(typeof model.mapReduce, 'function');
-
- // queries work
- const docs3 = await model.where('value.count').gt(1).sort({ _id: 1 }).exec();
-
- assert.equal(docs3[0]._id, 'aaron');
- assert.equal(docs3[1]._id, 'brian');
- assert.equal(docs3[2]._id, 'guillermo');
- assert.equal(docs3[3]._id, 'nathan');
-
- // update casting works
- const doc2 = await model.findOneAndUpdate({ _id: 'aaron' }, { published: true }, { new: true });
-
- assert.ok(doc2);
- assert.equal(doc2._id, 'aaron');
- assert.equal(doc2.published, true);
-
- // ad-hoc population works
- const doc3 = await model
- .findOne({ _id: 'aaron' })
- .populate({ path: 'value.own', model: 'MapReduce', strictPopulate: false })
- .exec();
-
- assert.equal(doc3.value.own.author, 'guillermo');
- });
-
- it('withholds stats with false verbosity', async function() {
- const MR = db.model('MapReduce', BlogPost);
-
- const o = {
- map: function() {
- },
- reduce: function() {
- return 'test';
- },
- verbose: false
- };
-
- assert.deepEqual(await MR.mapReduce(o), []);
- assert.equal(typeof stats, 'undefined');
- });
-
- describe('promises (gh-1628)', function() {
- it('are returned', function() {
- const MR = db.model('MapReduce', BlogPost);
-
- const o = {
- map: function() {
- },
- reduce: function() {
- return 'test';
- }
- };
-
- const promise = MR.mapReduce(o);
- assert.ok(promise instanceof mongoose.Promise);
- });
- });
-
- describe('with promises', function() {
- let MR;
- let db;
- let magicID;
- let id;
- const docs = [];
-
- before(async function() {
- db = start();
- MR = db.model('MapReduce', BlogPost);
-
- id = new mongoose.Types.ObjectId();
- const authors = 'aaron guillermo brian nathan'.split(' ');
- const num = 10;
- for (let i = 0; i < num; ++i) {
- docs.push({ author: authors[i % authors.length], owners: [id], published: true });
- }
-
- const [b] = await MR.create(docs);
- magicID = b._id;
- });
-
- after(async function() {
- await db.close();
- });
-
- it('works', function() {
- const o = {
- map: function() {
- emit(this.author, 1);
- },
- reduce: function(k, vals) {
- return vals.length;
- }
- };
-
- return MR.mapReduce(o).then(function(res) {
- const ret = res.results;
- assert.ok(Array.isArray(ret));
- ret.forEach(function(res) {
- if (res._id === 'aaron') {
- assert.equal(res.value, 6);
- }
- if (res._id === 'guillermo') {
- assert.equal(res.value, 6);
- }
- if (res._id === 'brian') {
- assert.equal(res.value, 4);
- }
- if (res._id === 'nathan') {
- assert.equal(res.value, 4);
- }
- });
- });
- });
-
- it('works with model', function() {
- const o = {
- map: function() {
- emit(this.author, { own: magicID });
- },
- scope: { magicID: magicID },
- reduce: function(k, vals) {
- return { own: vals[0].own, count: vals.length };
- },
- out: { replace: '_mapreduce_test_' + random() }
- };
-
- return MR.mapReduce(o).
- then(function(res) {
- const ret = res.model;
- // ret is a model
- assert.ok(!Array.isArray(ret));
- assert.equal(typeof ret.findOne, 'function');
- assert.equal(typeof ret.mapReduce, 'function');
-
- // queries work
- return ret.where('value.count').gt(1).sort({ _id: 1 });
- }).
- then(function(docs) {
- assert.equal(docs[0]._id, 'aaron');
- assert.equal(docs[1]._id, 'brian');
- assert.equal(docs[2]._id, 'guillermo');
- assert.equal(docs[3]._id, 'nathan');
- });
- });
- });
-
- it('withholds stats with false verbosity using then', async function() {
- const MR = db.model('MapReduce', BlogPost);
-
- const o = {
- map: function() {
- },
- reduce: function() {
- return 'test';
- },
- verbose: false
- };
-
- const { stats } = await MR.mapReduce(o);
- assert.equal(typeof stats, 'undefined');
- });
-});
diff --git a/test/model.middleware.preposttypes.test.js b/test/model.middleware.preposttypes.test.js
new file mode 100644
index 00000000000..952bc901001
--- /dev/null
+++ b/test/model.middleware.preposttypes.test.js
@@ -0,0 +1,315 @@
+'use strict';
+
+/*
+ * Test dependencies.
+ */
+const start = require('./common');
+const assert = require('assert');
+const { fail } = require('assert');
+
+const mongoose = start.mongoose;
+const Schema = mongoose.Schema;
+
+
+/*
+ * Constants and helpers
+ */
+const QUERY = 0;
+const DOC = 1;
+const UNION = 2;
+const NEVER = 3;
+const TYPE_TO_NAME = ['Query', 'Document', 'Document|Query', 'never'];
+
+function getTypeName(obj) {
+ if (typeof obj === 'object') {
+ if (obj instanceof mongoose.Query) {
+ return 'Query';
+ } else if (obj instanceof mongoose.Document) {
+ return 'Document';
+ } else {
+ try {
+ return this.constructor.name;
+ } catch (err) {
+ return 'unknown';
+ }
+ }
+ } else {
+ return typeof obj;
+ }
+}
+
+/* Helper for generating tsd test test/types/middleware.preposttypes.test.ts,
+ * only active when "GEN_TSD" is true
+ */
+const GEN_TSD = false;
+const tsdOut = [];
+if (GEN_TSD) {
+ tsdOut.push('/* The following tests were generated by test/model.middleware.preposttypes.test.js (with GEN_TSD hardcoded set to true) */');
+}
+function jsfy(obj) {
+ if (typeof obj === 'string') {
+ return `'${obj}'`;
+ } else if (obj instanceof Array) {
+ return `[${obj.map(jsfy).join(', ')}]`;
+ } else {
+ return JSON.stringify(obj).replace(/"/g, '').replace(/([{,:])/g, '$1 ').replace(/([}])/g, ' $1');
+ }
+}
+
+/* Tests */
+describe('pre/post hooks, type of this', function() {
+ let db;
+
+ before(function() {
+ db = start();
+ });
+
+ after(async function() {
+ await db.close();
+ });
+
+ afterEach(() => require('./util').clearTestData(db));
+ afterEach(() => require('./util').stopRemainingOps(db));
+
+ /**
+ * One single test for all the different types of hooks. This test is checking the type annotations of hooks in index.d.ts.
+ */
+ it('dynamic type of this in pre/post hooks', async function() {
+ const schema = new Schema({ data: String });
+ const signatures = new Map(); // hook name to be called with types with which it has been called
+
+ // register hooks to mongoose and to map in order to check whether hook has been called
+ function registerHooks(expThisType /* QUERY, DOC, UNION, NEVER */, method /* save, updateOne etc. */, options/* ? {document,query} */) {
+
+ for (const hook of ['pre', 'post']) {
+ // only for TSD test generation ----------------------
+ if (GEN_TSD) {
+ const thisTypeStr = ['Query', 'HydratedDocument', 'Query|HydratedDocument', 'never'][expThisType];
+ if (hook === 'pre') {
+ tsdOut.push(`schema.${hook}(${jsfy(method).replace(/"/g, '')}${options ? ', ' + jsfy(options) : ''}, function() {
+ expectType<${thisTypeStr}>(this);
+});`);
+ } else if (hook === 'post') {
+ tsdOut.push(`schema.${hook}(${jsfy(method).replace(/"/g, '')}${options ? ', ' + jsfy(options) : ''}, function(res) {
+ expectType<${thisTypeStr}>(this);
+ expectNotType>(res);
+});`);
+ }
+ }
+ // --------------------------------------------------
+
+ const methods = method instanceof Array ? method : [method];
+ // create signature (for messages)
+ const hookSignature = [ // not 100% accurate, but good enough for this test
+ () => `${hook}>(method: '${methods.join('\'|\'')}'${options ? ', ' + JSON.stringify(options) : ''}, fn: ${hook[0].toUpperCase()}${hook.slice(1)}MiddlewareFunction): this;`,
+ () => `${hook}>(method: '${methods.join('\'|\'')}'${options ? ', ' + JSON.stringify(options) : ''}, fn: ${hook[0].toUpperCase()}${hook.slice(1)}MiddlewareFunction): this;`,
+ () => `${hook}|Query>(method: '${methods.join('\'|\'')}'${options ? ', ' + JSON.stringify(options) : ''}, fn: ${hook[0].toUpperCase()}${hook.slice(1)}MiddlewareFunction): this;`,
+ () => `${hook}(method: '${methods.join('\'|\'')}'${options ? ', ' + JSON.stringify(options) : ''}, fn: ${hook[0].toUpperCase()}${hook.slice(1)}MiddlewareFunction): this;`
+ ][expThisType]();
+ assert(!signatures.has(hookSignature), `hook already registered: ${hookSignature}`);
+ signatures.set(hookSignature, new Set());
+
+ // the callback checking the type and registering the call
+ const fn = function() {
+ const actThisType = getTypeName(this);
+
+ switch (expThisType) {
+ case QUERY:
+ case DOC:
+ assert(actThisType === TYPE_TO_NAME[expThisType], `this was ${actThisType}, should be ${TYPE_TO_NAME[expThisType]} for hook ${hookSignature}`); break;
+ case UNION: assert(actThisType === 'Document' || actThisType === 'Query', `this was ${actThisType}, should be ${TYPE_TO_NAME[expThisType]} for hook ${hookSignature}`); break;
+ case NEVER: fail(`this was ${actThisType}, hook ${hookSignature} should never have been called`); break;
+ }
+ const calledWith = signatures.get(hookSignature);
+ calledWith.add(actThisType);
+
+ };
+
+ const fnPost = function(res) {
+ fn.call(this);
+ const resType = getTypeName(res);
+ assert(resType !== 'Query', 'type of res in post middleware is not expected to be a Query');
+ };
+
+ // register the hook
+ if (options) {
+ switch (hook) {
+ case 'pre': schema.pre(method, options, fn); break;
+ case 'post': schema.post(method, options, fnPost); break;
+ }
+ } else {
+ switch (hook) {
+ case 'pre': schema.pre(method, fn); break;
+ case 'post': schema.post(method, fnPost); break;
+ }
+ }
+ }
+ }
+
+ // checks whether all registered hooks have been called
+ function checkCalls() {
+ const failures = [];
+ for (const [hookName, calledWith] of signatures.entries()) {
+ const calledWithString = () => {
+ if (calledWith.size == 0) return 'never called';
+ return 'called with ' + [...calledWith].join(', ');
+ };
+ if (hookName.indexOf('never') >= 0) {
+ if (calledWith.size > 0) {
+ failures.push(`hook ${hookName} should never have been called but was ${calledWithString()}.`);
+ }
+ } else if (hookName.indexOf('|Query') >= 0) { // UNION
+ if (!(calledWith.has('Query') && calledWith.has('Document'))) {
+ failures.push(`hook ${hookName} should have been called with Document and Query, was ${calledWithString()}.`);
+ }
+ } else if (hookName.indexOf('Query') >= 0) { // QUERY
+ if (!calledWith.has('Query')) {
+ failures.push(`hook ${hookName} should have been called with Query, was ${calledWithString()}.`);
+ }
+ } else if (hookName.indexOf('Document') >= 0) { // DOC
+ if (!calledWith.has('Document')) {
+ failures.push(`hook ${hookName} should have been called with Document, was ${calledWithString()}.`);
+ }
+ } else {
+ failures.push(`Error in test, do not recognize type of hook ${hookName}`);
+ }
+ }
+ return failures.join('\n - ');
+ }
+
+ // --------------------------------------------------------------------------
+ // register hooks; here we actually see the correct type annotations in action
+ const MongooseQueryAndDocumentMiddleware = ['updateOne', 'deleteOne', 'validate'];
+
+ const MongooseDistinctDocumentMiddleware = ['save', 'init'];
+ const MongooseDocumentMiddleware = [...MongooseDistinctDocumentMiddleware, ...MongooseQueryAndDocumentMiddleware];
+
+ const MongooseDistinctQueryMiddleware = [
+ 'estimatedDocumentCount', 'countDocuments',
+ 'deleteMany', 'distinct',
+ 'find', 'findOne', 'findOneAndDelete', 'findOneAndReplace', 'findOneAndUpdate',
+ 'replaceOne', 'updateMany'];
+ const MongooseDefaultQueryMiddleware = [...MongooseDistinctQueryMiddleware, 'updateOne', 'deleteOne'];
+ const MongooseQueryMiddleware = [...MongooseDistinctQueryMiddleware, ...MongooseQueryAndDocumentMiddleware];
+
+ const MongooseQueryOrDocumentMiddleware = [
+ ...MongooseDistinctQueryMiddleware,
+ ...MongooseDistinctDocumentMiddleware,
+ ...MongooseQueryAndDocumentMiddleware];
+
+ // first: one method only
+ for (const method of MongooseDistinctDocumentMiddleware) {
+ registerHooks(DOC, method);
+ registerHooks(DOC, method, { document: true, query: false });
+ registerHooks(DOC, method, { document: true, query: true });
+ registerHooks(NEVER, method, { document: false, query: true });
+ registerHooks(NEVER, method, { document: false, query: false });
+ // ------------------------------------------------------------
+ // always Document (or never, which we do not need to defined in index.d.ts)
+ }
+ for (const method of MongooseDistinctQueryMiddleware) {
+ registerHooks(QUERY, method);
+ registerHooks(QUERY, method, { document: false, query: true });
+ registerHooks(QUERY, method, { document: true, query: true });
+ registerHooks(NEVER, method, { document: true, query: false });
+ registerHooks(NEVER, method, { document: false, query: false });
+ // ------------------------------------------------------------
+ // always Query (or never, which we do not need to defined in index.d.ts)
+ }
+ for (const method of ['updateOne', 'deleteOne']) { // MongooseDefaultQueryMiddleware w/o distinct
+ registerHooks(QUERY, method);
+ // defaults to Query
+ registerHooks(QUERY, method, { document: false, query: true });
+ registerHooks(DOC, method, { document: true, query: false });
+ registerHooks(UNION, method, { document: true, query: true });
+ registerHooks(NEVER, method, { document: false, query: false });
+ // ------------------------------------------------------------
+ // When literals are unknown, it is Union of Document|Query (or never, which we do not need to defined in index.d.ts)
+ }
+
+ // method arrays
+ registerHooks(DOC, MongooseDistinctDocumentMiddleware);
+ registerHooks(DOC, MongooseDistinctDocumentMiddleware, { document: true, query: false });
+ registerHooks(DOC, MongooseDistinctDocumentMiddleware, { document: true, query: true });
+ registerHooks(NEVER, MongooseDistinctDocumentMiddleware, { document: false, query: true });
+ registerHooks(NEVER, MongooseDistinctDocumentMiddleware, { document: false, query: false });
+
+ registerHooks(QUERY, MongooseDistinctQueryMiddleware);
+ registerHooks(QUERY, MongooseDistinctQueryMiddleware, { document: false, query: true });
+ registerHooks(QUERY, MongooseDistinctQueryMiddleware, { document: true, query: true });
+ registerHooks(NEVER, MongooseDistinctQueryMiddleware, { document: true, query: false });
+ registerHooks(NEVER, MongooseDistinctQueryMiddleware, { document: false, query: false });
+
+ registerHooks(QUERY, MongooseDefaultQueryMiddleware);
+ registerHooks(QUERY, MongooseDefaultQueryMiddleware, { document: false, query: true });
+ registerHooks(DOC, MongooseDefaultQueryMiddleware, { document: true, query: false });
+ registerHooks(UNION, MongooseDefaultQueryMiddleware, { document: true, query: true });
+ registerHooks(NEVER, MongooseDefaultQueryMiddleware, { document: false, query: false });
+
+ // registerHooks(DOC, MongooseDefaultDocumentMiddleware);
+ // registerHooks(QUERY, MongooseDefaultDocumentMiddleware, { document: false, query: true });
+ // registerHooks(DOC, MongooseDefaultDocumentMiddleware, { document: true, query: false });
+ // registerHooks(UNION, MongooseDefaultDocumentMiddleware, { document: true, query: true });
+ // registerHooks(NEVER, MongooseDefaultDocumentMiddleware, { document: false, query: false });
+
+ registerHooks(UNION, MongooseDocumentMiddleware);
+ registerHooks(QUERY, MongooseDocumentMiddleware, { document: false, query: true });
+ registerHooks(DOC, MongooseDocumentMiddleware, { document: true, query: false });
+ registerHooks(UNION, MongooseDocumentMiddleware, { document: true, query: true });
+ registerHooks(NEVER, MongooseDocumentMiddleware, { document: false, query: false });
+
+ registerHooks(UNION, MongooseQueryMiddleware);
+ registerHooks(QUERY, MongooseQueryMiddleware, { document: false, query: true });
+ registerHooks(DOC, MongooseQueryMiddleware, { document: true, query: false });
+ registerHooks(UNION, MongooseQueryMiddleware, { document: true, query: true });
+ registerHooks(NEVER, MongooseQueryMiddleware, { document: false, query: false });
+
+ registerHooks(UNION, MongooseQueryOrDocumentMiddleware);
+ registerHooks(QUERY, MongooseQueryOrDocumentMiddleware, { document: false, query: true });
+ registerHooks(DOC, MongooseQueryOrDocumentMiddleware, { document: true, query: false });
+ registerHooks(UNION, MongooseQueryOrDocumentMiddleware, { document: true, query: true });
+ registerHooks(NEVER, MongooseQueryOrDocumentMiddleware, { document: false, query: false });
+
+ // --------------------------------------------------------------------------
+ // trigger hooks
+ try {
+ const Doc = db.model('Test', schema);
+ let doc = new Doc({ data: 'value' });
+ await doc.save(); // triggers save and validate hooks
+
+ // MongooseDistinctQueryMiddleware
+ await Doc.estimatedDocumentCount().exec();
+ await Doc.countDocuments().exec();
+ await Doc.deleteMany().exec(); await Doc.create({ data: 'value' });
+ await Doc.distinct('data').exec();
+ await Doc.find({}).exec();
+ await Doc.findOne({}).exec();
+ await Doc.findOneAndDelete({}).exec(); await Doc.create({ data: 'value' });
+ await Doc.findOneAndReplace({}, { data: 'valueRep' }).exec();
+ await Doc.findOneAndUpdate({}, { data: 'valueUpd' }).exec();
+ await Doc.replaceOne({}, { data: 'value' }).exec();
+ await Doc.updateOne({ data: 'value' }).exec();
+ await Doc.updateMany({ data: 'value' }).exec();
+
+ // MongooseQueryOrDocumentMiddleware, use Query
+ await Doc.deleteOne({}).exec(); await Doc.create({ data: 'value' });
+ await Doc.updateOne({ data: 'value' }) // call updateOne and
+ .setOptions({ runValidators: true }) // validate hook
+ .exec();
+
+ // MongooseQueryOrDocumentMiddleware, use Document
+ doc = await Doc.create({ data: 'doc2' });
+ await doc.updateOne({ data: 'value' }); // updateOne
+ await doc.deleteOne(); doc = await Doc.create({ data: 'doc3' });
+
+ const callResult = checkCalls();
+ assert(callResult.length == 0, 'Unexpected hook calls:\n - ' + callResult);
+ } catch (err) {
+ assert.fail(err);
+ }
+ if (GEN_TSD) {
+ tsdOut.push('/* end of generated tests */');
+ console.log(tsdOut.join('\n\n'));
+ }
+ });
+});
diff --git a/test/model.middleware.test.js b/test/model.middleware.test.js
index 30114ad8ccc..92ca5224dee 100644
--- a/test/model.middleware.test.js
+++ b/test/model.middleware.test.js
@@ -26,7 +26,7 @@ describe('model middleware', function() {
afterEach(() => require('./util').clearTestData(db));
afterEach(() => require('./util').stopRemainingOps(db));
- it('post save', function(done) {
+ it('post save', async function() {
const schema = new Schema({
title: String
});
@@ -58,15 +58,13 @@ describe('model middleware', function() {
const test = new TestMiddleware({ title: 'Little Green Running Hood' });
- test.save(function(err) {
- assert.ifError(err);
- assert.equal(test.title, 'Little Green Running Hood');
- assert.equal(called, 3);
- done();
- });
+ await test.save();
+
+ assert.equal(test.title, 'Little Green Running Hood');
+ assert.equal(called, 3);
});
- it('sync error in post save (gh-3483)', function(done) {
+ it('sync error in post save (gh-3483)', async function() {
const schema = new Schema({
title: String
});
@@ -79,14 +77,13 @@ describe('model middleware', function() {
const test = new TestMiddleware({ title: 'Test' });
- test.save(function(err) {
+ await test.save().catch(err => {
assert.ok(err);
assert.equal(err.message, 'woops!');
- done();
});
});
- it('pre hook promises (gh-3779)', function(done) {
+ it('pre hook promises (gh-3779)', async function() {
const schema = new Schema({
title: String
});
@@ -105,11 +102,8 @@ describe('model middleware', function() {
const test = new TestMiddleware({ title: 'Test' });
- test.save(function(err) {
- assert.ifError(err);
- assert.equal(calledPre, 1);
- done();
- });
+ await test.save();
+ assert.equal(calledPre, 1);
});
it('post hook promises (gh-3779)', async function() {
@@ -135,7 +129,7 @@ describe('model middleware', function() {
assert.equal(doc.title, 'From Post Save');
});
- it('validate middleware runs before save middleware (gh-2462)', function(done) {
+ it('validate middleware runs before save middleware (gh-2462)', async() => {
const schema = new Schema({
title: String
});
@@ -153,13 +147,11 @@ describe('model middleware', function() {
const Book = db.model('Test', schema);
- Book.create({}, function() {
- assert.equal(count, 2);
- done();
- });
+ await Book.create({});
+ assert.equal(count, 2);
});
- it('works', function(done) {
+ it('works', async function() {
const schema = new Schema({
title: String
});
@@ -175,7 +167,7 @@ describe('model middleware', function() {
next(new Error('Error 101'));
});
- schema.pre('remove', function(next) {
+ schema.pre('deleteOne', { document: true, query: false }, function(next) {
called++;
next();
});
@@ -184,22 +176,21 @@ describe('model middleware', function() {
const test = new TestMiddleware();
- test.init({ title: 'Test' }, function(err) {
- assert.ifError(err);
- assert.equal(called, 1);
+ await test.init({ title: 'Test' });
+ assert.equal(called, 1);
- test.save(function(err) {
- assert.ok(err instanceof Error);
- assert.equal(err.message, 'Error 101');
- assert.equal(called, 2);
+ try {
+ await test.save();
- test.remove(function(err) {
- assert.ifError(err);
- assert.equal(called, 3);
- done();
- });
- });
- });
+ assert.ok(false);
+ } catch (err) {
+ assert.ok(err instanceof Error);
+ assert.equal(err.message, 'Error 101');
+ assert.equal(called, 2);
+ }
+
+ await test.deleteOne();
+ assert.equal(called, 3);
});
describe('post init hooks', function() {
@@ -242,7 +233,7 @@ describe('model middleware', function() {
});
});
- it('gh-1829', function(done) {
+ it('gh-1829', async function() {
const childSchema = new mongoose.Schema({
name: String
});
@@ -278,27 +269,26 @@ describe('model middleware', function() {
]
});
- parent.save(function(error) {
- assert.ifError(error);
- assert.equal(childPreCalls, 2);
- assert.equal(childPreCallsByName.Jaina, 1);
- assert.equal(childPreCallsByName.Jacen, 1);
- assert.equal(parentPreCalls, 1);
- parent.children[0].name = 'Anakin';
- parent.save(function(error) {
- assert.ifError(error);
- assert.equal(childPreCalls, 4);
- assert.equal(childPreCallsByName.Anakin, 1);
- assert.equal(childPreCallsByName.Jaina, 1);
- assert.equal(childPreCallsByName.Jacen, 2);
-
- assert.equal(parentPreCalls, 2);
- done();
- });
- });
+ await parent.save();
+
+ assert.equal(childPreCalls, 2);
+ assert.equal(childPreCallsByName.Jaina, 1);
+ assert.equal(childPreCallsByName.Jacen, 1);
+ assert.equal(parentPreCalls, 1);
+
+ parent.children[0].name = 'Anakin';
+
+ await parent.save();
+
+ assert.equal(childPreCalls, 4);
+ assert.equal(childPreCallsByName.Anakin, 1);
+ assert.equal(childPreCallsByName.Jaina, 1);
+ assert.equal(childPreCallsByName.Jacen, 2);
+
+ assert.equal(parentPreCalls, 2);
});
- it('sync error in pre save (gh-3483)', function(done) {
+ it('sync error in pre save (gh-3483)', async function() {
const schema = new Schema({
title: String
});
@@ -311,14 +301,17 @@ describe('model middleware', function() {
const test = new TestMiddleware({ title: 'Test' });
- test.save(function(err) {
+ try {
+ await test.save();
+
+ throw new Error('Should not get here');
+ } catch (err) {
assert.ok(err);
assert.equal(err.message, 'woops!');
- done();
- });
+ }
});
- it('sync error in pre save after next() (gh-3483)', function(done) {
+ it('sync error in pre save after next() (gh-3483)', async function() {
const schema = new Schema({
title: String
});
@@ -340,14 +333,11 @@ describe('model middleware', function() {
const test = new TestMiddleware({ title: 'Test' });
- test.save(function(error) {
- assert.ifError(error);
- assert.equal(called, 1);
- done();
- });
+ await test.save();
+ assert.equal(called, 1);
});
- it('validate + remove', function(done) {
+ it('validate + remove', async function() {
const schema = new Schema({
title: String
});
@@ -362,7 +352,7 @@ describe('model middleware', function() {
next();
});
- schema.pre('remove', function(next) {
+ schema.pre('deleteOne', { document: true, query: false }, function(next) {
++preRemove;
next();
});
@@ -372,7 +362,7 @@ describe('model middleware', function() {
++postValidate;
});
- schema.post('remove', function(doc) {
+ schema.post('deleteOne', { document: true, query: false }, function(doc) {
assert.ok(doc instanceof mongoose.Document);
++postRemove;
});
@@ -381,21 +371,17 @@ describe('model middleware', function() {
const test = new Test({ title: 'banana' });
- test.save(function(err) {
- assert.ifError(err);
- assert.equal(preValidate, 1);
- assert.equal(postValidate, 1);
- assert.equal(preRemove, 0);
- assert.equal(postRemove, 0);
- test.remove(function(err) {
- assert.ifError(err);
- assert.equal(preValidate, 1);
- assert.equal(postValidate, 1);
- assert.equal(preRemove, 1);
- assert.equal(postRemove, 1);
- done();
- });
- });
+ await test.save();
+ assert.equal(preValidate, 1);
+ assert.equal(postValidate, 1);
+ assert.equal(preRemove, 0);
+ assert.equal(postRemove, 0);
+
+ await test.deleteOne();
+ assert.equal(preValidate, 1);
+ assert.equal(postValidate, 1);
+ assert.equal(preRemove, 1);
+ assert.equal(postRemove, 1);
});
it('static hooks (gh-5982)', async function() {
@@ -461,14 +447,147 @@ describe('model middleware', function() {
await doc.deleteOne();
- assert.equal(queryPreCalled, 0);
+ assert.equal(queryPreCalled, 1);
assert.equal(preCalled, 1);
assert.equal(postCalled, 1);
await Model.deleteOne();
- assert.equal(queryPreCalled, 1);
+ assert.equal(queryPreCalled, 2);
assert.equal(preCalled, 1);
assert.equal(postCalled, 1);
});
+
+ describe('createCollection middleware', function() {
+ it('calls createCollection hooks', async function() {
+ const schema = new Schema({ name: String }, { autoCreate: true });
+
+ const pre = [];
+ const post = [];
+ schema.pre('createCollection', function() {
+ pre.push(this);
+ });
+ schema.post('createCollection', function() {
+ post.push(this);
+ });
+
+ const Test = db.model('Test', schema);
+ await Test.init();
+ assert.equal(pre.length, 1);
+ assert.equal(pre[0], Test);
+ assert.equal(post.length, 1);
+ assert.equal(post[0], Test);
+ });
+
+ it('allows skipping createCollection from hooks', async function() {
+ const schema = new Schema({ name: String }, { autoCreate: true });
+
+ schema.pre('createCollection', function(next) {
+ next(mongoose.skipMiddlewareFunction());
+ });
+
+ const Test = db.model('CreateCollectionHookTest', schema);
+ await Test.init();
+ const collections = await db.listCollections();
+ assert.equal(collections.length, 0);
+ });
+ });
+
+ describe('bulkWrite middleware', function() {
+ it('calls bulkWrite hooks', async function() {
+ const schema = new Schema({ name: String });
+
+ const pre = [];
+ const post = [];
+ schema.pre('bulkWrite', function(next, ops) {
+ pre.push(ops);
+ next();
+ });
+ schema.post('bulkWrite', function(res) {
+ post.push(res);
+ });
+
+ const Test = db.model('Test', schema);
+ await Test.bulkWrite([{
+ updateOne: {
+ filter: { name: 'foo' },
+ update: { $set: { name: 'bar' } }
+ }
+ }]);
+ assert.equal(pre.length, 1);
+ assert.deepStrictEqual(pre[0], [{
+ updateOne: {
+ filter: { name: 'foo' },
+ update: { $set: { name: 'bar' } }
+ }
+ }]);
+ assert.equal(post.length, 1);
+ assert.equal(post[0].constructor.name, 'BulkWriteResult');
+ });
+
+ it('allows updating ops', async function() {
+ const schema = new Schema({ name: String, prop: String });
+
+ schema.pre('bulkWrite', function(next, ops) {
+ ops[0].updateOne.filter.name = 'baz';
+ next();
+ });
+
+ const Test = db.model('Test', schema);
+ const { _id } = await Test.create({ name: 'baz' });
+ await Test.bulkWrite([{
+ updateOne: {
+ filter: { name: 'foo' },
+ update: { $set: { prop: 'test prop value' } }
+ }
+ }]);
+ const { prop } = await Test.findById(_id).orFail();
+ assert.equal(prop, 'test prop value');
+ });
+
+ it('supports error handlers', async function() {
+ const schema = new Schema({ name: String, prop: String });
+
+ const errors = [];
+ schema.post('bulkWrite', function(err, res, next) {
+ errors.push(err);
+ next();
+ });
+
+ const Test = db.model('Test', schema);
+ const { _id } = await Test.create({ name: 'baz' });
+ await assert.rejects(
+ Test.bulkWrite([{
+ insertOne: {
+ document: {
+ _id
+ }
+ }
+ }]),
+ /duplicate key error/
+ );
+ assert.equal(errors.length, 1);
+ assert.equal(errors[0].name, 'MongoBulkWriteError');
+ assert.ok(errors[0].message.includes('duplicate key error'), errors[0].message);
+ });
+
+ it('supports skipping wrapped function', async function() {
+ const schema = new Schema({ name: String, prop: String });
+
+ schema.pre('bulkWrite', function(next) {
+ next(mongoose.skipMiddlewareFunction('skipMiddlewareFunction test'));
+ });
+
+ const Test = db.model('Test', schema);
+ const { _id } = await Test.create({ name: 'baz' });
+ const res = await Test.bulkWrite([{
+ insertOne: {
+ document: {
+ _id
+ }
+ }
+ }]);
+ assert.strictEqual(res, 'skipMiddlewareFunction test');
+ });
+ });
});
diff --git a/test/model.populate.divergent.test.js b/test/model.populate.divergent.test.js
index e3afe80bfec..91a5dfb4579 100644
--- a/test/model.populate.divergent.test.js
+++ b/test/model.populate.divergent.test.js
@@ -47,40 +47,28 @@ describe('model: populate: divergent arrays', function() {
});
function test(check, fn) {
- it('using $set', function(done) {
- fn(function(err, doc) {
- assert.ifError(err);
- doc.array.unshift({ _id: 10, name: 'ten' });
- doc.save(function(err) {
- check(err);
- done();
- });
- });
+ it('using $set', async function() {
+ const doc = await fn();
+ doc.array.unshift({ _id: 10, name: 'ten' });
+ const err = await doc.save().then(() => null, err => err);
+ check(err);
});
- it('using $pop 1', function(done) {
- fn(function(err, doc) {
- assert.ifError(err);
- doc.array.$pop();
- doc.save(function(err) {
- check(err);
- done();
- });
- });
+ it('using $pop 1', async function() {
+ const doc = await fn();
+ doc.array.$pop();
+ const err = await doc.save().then(() => null, err => err);
+ check(err);
});
- it('using $pop -1', function(done) {
- fn(function(err, doc) {
- assert.ifError(err);
- doc.array.$shift();
- doc.save(function(err) {
- check(err);
- done();
- });
- });
+ it('using $pop -1', async function() {
+ const doc = await fn();
+ doc.array.$shift();
+ const err = await doc.save().then(() => null, err => err);
+ check(err);
});
}
function testOk(fn) {
- test(assert.ifError.bind(assert), fn);
+ test(err => assert.ifError(err), fn);
}
function testFails(fn) {
@@ -91,53 +79,53 @@ describe('model: populate: divergent arrays', function() {
}
describe('from match', function() {
- testFails(function(cb) {
- M.findOne().populate({ path: 'array', match: { name: 'one' } }).exec(cb);
+ testFails(() => {
+ return M.findOne().populate({ path: 'array', match: { name: 'one' } });
});
});
describe('from skip', function() {
describe('2', function() {
- testFails(function(cb) {
- M.findOne().populate({ path: 'array', options: { skip: 2 } }).exec(cb);
+ testFails(() => {
+ return M.findOne().populate({ path: 'array', options: { skip: 2 } });
});
});
describe('0', function() {
- testOk(function(cb) {
- M.findOne().populate({ path: 'array', options: { skip: 0 } }).exec(cb);
+ testOk(function() {
+ return M.findOne().populate({ path: 'array', options: { skip: 0 } });
});
});
});
describe('from limit', function() {
describe('0', function() {
- testFails(function(cb) {
- M.findOne().populate({ path: 'array', options: { limit: 0 } }).exec(cb);
+ testFails(function() {
+ return M.findOne().populate({ path: 'array', options: { limit: 0 } }).exec();
});
});
describe('1', function() {
- testFails(function(cb) {
- M.findOne().populate({ path: 'array', options: { limit: 1 } }).exec(cb);
+ testFails(function() {
+ return M.findOne().populate({ path: 'array', options: { limit: 1 } }).exec();
});
});
});
describe('from deselected _id', function() {
describe('using string and only -_id', function() {
- testFails(function(cb) {
- M.findOne().populate({ path: 'array', select: '-_id' }).exec(cb);
+ testFails(function() {
+ return M.findOne().populate({ path: 'array', select: '-_id' }).exec();
});
});
describe('using string', function() {
- testFails(function(cb) {
- M.findOne().populate({ path: 'array', select: 'name -_id' }).exec(cb);
+ testFails(function() {
+ return M.findOne().populate({ path: 'array', select: 'name -_id' }).exec();
});
});
describe('using object and only _id: 0', function() {
- testFails(function(cb) {
- M.findOne().populate({ path: 'array', select: { _id: 0 } }).exec(cb);
+ testFails(function() {
+ return M.findOne().populate({ path: 'array', select: { _id: 0 } }).exec();
});
});
describe('using object', function() {
- testFails(function(cb) {
- M.findOne().populate({ path: 'array', select: { _id: 0, name: 1 } }).exec(cb);
+ testFails(function() {
+ return M.findOne().populate({ path: 'array', select: { _id: 0, name: 1 } }).exec();
});
});
});
diff --git a/test/model.populate.setting.test.js b/test/model.populate.setting.test.js
index bcf072e8d0c..d1c6700e044 100644
--- a/test/model.populate.setting.test.js
+++ b/test/model.populate.setting.test.js
@@ -152,7 +152,7 @@ describe('model: populate:', function() {
assert.equal(doc.fans[6], null);
const _id = construct[id]();
- doc.fans.addToSet(_id);
+ doc.fans.addToSet({ _id });
if (Buffer.isBuffer(_id)) {
assert.equal(doc.fans[7]._id.toString('utf8'), _id.toString('utf8'));
} else {
diff --git a/test/model.populate.test.js b/test/model.populate.test.js
index 03aed108234..110471823be 100644
--- a/test/model.populate.test.js
+++ b/test/model.populate.test.js
@@ -152,19 +152,16 @@ describe('model: populate:', function() {
});
describe('deep populate', function() {
- it('deep population with refs (gh-3507)', function(done) {
- // handler schema
+ it('deep population with refs (gh-3507)', async function() {
const handlerSchema = new Schema({
name: String
});
- // task schema
const taskSchema = new Schema({
name: String,
handler: { type: Schema.Types.ObjectId, ref: 'Test' }
});
- // application schema
const applicationSchema = new Schema({
name: String,
tasks: [{ type: Schema.Types.ObjectId, ref: 'Test1' }]
@@ -174,33 +171,17 @@ describe('model: populate:', function() {
const Task = db.model('Test1', taskSchema);
const Application = db.model('Test2', applicationSchema);
- Handler.create({ name: 'test' }, function(error, doc) {
- assert.ifError(error);
- Task.create({ name: 'test2', handler: doc._id }, function(error, doc) {
- assert.ifError(error);
- const obj = { name: 'test3', tasks: [doc._id] };
- Application.create(obj, function(error, doc) {
- assert.ifError(error);
- test(doc._id);
- });
- });
- });
+ const handler = await Handler.create({ name: 'test' });
+ const task = await Task.create({ name: 'test2', handler: handler._id });
+ const obj = { name: 'test3', tasks: [task._id] };
+ const application = await Application.create(obj);
- function test(id) {
- Application.
- findById(id).
- populate([
- { path: 'tasks', populate: { path: 'handler' } }
- ]).
- exec(function(error, doc) {
- assert.ifError(error);
- assert.ok(doc.tasks[0].handler._id);
- done();
- });
- }
+ const doc = await Application.findById(application._id).populate([{ path: 'tasks', populate: { path: 'handler' } }]);
+
+ assert.ok(doc.tasks[0].handler._id);
});
- it('multiple paths with same options (gh-3808)', function(done) {
+ it('multiple paths with same options (gh-3808)', async function() {
const companySchema = new Schema({
name: String,
description: String
@@ -234,67 +215,46 @@ describe('model: populate:', function() {
target: user2._id
});
- company.save(function(error) {
- assert.ifError(error);
- User.create(user1, user2, function(error) {
- assert.ifError(error);
- message.save(function(error) {
- assert.ifError(error);
- next();
- });
- });
- });
+ await company.save();
+ await User.create(user1, user2);
+ await message.save();
- function next() {
- Comment.findOne({ _id: message._id }, function(error, message) {
- assert.ifError(error);
- const options = {
- path: 'author target',
- select: '_id name company',
- populate: {
- path: 'company',
- model: 'Company'
- }
- };
- message.populate(options, function(error) {
- assert.ifError(error);
- assert.equal(message.target.company.name, 'IniTech');
- done();
- });
- });
- }
+ const options = {
+ path: 'author target',
+ select: '_id name company',
+ populate: {
+ path: 'company',
+ model: 'Company'
+ }
+ };
+
+ const message2 = await Comment.findOne({ _id: message._id });
+
+ await message2.populate(options);
+
+ assert.equal(message2.target.company.name, 'IniTech');
});
});
- it('populating a single ref', function(done) {
+ it('populating a single ref', async function() {
const BlogPost = db.model('BlogPost', blogPostSchema);
const User = db.model('User', userSchema);
- User.create({
+ const creator = await User.create({
name: 'Guillermo',
email: 'rauchg@gmail.com'
- }, function(err, creator) {
- assert.ifError(err);
+ });
- BlogPost.create({
- title: 'woot',
- _creator: creator
- }, function(err, post) {
- assert.ifError(err);
-
- BlogPost
- .findById(post._id)
- .populate('_creator')
- .exec(function(err, post) {
- assert.ifError(err);
-
- assert.ok(post._creator instanceof User);
- assert.equal(post._creator.name, 'Guillermo');
- assert.equal(post._creator.email, 'rauchg@gmail.com');
- done();
- });
- });
+ const post = await BlogPost.create({
+ title: 'woot',
+ _creator: creator
});
+
+ const postPopulated = await BlogPost.findById(post._id).populate('_creator');
+
+ assert.ok(postPopulated._creator instanceof User);
+ assert.equal(postPopulated._creator.name, 'Guillermo');
+ assert.equal(postPopulated._creator.email, 'rauchg@gmail.com');
});
it('not failing on null as ref', async function() {
@@ -314,196 +274,158 @@ describe('model: populate:', function() {
assert.equal(foundPost._creator, null);
});
- it('not failing on empty object as ref', function(done) {
+ it('not failing on empty object as ref', async function() {
const BlogPost = db.model('BlogPost', blogPostSchema);
- BlogPost.create(
- { title: 'woot' },
- function(err, post) {
- assert.ifError(err);
+ const post = await BlogPost.create({ title: 'woot' });
- BlogPost.
- findByIdAndUpdate(post._id, { $set: { _creator: {} } }, function(err) {
- assert.ok(err);
- done();
- });
- });
+ try {
+ await BlogPost.findByIdAndUpdate(post._id, { $set: { _creator: {} } });
+ } catch (err) {
+ assert.ok(err);
+ }
});
- it('across DBs', function(done) {
+ it('across DBs', async function() {
db2 = db.useDb(start.databases[1]);
const BlogPost = db.model('BlogPost', blogPostSchema);
const User = db2.model('User', userSchema);
- User.create({
+ const creator = await User.create({
name: 'Guillermo',
email: 'rauchg@gmail.com'
- }, function(err, creator) {
- assert.ifError(err);
+ });
- BlogPost.create({
- title: 'woot',
- _creator: creator._id
- }, function(err, post) {
- assert.ifError(err);
- BlogPost
- .findById(post._id)
- .populate({ path: '_creator', select: 'name', model: User })
- .exec(function(err, post) {
- assert.ifError(err);
- assert.ok(post._creator.name === 'Guillermo');
- done();
- });
- });
+ const post = await BlogPost.create({
+ title: 'woot',
+ _creator: creator._id
});
+
+ const populatedPost = await BlogPost
+ .findById(post._id)
+ .populate({ path: '_creator', select: 'name', model: User })
+ .exec();
+
+ assert.ok(populatedPost._creator.name === 'Guillermo');
});
- it('an error in single ref population propagates', function(done) {
+
+ it('an error in single ref population propagates', async function() {
const BlogPost = db.model('BlogPost', blogPostSchema);
const User = db.model('User', userSchema);
- User.create({
+ const creator = await User.create({
name: 'Guillermo',
email: 'rauchg@gmail.com'
- }, function(err, creator) {
- assert.ifError(err);
+ });
- BlogPost.create({
- title: 'woot',
- _creator: creator
- }, function(err, post) {
- assert.ifError(err);
+ const post = await BlogPost.create({
+ title: 'woot',
+ _creator: creator
+ });
- const origExec = User.Query.prototype.exec;
+ const origExec = User.Query.prototype.exec;
- // mock an error
- User.Query.prototype.exec = function() {
- const args = Array.prototype.map.call(arguments, function(arg) {
- return typeof arg === 'function' ? function() {
- arg(new Error('woot'));
- } : arg;
- });
- return origExec.apply(this, args);
- };
+ // mock an error
+ User.Query.prototype.exec = function() {
+ throw new Error('woot');
+ };
- BlogPost
- .findById(post._id)
- .populate('_creator')
- .exec(function(err) {
- assert.ok(err instanceof Error);
- assert.equal(err.message, 'woot');
- User.Query.prototype.exec = origExec;
- done();
- });
- });
- });
+ try {
+ await BlogPost
+ .findById(post._id)
+ .populate('_creator')
+ .exec();
+ } catch (err) {
+ assert.ok(err instanceof Error);
+ assert.equal(err.message, 'woot');
+ User.Query.prototype.exec = origExec;
+ }
});
- it('populating with partial fields selection', function(done) {
+
+ it('populating with partial fields selection', async function() {
const BlogPost = db.model('BlogPost', blogPostSchema);
const User = db.model('User', userSchema);
- User.create({
+ const creator = await User.create({
name: 'Guillermo',
email: 'rauchg@gmail.com'
- }, function(err, creator) {
- assert.ifError(err);
+ });
- BlogPost.create({
- title: 'woot',
- _creator: creator
- }, function(err, post) {
- assert.ifError(err);
-
- BlogPost
- .findById(post._id)
- .populate('_creator', 'email')
- .exec(function(err, post) {
- assert.ifError(err);
-
- assert.ok(post._creator instanceof User);
- assert.equal(post._creator.isInit('name'), false);
- assert.equal(post._creator.email, 'rauchg@gmail.com');
- done();
- });
- });
+ const post = await BlogPost.create({
+ title: 'woot',
+ _creator: creator
});
+
+ const postFound = await BlogPost
+ .findById(post._id)
+ .populate('_creator', 'email')
+ .exec();
+
+ assert.ok(postFound._creator instanceof User);
+ assert.equal(postFound._creator.isInit('name'), false);
+ assert.equal(postFound._creator.email, 'rauchg@gmail.com');
});
- it('population of single oid with partial field selection and filter', function(done) {
+ it('population of single oid with partial field selection and filter', async function() {
const BlogPost = db.model('BlogPost', blogPostSchema);
const User = db.model('User', userSchema);
- User.create({
+ const creator = await User.create({
name: 'Banana',
email: 'cats@example.com'
- }, function(err, creator) {
- assert.ifError(err);
+ });
- BlogPost.create({
- title: 'woot',
- _creator: creator
- }, function(err, post) {
- assert.ifError(err);
-
- BlogPost
- .findById(post._id)
- .populate('_creator', 'email', { name: 'Peanut' })
- .exec(function(err, post) {
- assert.ifError(err);
- assert.strictEqual(post._creator, null);
-
- BlogPost
- .findById(post._id)
- .populate('_creator', 'email', { name: 'Banana' })
- .exec(function(err, post) {
- assert.ifError(err);
- assert.ok(post._creator instanceof User);
- assert.equal(false, post._creator.isInit('name'));
- assert.equal(post._creator.email, 'cats@example.com');
- done();
- });
- });
- });
+ const post = await BlogPost.create({
+ title: 'woot',
+ _creator: creator
});
+
+ const post2 = await BlogPost
+ .findById(post._id)
+ .populate('_creator', 'email', { name: 'Peanut' })
+ .exec();
+
+ assert.strictEqual(post2._creator, null);
+
+ const post3 = await BlogPost
+ .findById(post._id)
+ .populate('_creator', 'email', { name: 'Banana' })
+ .exec();
+
+ assert.ok(post3._creator instanceof User);
+ assert.equal(false, post3._creator.isInit('name'));
+ assert.equal(post3._creator.email, 'cats@example.com');
});
- it('population of undefined fields in a collection of docs', function(done) {
+ it('population of undefined fields in a collection of docs', async function() {
const BlogPost = db.model('BlogPost', blogPostSchema);
const User = db.model('User', userSchema);
- User.create({
+ const user = await User.create({
name: 'Eloy',
email: 'eloytoro@gmail.com'
- }, function(err, user) {
- assert.ifError(err);
-
- BlogPost.create({
- title: 'I have a user ref',
- _creator: user
- }, function(err) {
- assert.ifError(err);
-
- BlogPost.create({
- title: 'I don\'t'
- }, function(err) {
- assert.ifError(err);
- BlogPost
- .find()
- .populate('_creator')
- .exec(function(err, posts) {
- posts.forEach(function(post) {
- if ('_creator' in post) {
- assert.ok(post._creator !== null);
- }
- });
- done();
- });
- });
- });
+ });
+
+ await BlogPost.create({
+ title: 'I have a user ref',
+ _creator: user
+ });
+
+ await BlogPost.create({
+ title: 'I don\'t'
+ });
+
+ const posts = await BlogPost.find().populate('_creator').exec();
+
+ posts.forEach(function(post) {
+ if ('_creator' in post) {
+ assert.ok(post._creator !== null);
+ }
});
});
- it('undefined for nested paths (gh-3859)', function(done) {
+ it('undefined for nested paths (gh-3859)', async function() {
const companySchema = new mongoose.Schema({
name: String,
description: String
@@ -529,545 +451,430 @@ describe('model: populate:', function() {
items: [user1, user2]
});
- company.save(function(error) {
- assert.ifError(error);
- User.create(user1, user2, function(error) {
- assert.ifError(error);
- sample.save(function(error) {
- assert.ifError(error);
- next(sample._id);
- });
- });
- });
+ await company.save();
+ await User.create(user1, user2);
+ await sample.save();
- function next(_id) {
- Sample.findOne({ _id }, function(error, sample) {
- assert.ifError(error);
- const opts = {
- path: 'items.company',
- options: { lean: true },
- model: Company
- };
- Sample.populate(sample, opts, function(error) {
- assert.ifError(error);
- assert.strictEqual(sample.items[1].company, void 0);
- done();
- });
- });
- }
+ const _id = sample._id;
+
+ const sample2 = await Sample.findOne({ _id });
+
+ const opts = {
+ path: 'items.company',
+ options: { lean: true },
+ model: Company
+ };
+
+ await Sample.populate(sample2, opts);
+ assert.strictEqual(sample2.items[1].company, void 0);
});
- it('population and changing a reference', function(done) {
+
+ it('population and changing a reference', async function() {
const BlogPost = db.model('BlogPost', blogPostSchema);
const User = db.model('User', userSchema);
- User.create({
+ const creator = await User.create({
name: 'Guillermo',
email: 'rauchg@gmail.com'
- }, function(err, creator) {
- assert.ifError(err);
+ });
- BlogPost.create({
- title: 'woot',
- _creator: creator
- }, function(err, post) {
- assert.ifError(err);
-
- BlogPost
- .findById(post._id)
- .populate('_creator')
- .exec(function(err, post) {
- assert.ifError(err);
-
- assert.ok(post._creator instanceof User);
- assert.equal(post._creator.name, 'Guillermo');
- assert.equal(post._creator.email, 'rauchg@gmail.com');
-
- User.create({
- name: 'Aaron',
- email: 'aaron.heckmann@gmail.com'
- }, function(err, newCreator) {
- assert.ifError(err);
-
- post._creator = newCreator._id;
- assert.equal(newCreator._id, String(post._creator));
-
- post.save(function(err) {
- assert.ifError(err);
-
- BlogPost
- .findById(post._id)
- .populate('_creator')
- .exec(function(err, post) {
- assert.ifError(err);
- assert.equal(post._creator.name, 'Aaron');
- assert.equal(post._creator.email, 'aaron.heckmann@gmail.com');
- done();
- });
- });
- });
- });
- });
+ const post = await BlogPost.create({
+ title: 'woot',
+ _creator: creator
+ });
+
+ const populatedPost = await BlogPost
+ .findById(post._id)
+ .populate('_creator')
+ .exec();
+
+ assert.ok(populatedPost._creator instanceof User);
+ assert.equal(populatedPost._creator.name, 'Guillermo');
+ assert.equal(populatedPost._creator.email, 'rauchg@gmail.com');
+
+ const newCreator = await User.create({
+ name: 'Aaron',
+ email: 'aaron.heckmann@gmail.com'
});
+
+ post._creator = newCreator._id;
+ assert.equal(newCreator._id, String(post._creator));
+
+ await post.save();
+
+ const populatedPost2 = await BlogPost
+ .findById(post._id)
+ .populate('_creator')
+ .exec();
+
+ assert.equal(populatedPost2._creator.name, 'Aaron');
+ assert.equal(populatedPost2._creator.email, 'aaron.heckmann@gmail.com');
});
- it('populating with partial fields selection and changing ref', function(done) {
+ it('populating with partial fields selection and changing ref', async function() {
const BlogPost = db.model('BlogPost', blogPostSchema);
const User = db.model('User', userSchema);
- User.create({
+ const creator = await User.create({
name: 'Guillermo',
email: 'rauchg@gmail.com'
- }, function(err, creator) {
- assert.ifError(err);
+ });
- BlogPost.create({
- title: 'woot',
- _creator: creator
- }, function(err, post) {
- assert.ifError(err);
-
- BlogPost
- .findById(post._id)
- .populate('_creator', { name: 1 })
- .exec(function(err, post) {
- assert.ifError(err);
-
- assert.ok(post._creator instanceof User);
- assert.equal(post._creator.name, 'Guillermo');
-
- User.create({
- name: 'Aaron',
- email: 'aaron@learnboost.com'
- }, function(err, newCreator) {
- assert.ifError(err);
-
- post._creator = newCreator._id;
- post.save(function(err) {
- assert.ifError(err);
-
- BlogPost
- .findById(post._id)
- .populate('_creator', '-email')
- .exec(function(err, post) {
- assert.ifError(err);
-
- assert.equal(post._creator.name, 'Aaron');
- assert.ok(!post._creator.email);
- done();
- });
- });
- });
- });
- });
+ const post = await BlogPost.create({
+ title: 'woot',
+ _creator: creator
});
- });
- it('populating an array of refs and fetching many', function(done) {
- const BlogPost = db.model('BlogPost', blogPostSchema);
- const User = db.model('User', userSchema);
+ const populatedPost = await BlogPost
+ .findById(post._id)
+ .populate('_creator', { name: 1 })
+ .exec();
- User.create({
- name: 'Fan 1',
- email: 'fan1@learnboost.com'
- }, function(err, fan1) {
- assert.ifError(err);
+ assert.ok(populatedPost._creator instanceof User);
+ assert.equal(populatedPost._creator.name, 'Guillermo');
- User.create({
- name: 'Fan 2',
- email: 'fan2@learnboost.com'
- }, function(err, fan2) {
- assert.ifError(err);
-
- BlogPost.create({
- title: 'Woot',
- fans: [fan1, fan2]
- }, function(err, post1) {
- assert.ifError(err);
-
- BlogPost.create({
- title: 'Woot',
- fans: [fan2, fan1]
- }, function(err, post2) {
- assert.ifError(err);
-
- BlogPost
- .find({ _id: { $in: [post1._id, post2._id] } })
- .populate('fans')
- .exec(function(err, blogposts) {
- assert.ifError(err);
-
- assert.equal(blogposts[0].fans[0].name, 'Fan 1');
- assert.equal(blogposts[0].fans[0].email, 'fan1@learnboost.com');
- assert.equal(blogposts[0].fans[1].name, 'Fan 2');
- assert.equal(blogposts[0].fans[1].email, 'fan2@learnboost.com');
-
- assert.equal(blogposts[1].fans[0].name, 'Fan 2');
- assert.equal(blogposts[1].fans[0].email, 'fan2@learnboost.com');
- assert.equal(blogposts[1].fans[1].name, 'Fan 1');
- assert.equal(blogposts[1].fans[1].email, 'fan1@learnboost.com');
- done();
- });
- });
- });
- });
+ const newCreator = await User.create({
+ name: 'Aaron',
+ email: 'aaron@learnboost.com'
});
+
+ post._creator = newCreator._id;
+ await post.save();
+
+ const populatedPost2 = await BlogPost
+ .findById(post._id)
+ .populate('_creator', '-email')
+ .exec();
+
+ assert.equal(populatedPost2._creator.name, 'Aaron');
+ assert.ok(!populatedPost2._creator.email);
});
- it('an error in array reference population propagates', function(done) {
+ it('populating an array of refs and fetching many', async function() {
const BlogPost = db.model('BlogPost', blogPostSchema);
const User = db.model('User', userSchema);
- User.create({
+ const fan1 = await User.create({
name: 'Fan 1',
email: 'fan1@learnboost.com'
- }, function(err, fan1) {
- assert.ifError(err);
+ });
- User.create({
- name: 'Fan 2',
- email: 'fan2@learnboost.com'
- }, function(err, fan2) {
- assert.ifError(err);
-
- BlogPost.create({
- title: 'Woot',
- fans: [fan1, fan2]
- }, function(err, post1) {
- assert.ifError(err);
-
- BlogPost.create({
- title: 'Woot',
- fans: [fan2, fan1]
- }, function(err, post2) {
- assert.ifError(err);
-
- // mock an error
- const origExec = User.Query.prototype.exec;
- User.Query.prototype.exec = function() {
- const args = Array.prototype.map.call(arguments, function(arg) {
- return typeof arg === 'function' ? function() {
- arg(new Error('woot 2'));
- } : arg;
- });
- return origExec.apply(this, args);
- };
+ const fan2 = await User.create({
+ name: 'Fan 2',
+ email: 'fan2@learnboost.com'
+ });
- BlogPost
- .find({ $or: [{ _id: post1._id }, { _id: post2._id }] })
- .populate('fans')
- .exec(function(err) {
- assert.ok(err instanceof Error);
- assert.equal(err.message, 'woot 2');
- User.Query.prototype.exec = origExec;
- done();
- });
- });
- });
- });
+ const post1 = await BlogPost.create({
+ title: 'Woot',
+ fans: [fan1, fan2]
+ });
+
+ const post2 = await BlogPost.create({
+ title: 'Woot',
+ fans: [fan2, fan1]
});
+
+ const blogposts = await BlogPost.find({ _id: { $in: [post1._id, post2._id] } }).populate('fans');
+
+ assert.equal(blogposts[0].fans[0].name, 'Fan 1');
+ assert.equal(blogposts[0].fans[0].email, 'fan1@learnboost.com');
+ assert.equal(blogposts[0].fans[1].name, 'Fan 2');
+ assert.equal(blogposts[0].fans[1].email, 'fan2@learnboost.com');
+
+ assert.equal(blogposts[1].fans[0].name, 'Fan 2');
+ assert.equal(blogposts[1].fans[0].email, 'fan2@learnboost.com');
+ assert.equal(blogposts[1].fans[1].name, 'Fan 1');
+ assert.equal(blogposts[1].fans[1].email, 'fan1@learnboost.com');
});
- it('populating an array of references with fields selection', function(done) {
+ it('an error in array reference population propagates', async function() {
const BlogPost = db.model('BlogPost', blogPostSchema);
const User = db.model('User', userSchema);
- User.create({
+ const fan1 = await User.create({
name: 'Fan 1',
email: 'fan1@learnboost.com'
- }, function(err, fan1) {
- assert.ifError(err);
+ });
- User.create({
- name: 'Fan 2',
- email: 'fan2@learnboost.com'
- }, function(err, fan2) {
- assert.ifError(err);
-
- BlogPost.create({
- title: 'Woot',
- fans: [fan1, fan2]
- }, function(err, post1) {
- assert.ifError(err);
-
- BlogPost.create({
- title: 'Woot',
- fans: [fan2, fan1]
- }, function(err, post2) {
- assert.ifError(err);
-
- BlogPost
- .find({ _id: { $in: [post1._id, post2._id] } })
- .populate('fans', 'name')
- .exec(function(err, blogposts) {
- assert.ifError(err);
-
- assert.equal(blogposts[0].fans[0].name, 'Fan 1');
- assert.equal(blogposts[0].fans[0].isInit('email'), false);
- assert.equal(blogposts[0].fans[1].name, 'Fan 2');
- assert.equal(blogposts[0].fans[1].isInit('email'), false);
- assert.strictEqual(blogposts[0].fans[1].email, undefined);
-
- assert.equal(blogposts[1].fans[0].name, 'Fan 2');
- assert.equal(blogposts[1].fans[0].isInit('email'), false);
- assert.equal(blogposts[1].fans[1].name, 'Fan 1');
- assert.equal(blogposts[1].fans[1].isInit('email'), false);
-
- done();
- });
- });
- });
- });
+ const fan2 = await User.create({
+ name: 'Fan 2',
+ email: 'fan2@learnboost.com'
});
+
+ const post1 = await BlogPost.create({
+ title: 'Woot',
+ fans: [fan1, fan2]
+ });
+
+ const post2 = await BlogPost.create({
+ title: 'Woot',
+ fans: [fan2, fan1]
+ });
+
+ // mock an error
+ const origExec = User.Query.prototype.exec;
+ User.Query.prototype.exec = function() {
+ throw new Error('woot 2');
+ };
+
+ try {
+ await BlogPost
+ .find({ $or: [{ _id: post1._id }, { _id: post2._id }] })
+ .populate('fans')
+ .exec();
+
+ throw new Error('should not get here');
+ } catch (err) {
+ assert.ok(err instanceof Error);
+ assert.equal(err.message, 'woot 2');
+ User.Query.prototype.exec = origExec;
+ }
});
- it('populating an array of references and filtering', function(done) {
+ it('populating an array of references with fields selection', async function() {
const BlogPost = db.model('BlogPost', blogPostSchema);
const User = db.model('User', userSchema);
- User.create({
+ const fan1 = await User.create({
name: 'Fan 1',
email: 'fan1@learnboost.com'
- }, function(err, fan1) {
- assert.ifError(err);
+ });
- User.create({
- name: 'Fan 2',
- email: 'fan2@learnboost.com',
- gender: 'female'
- }, function(err, fan2) {
- assert.ifError(err);
-
- User.create({
- name: 'Fan 3',
- email: 'fan3@learnboost.com',
- gender: 'female'
- }, function(err, fan3) {
- assert.ifError(err);
-
- BlogPost.create({
- title: 'Woot',
- fans: [fan1, fan2, fan3]
- }, function(err, post1) {
- assert.ifError(err);
-
- BlogPost.create({
- title: 'Woot',
- fans: [fan3, fan2, fan1]
- }, function(err, post2) {
- assert.ifError(err);
-
- BlogPost
- .find({ _id: { $in: [post1._id, post2._id] } })
- .populate('fans', '', { gender: 'female', _id: { $in: [fan2] } })
- .exec(function(err, blogposts) {
- assert.ifError(err);
-
- assert.equal(blogposts[0].fans.length, 1);
- assert.equal(blogposts[0].fans[0].gender, 'female');
- assert.equal(blogposts[0].fans[0].name, 'Fan 2');
- assert.equal(blogposts[0].fans[0].email, 'fan2@learnboost.com');
-
- assert.equal(blogposts[1].fans.length, 1);
- assert.equal(blogposts[1].fans[0].gender, 'female');
- assert.equal(blogposts[1].fans[0].name, 'Fan 2');
- assert.equal(blogposts[1].fans[0].email, 'fan2@learnboost.com');
-
- BlogPost
- .find({ _id: { $in: [post1._id, post2._id] } })
- .populate('fans', false, { gender: 'female' })
- .exec(function(err, blogposts) {
- assert.ifError(err);
-
- assert.strictEqual(blogposts[0].fans.length, 2);
- assert.equal(blogposts[0].fans[0].gender, 'female');
- assert.equal(blogposts[0].fans[0].name, 'Fan 2');
- assert.equal(blogposts[0].fans[0].email, 'fan2@learnboost.com');
- assert.equal(blogposts[0].fans[1].gender, 'female');
- assert.equal(blogposts[0].fans[1].name, 'Fan 3');
- assert.equal(blogposts[0].fans[1].email, 'fan3@learnboost.com');
-
- assert.strictEqual(blogposts[1].fans.length, 2);
- assert.equal(blogposts[1].fans[0].gender, 'female');
- assert.equal(blogposts[1].fans[0].name, 'Fan 3');
- assert.equal(blogposts[1].fans[0].email, 'fan3@learnboost.com');
- assert.equal(blogposts[1].fans[1].gender, 'female');
- assert.equal(blogposts[1].fans[1].name, 'Fan 2');
- assert.equal(blogposts[1].fans[1].email, 'fan2@learnboost.com');
-
- done();
- });
- });
- });
- });
- });
- });
+ const fan2 = await User.create({
+ name: 'Fan 2',
+ email: 'fan2@learnboost.com'
+ });
+
+ const post1 = await BlogPost.create({
+ title: 'Woot',
+ fans: [fan1, fan2]
+ });
+
+ const post2 = await BlogPost.create({
+ title: 'Woot',
+ fans: [fan2, fan1]
});
+
+ const blogposts = await BlogPost.find({ _id: { $in: [post1._id, post2._id] } })
+ .populate('fans', 'name');
+
+ assert.equal(blogposts[0].fans[0].name, 'Fan 1');
+ assert.equal(blogposts[0].fans[0].isInit('email'), false);
+ assert.equal(blogposts[0].fans[1].name, 'Fan 2');
+ assert.equal(blogposts[0].fans[1].isInit('email'), false);
+ assert.strictEqual(blogposts[0].fans[1].email, undefined);
+
+ assert.equal(blogposts[1].fans[0].name, 'Fan 2');
+ assert.equal(blogposts[1].fans[0].isInit('email'), false);
+ assert.equal(blogposts[1].fans[1].name, 'Fan 1');
+ assert.equal(blogposts[1].fans[1].isInit('email'), false);
});
- it('populating an array of references and multi-filtering', function(done) {
+ it('populating an array of references and filtering', async function() {
const BlogPost = db.model('BlogPost', blogPostSchema);
const User = db.model('User', userSchema);
- User.create({
+ const fan1 = await User.create({
name: 'Fan 1',
email: 'fan1@learnboost.com'
- }, function(err, fan1) {
- assert.ifError(err);
+ });
- User.create({
- name: 'Fan 2',
- email: 'fan2@learnboost.com',
- gender: 'female'
- }, function(err, fan2) {
- assert.ifError(err);
-
- User.create({
- name: 'Fan 3',
- email: 'fan3@learnboost.com',
- gender: 'female',
- age: 25
- }, function(err, fan3) {
- assert.ifError(err);
-
- BlogPost.create({
- title: 'Woot',
- fans: [fan1, fan2, fan3]
- }, function(err, post1) {
- assert.ifError(err);
-
- BlogPost.create({
- title: 'Woot',
- fans: [fan3, fan2, fan1]
- }, function(err, post2) {
- assert.ifError(err);
-
- BlogPost
- .find({ _id: { $in: [post1._id, post2._id] } })
- .populate('fans', undefined, { _id: fan3 })
- .exec(function(err, blogposts) {
- assert.ifError(err);
-
- assert.equal(blogposts[0].fans.length, 1);
- assert.equal(blogposts[0].fans[0].gender, 'female');
- assert.equal(blogposts[0].fans[0].name, 'Fan 3');
- assert.equal(blogposts[0].fans[0].email, 'fan3@learnboost.com');
- assert.equal(blogposts[0].fans[0].age, 25);
-
- assert.equal(blogposts[1].fans.length, 1);
- assert.equal(blogposts[1].fans[0].gender, 'female');
- assert.equal(blogposts[1].fans[0].name, 'Fan 3');
- assert.equal(blogposts[1].fans[0].email, 'fan3@learnboost.com');
- assert.equal(blogposts[1].fans[0].age, 25);
-
- BlogPost
- .find({ _id: { $in: [post1._id, post2._id] } })
- .populate('fans', 0, { gender: 'female' })
- .exec(function(err, blogposts) {
- assert.ifError(err);
-
- assert.equal(blogposts[0].fans.length, 2);
- assert.equal(blogposts[0].fans[0].gender, 'female');
- assert.equal(blogposts[0].fans[0].name, 'Fan 2');
- assert.equal(blogposts[0].fans[0].email, 'fan2@learnboost.com');
- assert.equal(blogposts[0].fans[1].gender, 'female');
- assert.equal(blogposts[0].fans[1].name, 'Fan 3');
- assert.equal(blogposts[0].fans[1].email, 'fan3@learnboost.com');
- assert.equal(blogposts[0].fans[1].age, 25);
-
- assert.equal(blogposts[1].fans.length, 2);
- assert.equal(blogposts[1].fans[0].gender, 'female');
- assert.equal(blogposts[1].fans[0].name, 'Fan 3');
- assert.equal(blogposts[1].fans[0].email, 'fan3@learnboost.com');
- assert.equal(blogposts[1].fans[0].age, 25);
- assert.equal(blogposts[1].fans[1].gender, 'female');
- assert.equal(blogposts[1].fans[1].name, 'Fan 2');
- assert.equal(blogposts[1].fans[1].email, 'fan2@learnboost.com');
-
- done();
- });
- });
- });
- });
- });
- });
+ const fan2 = await User.create({
+ name: 'Fan 2',
+ email: 'fan2@learnboost.com',
+ gender: 'female'
+ });
+
+ const fan3 = await User.create({
+ name: 'Fan 3',
+ email: 'fan3@learnboost.com',
+ gender: 'female'
});
+
+ const post1 = await BlogPost.create({
+ title: 'Woot',
+ fans: [fan1, fan2, fan3]
+ });
+
+ const post2 = await BlogPost.create({
+ title: 'Woot',
+ fans: [fan3, fan2, fan1]
+ });
+
+ const blogposts = await BlogPost
+ .find({ _id: { $in: [post1._id, post2._id] } })
+ .populate('fans', '', { gender: 'female', _id: { $in: [fan2] } })
+ .exec();
+
+ assert.equal(blogposts[0].fans.length, 1);
+ assert.equal(blogposts[0].fans[0].gender, 'female');
+ assert.equal(blogposts[0].fans[0].name, 'Fan 2');
+ assert.equal(blogposts[0].fans[0].email, 'fan2@learnboost.com');
+
+ assert.equal(blogposts[1].fans.length, 1);
+ assert.equal(blogposts[1].fans[0].gender, 'female');
+ assert.equal(blogposts[1].fans[0].name, 'Fan 2');
+ assert.equal(blogposts[1].fans[0].email, 'fan2@learnboost.com');
+
+ const blogposts2 = await BlogPost
+ .find({ _id: { $in: [post1._id, post2._id] } })
+ .populate('fans', false, { gender: 'female' })
+ .exec();
+
+ assert.strictEqual(blogposts2[0].fans.length, 2);
+ assert.equal(blogposts2[0].fans[0].gender, 'female');
+ assert.equal(blogposts2[0].fans[0].name, 'Fan 2');
+ assert.equal(blogposts2[0].fans[0].email, 'fan2@learnboost.com');
+ assert.equal(blogposts2[0].fans[1].gender, 'female');
+ assert.equal(blogposts2[0].fans[1].name, 'Fan 3');
+ assert.equal(blogposts2[0].fans[1].email, 'fan3@learnboost.com');
+
+ assert.strictEqual(blogposts2[1].fans.length, 2);
+ assert.equal(blogposts2[1].fans[0].gender, 'female');
+ assert.equal(blogposts2[1].fans[0].name, 'Fan 3');
+ assert.equal(blogposts2[1].fans[0].email, 'fan3@learnboost.com');
+ assert.equal(blogposts2[1].fans[1].gender, 'female');
+ assert.equal(blogposts2[1].fans[1].name, 'Fan 2');
+ assert.equal(blogposts2[1].fans[1].email, 'fan2@learnboost.com');
+
});
- it('populating an array of references and multi-filtering with field selection', function(done) {
+ it('populating an array of references and multi-filtering', async function() {
const BlogPost = db.model('BlogPost', blogPostSchema);
const User = db.model('User', userSchema);
- User.create({
+ const fan1 = await User.create({
name: 'Fan 1',
email: 'fan1@learnboost.com'
- }, function(err, fan1) {
- assert.ifError(err);
+ });
- User.create({
- name: 'Fan 2',
- email: 'fan2@learnboost.com',
- gender: 'female'
- }, function(err, fan2) {
- assert.ifError(err);
-
- User.create({
- name: 'Fan 3',
- email: 'fan3@learnboost.com',
- gender: 'female',
- age: 25
- }, function(err, fan3) {
- assert.ifError(err);
-
- BlogPost.create({
- title: 'Woot',
- fans: [fan1, fan2, fan3]
- }, function(err, post1) {
- assert.ifError(err);
-
- BlogPost.create({
- title: 'Woot',
- fans: [fan3, fan2, fan1]
- }, function(err, post2) {
- assert.ifError(err);
-
- BlogPost
- .find({ _id: { $in: [post1._id, post2._id] } })
- .populate('fans', 'name email', { gender: 'female', age: 25 })
- .exec(function(err, blogposts) {
- assert.ifError(err);
-
- assert.strictEqual(blogposts[0].fans.length, 1);
- assert.equal(blogposts[0].fans[0].name, 'Fan 3');
- assert.equal(blogposts[0].fans[0].email, 'fan3@learnboost.com');
- assert.equal(blogposts[0].fans[0].isInit('email'), true);
- assert.equal(blogposts[0].fans[0].isInit(['email']), true);
- assert.equal(blogposts[0].fans[0].isInit('gender'), false);
- assert.equal(blogposts[0].fans[0].isInit('age'), false);
- assert.equal(blogposts[0].fans[0].isInit(['email', 'age']), true);
- assert.equal(blogposts[0].fans[0].isInit(['gender', 'age']), false);
-
- assert.strictEqual(blogposts[1].fans.length, 1);
- assert.equal(blogposts[1].fans[0].name, 'Fan 3');
- assert.equal(blogposts[1].fans[0].email, 'fan3@learnboost.com');
- assert.equal(blogposts[1].fans[0].isInit('email'), true);
- assert.equal(blogposts[1].fans[0].isInit('gender'), false);
- assert.equal(blogposts[1].fans[0].isInit('age'), false);
-
- done();
- });
- });
- });
- });
- });
+ const fan2 = await User.create({
+ name: 'Fan 2',
+ email: 'fan2@learnboost.com',
+ gender: 'female'
+ });
+
+ const fan3 = await User.create({
+ name: 'Fan 3',
+ email: 'fan3@learnboost.com',
+ gender: 'female',
+ age: 25
+ });
+
+ const post1 = await BlogPost.create({
+ title: 'Woot',
+ fans: [fan1, fan2, fan3]
+ });
+
+ const post2 = await BlogPost.create({
+ title: 'Woot',
+ fans: [fan3, fan2, fan1]
+ });
+
+ const blogposts = await BlogPost
+ .find({ _id: { $in: [post1._id, post2._id] } })
+ .populate('fans', undefined, { _id: fan3 })
+ .exec();
+
+ assert.equal(blogposts[0].fans.length, 1);
+ assert.equal(blogposts[0].fans[0].gender, 'female');
+ assert.equal(blogposts[0].fans[0].name, 'Fan 3');
+ assert.equal(blogposts[0].fans[0].email, 'fan3@learnboost.com');
+ assert.equal(blogposts[0].fans[0].age, 25);
+
+ assert.equal(blogposts[1].fans.length, 1);
+ assert.equal(blogposts[1].fans[0].gender, 'female');
+ assert.equal(blogposts[1].fans[0].name, 'Fan 3');
+ assert.equal(blogposts[1].fans[0].email, 'fan3@learnboost.com');
+ assert.equal(blogposts[1].fans[0].age, 25);
+
+ const blogposts2 = await BlogPost
+ .find({ _id: { $in: [post1._id, post2._id] } })
+ .populate('fans', 0, { gender: 'female' })
+ .exec();
+
+ assert.equal(blogposts2[0].fans.length, 2);
+ assert.equal(blogposts2[0].fans[0].gender, 'female');
+ assert.equal(blogposts2[0].fans[0].name, 'Fan 2');
+ assert.equal(blogposts2[0].fans[0].email, 'fan2@learnboost.com');
+ assert.equal(blogposts2[0].fans[1].gender, 'female');
+ assert.equal(blogposts2[0].fans[1].name, 'Fan 3');
+ assert.equal(blogposts2[0].fans[1].email, 'fan3@learnboost.com');
+ assert.equal(blogposts2[0].fans[1].age, 25);
+
+ assert.equal(blogposts2[1].fans.length, 2);
+ assert.equal(blogposts2[1].fans[0].gender, 'female');
+ assert.equal(blogposts2[1].fans[0].name, 'Fan 3');
+ assert.equal(blogposts2[1].fans[0].email, 'fan3@learnboost.com');
+ assert.equal(blogposts2[1].fans[0].age, 25);
+ assert.equal(blogposts2[1].fans[1].age, 21);
+ assert.equal(blogposts2[1].fans[1].gender, 'female');
+ assert.equal(blogposts2[1].fans[1].name, 'Fan 2');
+ assert.equal(blogposts2[1].fans[1].email, 'fan2@learnboost.com');
+ });
+
+ it('populating an array of references and multi-filtering with field selection', async function() {
+ const BlogPost = db.model('BlogPost', blogPostSchema);
+ const User = db.model('User', userSchema);
+
+ const fan1 = await User.create({
+ name: 'Fan 1',
+ email: 'fan1@learnboost.com'
+ });
+
+ const fan2 = await User.create({
+ name: 'Fan 2',
+ email: 'fan2@learnboost.com',
+ gender: 'female'
+ });
+
+ const fan3 = await User.create({
+ name: 'Fan 3',
+ email: 'fan3@learnboost.com',
+ gender: 'female',
+ age: 25
+ });
+
+ const post1 = await BlogPost.create({
+ title: 'Woot',
+ fans: [fan1, fan2, fan3]
});
+
+ const post2 = await BlogPost.create({
+ title: 'Woot',
+ fans: [fan3, fan2, fan1]
+ });
+
+ const blogposts = await BlogPost
+ .find({ _id: { $in: [post1._id, post2._id] } })
+ .populate('fans', 'name email', { gender: 'female', age: 25 })
+ .exec();
+
+ assert.strictEqual(blogposts[0].fans.length, 1);
+ assert.equal(blogposts[0].fans[0].name, 'Fan 3');
+ assert.equal(blogposts[0].fans[0].email, 'fan3@learnboost.com');
+ assert.equal(blogposts[0].fans[0].isInit('email'), true);
+ assert.equal(blogposts[0].fans[0].isInit(['email']), true);
+ assert.equal(blogposts[0].fans[0].isInit('gender'), false);
+ assert.equal(blogposts[0].fans[0].isInit('age'), false);
+ assert.equal(blogposts[0].fans[0].isInit(['email', 'age']), true);
+ assert.equal(blogposts[0].fans[0].isInit(['gender', 'age']), false);
+
+ assert.strictEqual(blogposts[1].fans.length, 1);
+ assert.equal(blogposts[1].fans[0].name, 'Fan 3');
+ assert.equal(blogposts[1].fans[0].email, 'fan3@learnboost.com');
+ assert.equal(blogposts[1].fans[0].isInit('email'), true);
+ assert.equal(blogposts[1].fans[0].isInit('gender'), false);
+ assert.equal(blogposts[1].fans[0].isInit('age'), false);
});
- it('populating an array of refs changing one and removing one', function(done) {
+
+ it('populating an array of refs changing one and removing one', async function() {
const BlogPost = db.model('BlogPost', blogPostSchema);
const User = db.model('User', userSchema);
- User.create({
+ const [fan1, fan2, fan3, fan4] = await User.create([{
name: 'Fan 1',
email: 'fan1@learnboost.com'
}, {
@@ -1079,361 +886,260 @@ describe('model: populate:', function() {
}, {
name: 'Fan 4',
email: 'fan4@learnboost.com'
- }, function(err, fan1, fan2, fan3, fan4) {
- assert.ifError(err);
+ }]);
- BlogPost.create({
- title: 'Woot',
- fans: [fan1, fan2]
- }, {
- title: 'Woot',
- fans: [fan2, fan1]
- }, function(err, post1, post2) {
- assert.ifError(err);
-
- BlogPost
- .find({ _id: { $in: [post1._id, post2._id] } })
- .populate('fans', 'name')
- .exec(function(err, blogposts) {
- assert.ifError(err);
-
- assert.equal(blogposts[0].fans[0].name, 'Fan 1');
- assert.equal(blogposts[0].fans[0].isInit('email'), false);
- assert.equal(blogposts[0].fans[1].name, 'Fan 2');
- assert.equal(blogposts[0].fans[1].isInit('email'), false);
-
- assert.equal(blogposts[1].fans[0].name, 'Fan 2');
- assert.equal(blogposts[1].fans[0].isInit('email'), false);
- assert.equal(blogposts[1].fans[1].name, 'Fan 1');
- assert.equal(blogposts[1].fans[1].isInit('email'), false);
-
- blogposts[1].fans = [fan3, fan4];
-
- blogposts[1].save(function(err) {
- assert.ifError(err);
-
- BlogPost
- .findById(blogposts[1]._id, '', { populate: ['fans'] })
- .exec(function(err, post) {
- assert.ifError(err);
-
- assert.equal(post.fans[0].name, 'Fan 3');
- assert.equal(post.fans[1].name, 'Fan 4');
-
- post.fans.splice(0, 1);
- post.save(function(err) {
- assert.ifError(err);
-
- BlogPost
- .findById(post._id)
- .populate('fans')
- .exec(function(err, post) {
- assert.ifError(err);
- assert.equal(post.fans.length, 1);
- assert.equal(post.fans[0].name, 'Fan 4');
- done();
- });
- });
- });
- });
- });
- });
- });
+ const [post1, post2] = await BlogPost.create([{
+ title: 'Woot',
+ fans: [fan1, fan2]
+ }, {
+ title: 'Woot',
+ fans: [fan2, fan1]
+ }]);
+
+ const blogposts = await BlogPost.find({ _id: { $in: [post1._id, post2._id] } }).populate('fans', 'name');
+
+ assert.equal(blogposts[0].fans[0].name, 'Fan 1');
+ assert.equal(blogposts[0].fans[0].isInit('email'), false);
+ assert.equal(blogposts[0].fans[1].name, 'Fan 2');
+ assert.equal(blogposts[0].fans[1].isInit('email'), false);
+
+ assert.equal(blogposts[1].fans[0].name, 'Fan 2');
+ assert.equal(blogposts[1].fans[0].isInit('email'), false);
+ assert.equal(blogposts[1].fans[1].name, 'Fan 1');
+ assert.equal(blogposts[1].fans[1].isInit('email'), false);
+
+ blogposts[1].fans = [fan3, fan4];
+
+ await blogposts[1].save();
+
+ const post = await BlogPost.findById(blogposts[1]._id, '', { populate: ['fans'] });
+
+ assert.equal(post.fans[0].name, 'Fan 3');
+ assert.equal(post.fans[1].name, 'Fan 4');
+
+ post.fans.splice(0, 1);
+
+ await post.save();
+
+ const postFinal = await BlogPost.findById(post._id).populate('fans');
+
+ assert.equal(postFinal.fans.length, 1);
+ assert.equal(postFinal.fans[0].name, 'Fan 4');
});
describe('populating sub docs', function() {
- it('works with findById', function(done) {
+ it('works with findById', async function() {
const BlogPost = db.model('BlogPost', blogPostSchema);
const User = db.model('User', userSchema);
- User.create({ name: 'User 1' }, function(err, user1) {
- assert.ifError(err);
+ const user1 = await User.create({ name: 'User 1' });
+ const user2 = await User.create({ name: 'User 2' });
- User.create({ name: 'User 2' }, function(err, user2) {
- assert.ifError(err);
-
- BlogPost.create({
- title: 'Woot',
- _creator: user1._id,
- comments: [
- { _creator: user1._id, content: 'Woot woot' },
- { _creator: user2._id, content: 'Wha wha' }
- ]
- }, function(err, post) {
- assert.ifError(err);
-
- BlogPost
- .findById(post._id)
- .populate('_creator')
- .populate('comments._creator')
- .exec(function(err, post) {
- assert.ifError(err);
-
- assert.equal(post._creator.name, 'User 1');
- assert.equal(post.comments[0]._creator.name, 'User 1');
- assert.equal(post.comments[1]._creator.name, 'User 2');
- done();
- });
- });
- });
+ const post = await BlogPost.create({
+ title: 'Woot',
+ _creator: user1._id,
+ comments: [
+ { _creator: user1._id, content: 'Woot woot' },
+ { _creator: user2._id, content: 'Wha wha' }
+ ]
});
+
+ const foundPost = await BlogPost
+ .findById(post._id)
+ .populate('_creator')
+ .populate('comments._creator');
+
+ assert.equal(foundPost._creator.name, 'User 1');
+ assert.equal(foundPost.comments[0]._creator.name, 'User 1');
+ assert.equal(foundPost.comments[1]._creator.name, 'User 2');
});
- it('works when first doc returned has empty array for populated path (gh-1055)', function(done) {
+ it('works when first doc returned has empty array for populated path (gh-1055)', async function() {
const BlogPost = db.model('BlogPost', blogPostSchema);
const User = db.model('User', userSchema);
- User.create({ name: 'gh-1055-1' }, { name: 'gh-1055-2' }, function(err, user1, user2) {
- assert.ifError(err);
+ const [user1, user2] = await User.create({ name: 'gh-1055-1' }, { name: 'gh-1055-2' });
- BlogPost.create({
- title: 'gh-1055 post1',
- _creator: user1._id,
- comments: []
- }, {
- title: 'gh-1055 post2',
- _creator: user1._id,
- comments: [
- { _creator: user1._id, content: 'Woot woot', asers: [] },
- { _creator: user2._id, content: 'Wha wha', asers: [user1, user2] }
- ]
- }, function(err) {
- assert.ifError(err);
-
- let ran = false;
- BlogPost
- .find({ title: /gh-1055/ })
- .sort('title')
- .select('comments')
- .populate('comments._creator')
- .populate('comments.asers')
- .exec(function(err, posts) {
- assert.equal(ran, false);
- ran = true;
- assert.ifError(err);
- assert.ok(posts.length);
- assert.ok(posts[1].comments[0]._creator);
- assert.equal(posts[1].comments[0]._creator.name, 'gh-1055-1');
- done();
- });
- });
+ await BlogPost.create({
+ title: 'gh-1055 post1',
+ _creator: user1._id,
+ comments: []
+ }, {
+ title: 'gh-1055 post2',
+ _creator: user1._id,
+ comments: [
+ { _creator: user1._id, content: 'Woot woot', asers: [] },
+ { _creator: user2._id, content: 'Wha wha', asers: [user1, user2] }
+ ]
});
+
+ const posts = await BlogPost
+ .find({ title: /gh-1055/ })
+ .sort('title')
+ .select('comments')
+ .populate('comments._creator')
+ .populate('comments.asers');
+
+ assert.ok(posts.length);
+ assert.ok(posts[1].comments[0]._creator);
+ assert.equal(posts[1].comments[0]._creator.name, 'gh-1055-1');
});
});
- it('clears cache when array has been re-assigned (gh-2176)', function(done) {
+ it('clears cache when array has been re-assigned (gh-2176)', async function() {
const BlogPost = db.model('BlogPost', blogPostSchema);
const User = db.model('User', userSchema);
- User.create({ name: 'aaron' }, { name: 'val' }, function(err, user1, user2) {
- assert.ifError(err);
+ const [user1, user2] = await User.create({ name: 'aaron' }, { name: 'val' });
- BlogPost.create(
- {
- title: 'gh-2176',
- _creator: user1._id,
- comments: []
- },
- function(err) {
- assert.ifError(err);
- BlogPost.
- find({ title: 'gh-2176' }).
- populate('_creator').
- exec(function(error, posts) {
- assert.ifError(error);
- assert.equal(posts.length, 1);
- assert.equal(posts[0]._creator.name, 'aaron');
- posts[0]._creator = user2;
- assert.equal(posts[0]._creator.name, 'val');
- posts[0].save(function(error, post) {
- assert.ifError(error);
- assert.equal(post._creator.name, 'val');
- posts[0].populate('_creator', function(error, doc) {
- assert.ifError(error);
- assert.equal(doc._creator.name, 'val');
- done();
- });
- });
- });
- });
+ await BlogPost.create({
+ title: 'gh-2176',
+ _creator: user1._id,
+ comments: []
});
+
+ const posts = await BlogPost.find({ title: 'gh-2176' }).populate('_creator');
+
+ assert.equal(posts.length, 1);
+ assert.equal(posts[0]._creator.name, 'aaron');
+
+ posts[0]._creator = user2;
+ assert.equal(posts[0]._creator.name, 'val');
+
+ const post = await posts[0].save();
+ assert.equal(post._creator.name, 'val');
+
+ const doc = await posts[0].populate('_creator');
+ assert.equal(doc._creator.name, 'val');
});
- it('populating subdocuments partially', function(done) {
+ it('populating subdocuments partially', async function() {
const BlogPost = db.model('BlogPost', blogPostSchema);
const User = db.model('User', userSchema);
- User.create({
+ const user1 = await User.create({
name: 'User 1',
email: 'user1@learnboost.com'
- }, function(err, user1) {
- assert.ifError(err);
-
- User.create({
- name: 'User 2',
- email: 'user2@learnboost.com'
- }, function(err, user2) {
- assert.ifError(err);
-
- BlogPost.create({
- title: 'Woot',
- comments: [
- { _creator: user1, content: 'Woot woot' },
- { _creator: user2, content: 'Wha wha' }
- ]
- }, function(err, post) {
- assert.ifError(err);
-
- BlogPost
- .findById(post._id)
- .populate('comments._creator', 'email')
- .exec(function(err, post) {
- assert.ifError(err);
+ });
- assert.equal(post.comments[0]._creator.email, 'user1@learnboost.com');
- assert.equal(post.comments[0]._creator.isInit('name'), false);
- assert.equal(post.comments[1]._creator.email, 'user2@learnboost.com');
- assert.equal(post.comments[1]._creator.isInit('name'), false);
+ const user2 = await User.create({
+ name: 'User 2',
+ email: 'user2@learnboost.com'
+ });
- done();
- });
- });
- });
+ const post = await BlogPost.create({
+ title: 'Woot',
+ comments: [
+ { _creator: user1, content: 'Woot woot' },
+ { _creator: user2, content: 'Wha wha' }
+ ]
});
+
+ const post2 = await BlogPost.findById(post._id).populate('comments._creator', 'email');
+
+ assert.equal(post2.comments[0]._creator.email, 'user1@learnboost.com');
+ assert.equal(post2.comments[0]._creator.isInit('name'), false);
+ assert.equal(post2.comments[1]._creator.email, 'user2@learnboost.com');
+ assert.equal(post2.comments[1]._creator.isInit('name'), false);
});
- it('populating subdocuments partially with conditions', function(done) {
+ it('populating subdocuments partially with conditions', async function() {
const BlogPost = db.model('BlogPost', blogPostSchema);
const User = db.model('User', userSchema);
- User.create({
+ const user1 = await User.create({
name: 'User 1',
email: 'user1@learnboost.com'
- }, function(err, user1) {
- assert.ifError(err);
-
- User.create({
- name: 'User 2',
- email: 'user2@learnboost.com'
- }, function(err, user2) {
- assert.ifError(err);
-
- BlogPost.create({
- title: 'Woot',
- comments: [
- { _creator: user1, content: 'Woot woot' },
- { _creator: user2, content: 'Wha wha' }
- ]
- }, function(err, post) {
- assert.ifError(err);
-
- BlogPost
- .findById(post._id)
- .populate('comments._creator', { email: 1 }, { name: /User/ })
- .exec(function(err, post) {
- assert.ifError(err);
+ });
- assert.equal(post.comments[0]._creator.email, 'user1@learnboost.com');
- assert.equal(post.comments[0]._creator.isInit('name'), false);
- assert.equal(post.comments[1]._creator.email, 'user2@learnboost.com');
- assert.equal(post.comments[1]._creator.isInit('name'), false);
+ const user2 = await User.create({
+ name: 'User 2',
+ email: 'user2@learnboost.com'
+ });
- done();
- });
- });
- });
+ const post = await BlogPost.create({
+ title: 'Woot',
+ comments: [
+ { _creator: user1, content: 'Woot woot' },
+ { _creator: user2, content: 'Wha wha' }
+ ]
});
+
+ const populatedPost = await BlogPost
+ .findById(post._id)
+ .populate('comments._creator', { email: 1 }, { name: /User/ })
+ .exec();
+
+ assert.equal(populatedPost.comments[0]._creator.email, 'user1@learnboost.com');
+ assert.equal(populatedPost.comments[0]._creator.isInit('name'), false);
+ assert.equal(populatedPost.comments[1]._creator.email, 'user2@learnboost.com');
+ assert.equal(populatedPost.comments[1]._creator.isInit('name'), false);
});
- it('populating subdocs with invalid/missing subproperties', function(done) {
+ it('populating subdocs with invalid/missing subproperties', async function() {
const BlogPost = db.model('BlogPost', blogPostSchema);
const User = db.model('User', userSchema);
- User.create({
+ await User.create({
name: 'T-100',
email: 'terminator100@learnboost.com'
- }, function(err) {
- assert.ifError(err);
-
- User.create({
- name: 'T-1000',
- email: 'terminator1000@learnboost.com'
- }, function(err, user2) {
- assert.ifError(err);
-
- BlogPost.create({
- title: 'Woot',
- comments: [
- { _creator: null, content: 'Woot woot' },
- { _creator: user2, content: 'Wha wha' }
- ]
- }, function(err, post) {
- assert.ifError(err);
-
- // non-existant subprop
- BlogPost
- .findById(post._id)
- .populate({
- path: 'comments._idontexist',
- select: 'email',
- strictPopulate: false
- })
- .exec(function(err) {
- assert.ifError(err);
-
- // add a non-schema property to the document.
- BlogPost.collection.updateOne(
- { _id: post._id }
- , { $set: { 'comments.0._idontexist': user2._id } }, function(err) {
- assert.ifError(err);
-
- // allow population of unknown property by passing model name.
- // helpful when populating mapReduce results too.
- BlogPost
- .findById(post._id)
- .populate({
- path: 'comments._idontexist',
- select: 'email',
- model: 'User',
- strictPopulate: false
- })
- .exec(function(err, post) {
- assert.ifError(err);
- assert.ok(post);
- assert.equal(post.comments.length, 2);
- assert.ok(post.comments[0].get('_idontexist'));
- assert.equal(String(post.comments[0].get('_idontexist')._id), user2.id);
- assert.equal(post.comments[0].get('_idontexist').email, 'terminator1000@learnboost.com');
- assert.equal(post.comments[0].get('_idontexist').isInit('name'), false);
- assert.strictEqual(post.comments[0]._creator, null);
- assert.equal(post.comments[1]._creator.toString(), user2.id);
-
- // subprop is null in a doc
- BlogPost
- .findById(post._id)
- .populate('comments._creator', 'email')
- .exec(function(err, post) {
- assert.ifError(err);
-
- assert.ok(post.comments);
- assert.equal(post.comments.length, 2);
- assert.strictEqual(post.comments[0]._creator, null);
- assert.strictEqual(post.comments[0].content, 'Woot woot');
- assert.equal(post.comments[1]._creator.email, 'terminator1000@learnboost.com');
- assert.equal(post.comments[1]._creator.isInit('name'), false);
- assert.equal(post.comments[1].content, 'Wha wha');
-
- done();
- });
- });
- });
- });
- });
- });
});
+
+ const user2 = await User.create({
+ name: 'T-1000',
+ email: 'terminator1000@learnboost.com'
+ });
+
+ const post = await BlogPost.create({
+ title: 'Woot',
+ comments: [
+ { _creator: null, content: 'Woot woot' },
+ { _creator: user2, content: 'Wha wha' }
+ ]
+ });
+
+ // non-existant subprop
+ await BlogPost.findById(post._id).populate({
+ path: 'comments._idontexist',
+ select: 'email',
+ strictPopulate: false
+ });
+
+ // add a non-schema property to the document.
+ await BlogPost.collection.updateOne(
+ { _id: post._id }, { $set: { 'comments.0._idontexist': user2._id } }
+ );
+
+ // allow population of unknown property by passing model name.
+ const post2 = await BlogPost.findById(post._id).populate({
+ path: 'comments._idontexist',
+ select: 'email',
+ model: 'User',
+ strictPopulate: false
+ });
+
+ assert.ok(post2);
+ assert.equal(post2.comments.length, 2);
+ assert.ok(post2.comments[0].get('_idontexist'));
+ assert.equal(String(post2.comments[0].get('_idontexist')._id), user2.id);
+ assert.equal(post2.comments[0].get('_idontexist').email, 'terminator1000@learnboost.com');
+ assert.equal(post2.comments[0].get('_idontexist').isInit('name'), false);
+ assert.strictEqual(post2.comments[0]._creator, null);
+ assert.equal(post2.comments[1]._creator.toString(), user2.id);
+
+ // subprop is null in a doc
+ const post3 = await BlogPost.findById(post._id).populate('comments._creator', 'email');
+
+ assert.ok(post3.comments);
+ assert.equal(post3.comments.length, 2);
+ assert.strictEqual(post3.comments[0]._creator, null);
+ assert.strictEqual(post3.comments[0].content, 'Woot woot');
+ assert.equal(post3.comments[1]._creator.email, 'terminator1000@learnboost.com');
+ assert.equal(post3.comments[1]._creator.isInit('name'), false);
+ assert.equal(post3.comments[1].content, 'Wha wha');
});
- it('properly handles limit per document (gh-2151)', function(done) {
+ it('properly handles limit per document (gh-2151)', async function() {
const ObjectId = mongoose.Types.ObjectId;
const user = new Schema({
@@ -1479,94 +1185,70 @@ describe('model: populate:', function() {
friends: [userIds[0], userIds[1], userIds[2]]
});
- User.create(users, function(err) {
- assert.ifError(err);
+ await User.create(users);
- const blogposts = [];
- blogposts.push({
- title: 'blog 1',
- tags: ['fun', 'cool'],
- author: userIds[3]
- });
- blogposts.push({
- title: 'blog 2',
- tags: ['cool'],
- author: userIds[1]
- });
- blogposts.push({
- title: 'blog 3',
- tags: ['fun', 'odd'],
- author: userIds[2]
- });
+ const blogposts = [];
+ blogposts.push({
+ title: 'blog 1',
+ tags: ['fun', 'cool'],
+ author: userIds[3]
+ });
+ blogposts.push({
+ title: 'blog 2',
+ tags: ['cool'],
+ author: userIds[1]
+ });
+ blogposts.push({
+ title: 'blog 3',
+ tags: ['fun', 'odd'],
+ author: userIds[2]
+ });
- BlogPost.create(blogposts, function(err) {
- assert.ifError(err);
+ await BlogPost.create(blogposts);
- BlogPost.
- find({ tags: 'fun' }).
- lean().
- populate('author').
- exec(function(err, docs) {
- assert.ifError(err);
- const opts = {
- path: 'author.friends',
- select: 'name',
- options: { limit: 1 }
- };
+ const docs = await BlogPost.find({ tags: 'fun' }).lean().populate('author');
- BlogPost.populate(docs, opts, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 2);
- assert.equal(docs[0].author.friends.length, 1);
- assert.equal(docs[1].author.friends.length, 1);
- assert.equal(opts.options.limit, 1);
- done();
- });
- });
- });
- });
+ const opts = { path: 'author.friends', select: 'name', options: { limit: 1 } };
+
+ await BlogPost.populate(docs, opts);
+
+ assert.equal(docs.length, 2);
+ assert.equal(docs[0].author.friends.length, 1);
+ assert.equal(docs[1].author.friends.length, 1);
+ assert.equal(opts.options.limit, 1);
});
- it('populating subdocuments partially with empty array (gh-481)', function(done) {
+ it('populating subdocuments partially with empty array (gh-481)', async function() {
const BlogPost = db.model('BlogPost', blogPostSchema);
db.model('User', userSchema);
- BlogPost.create({
+ const post = await BlogPost.create({
title: 'Woot',
comments: [] // EMPTY ARRAY
- }, function(err, post) {
- assert.ifError(err);
-
- BlogPost
- .findById(post._id)
- .populate('comments._creator', 'email')
- .exec(function(err, returned) {
- assert.ifError(err);
- assert.equal(returned.id, post.id);
- done();
- });
});
+
+ const returned = await BlogPost
+ .findById(post._id)
+ .populate('comments._creator', 'email')
+ .exec();
+
+ assert.equal(returned.id, post.id);
});
- it('populating subdocuments partially with null array', function(done) {
+ it('populating subdocuments partially with null array', async function() {
const BlogPost = db.model('BlogPost', blogPostSchema);
db.model('User', userSchema);
- BlogPost.create({
+ const post = await BlogPost.create({
title: 'Woot',
comments: null
- }, function(err, post) {
- assert.ifError(err);
-
- BlogPost
- .findById(post._id)
- .populate('comments._creator')
- .exec(function(err, returned) {
- assert.ifError(err);
- assert.equal(returned.id, post.id);
- done();
- });
});
+
+ const returned = await BlogPost
+ .findById(post._id)
+ .populate('comments._creator');
+
+ assert.equal(returned.id, post.id);
});
it('populating subdocuments with array including nulls', async function() {
@@ -1623,7 +1305,7 @@ describe('model: populate:', function() {
assert.strictEqual(returned.fans[3], null);
});
- it('supports `retainNullValues` while supressing _id of subdocument', async function() {
+ it('supports `retainNullValues` while suppressing _id of subdocument', async function() {
const BlogPost = db.model('BlogPost', blogPostSchema);
const User = db.model('User', userSchema);
@@ -1639,7 +1321,7 @@ describe('model: populate:', function() {
$set: { fans: [user.id] }
});
- await user.delete();
+ await user.deleteOne();
const returned = await BlogPost.
findById(post._id).
@@ -1649,7 +1331,7 @@ describe('model: populate:', function() {
assert.strictEqual(returned.fans[0], null);
});
- it('populating more than one array at a time', function(done) {
+ it('populating more than one array at a time', async function() {
const User = db.model('User', userSchema);
const M = db.model('Test', new Schema({
users: [{ type: ObjectId, ref: 'User' }],
@@ -1657,7 +1339,7 @@ describe('model: populate:', function() {
comments: [commentSchema]
}));
- User.create({
+ const [fan1, fan2, fan3] = await User.create({
email: 'fan1@learnboost.com'
}, {
name: 'Fan 2',
@@ -1665,64 +1347,56 @@ describe('model: populate:', function() {
gender: 'female'
}, {
name: 'Fan 3'
- }, function(err, fan1, fan2, fan3) {
- assert.ifError(err);
-
- M.create({
- users: [fan3],
- fans: [fan1],
- comments: [
- { _creator: fan1, content: 'bejeah!' },
- { _creator: fan2, content: 'chickfila' }
- ]
- }, {
- users: [fan1],
- fans: [fan2],
- comments: [
- { _creator: fan3, content: 'hello' },
- { _creator: fan1, content: 'world' }
- ]
- }, function(err, post1, post2) {
- assert.ifError(err);
-
- M.where('_id').in([post1, post2])
- .populate('fans', 'name', { gender: 'female' })
- .populate('users', 'name', { gender: 'male' })
- .populate('comments._creator', 'email', { name: null })
- .exec(function(err, posts) {
- assert.ifError(err);
-
- assert.ok(posts);
- assert.equal(posts.length, 2);
- const p1 = posts[0];
- const p2 = posts[1];
- assert.strictEqual(p1.fans.length, 0);
- assert.strictEqual(p2.fans.length, 1);
- assert.equal(p2.fans[0].name, 'Fan 2');
- assert.equal(p2.fans[0].isInit('email'), false);
- assert.equal(p2.fans[0].isInit('gender'), false);
- assert.equal(p1.comments.length, 2);
- assert.equal(p2.comments.length, 2);
- assert.ok(p1.comments[0]._creator.email);
- assert.ok(!p2.comments[0]._creator);
- assert.equal(p1.comments[0]._creator.email, 'fan1@learnboost.com');
- assert.equal(p2.comments[1]._creator.email, 'fan1@learnboost.com');
- assert.equal(p1.comments[0]._creator.isInit('name'), false);
- assert.equal(p2.comments[1]._creator.isInit('name'), false);
- assert.equal(p1.comments[0].content, 'bejeah!');
- assert.equal(p2.comments[1].content, 'world');
- assert.ok(!p1.comments[1]._creator);
- assert.ok(!p2.comments[0]._creator);
- assert.equal(p1.comments[1].content, 'chickfila');
- assert.equal(p2.comments[0].content, 'hello');
+ });
- done();
- });
- });
+ const [post1, post2] = await M.create({
+ users: [fan3],
+ fans: [fan1],
+ comments: [
+ { _creator: fan1, content: 'bejeah!' },
+ { _creator: fan2, content: 'chickfila' }
+ ]
+ }, {
+ users: [fan1],
+ fans: [fan2],
+ comments: [
+ { _creator: fan3, content: 'hello' },
+ { _creator: fan1, content: 'world' }
+ ]
});
+
+ const posts = await M.where('_id').in([post1, post2])
+ .populate('fans', 'name', { gender: 'female' })
+ .populate('users', 'name', { gender: 'male' })
+ .populate('comments._creator', 'email', { name: null });
+
+ assert.ok(posts);
+ assert.equal(posts.length, 2);
+ const p1 = posts[0];
+ const p2 = posts[1];
+ assert.strictEqual(p1.fans.length, 0);
+ assert.strictEqual(p2.fans.length, 1);
+ assert.equal(p2.fans[0].name, 'Fan 2');
+ assert.equal(p2.fans[0].isInit('email'), false);
+ assert.equal(p2.fans[0].isInit('gender'), false);
+ assert.equal(p1.comments.length, 2);
+ assert.equal(p2.comments.length, 2);
+ assert.ok(p1.comments[0]._creator.email);
+ assert.ok(!p2.comments[0]._creator);
+ assert.equal(p1.comments[0]._creator.email, 'fan1@learnboost.com');
+ assert.equal(p2.comments[1]._creator.email, 'fan1@learnboost.com');
+ assert.equal(p1.comments[0]._creator.isInit('name'), false);
+ assert.equal(p2.comments[1]._creator.isInit('name'), false);
+ assert.equal(p1.comments[0].content, 'bejeah!');
+ assert.equal(p2.comments[1].content, 'world');
+ assert.ok(!p1.comments[1]._creator);
+ assert.ok(!p2.comments[0]._creator);
+ assert.equal(p1.comments[1].content, 'chickfila');
+ assert.equal(p2.comments[0].content, 'hello');
+
});
- it('populating multiple children of a sub-array at a time', function(done) {
+ it('populating multiple children of a sub-array at a time', async function() {
const BlogPost = db.model('BlogPost', blogPostSchema);
const User = db.model('User', userSchema);
const Inner = new Schema({
@@ -1735,7 +1409,7 @@ describe('model: populate:', function() {
kids: [Inner]
}));
- User.create({
+ const [fan1, fan2] = await User.create({
name: 'Fan 1',
email: 'fan1@learnboost.com',
gender: 'male'
@@ -1743,104 +1417,81 @@ describe('model: populate:', function() {
name: 'Fan 2',
email: 'fan2@learnboost.com',
gender: 'female'
- }, function(err, fan1, fan2) {
- assert.ifError(err);
+ });
- BlogPost.create({
- title: 'woot'
- }, {
- title: 'yay'
- }, function(err, post1, post2) {
- assert.ifError(err);
- M.create({
- kids: [
- { user: fan1, post: post1, y: 5 },
- { user: fan2, post: post2, y: 8 }
- ],
- x: 4
- }, function(err, m1) {
- assert.ifError(err);
-
- M.findById(m1)
- .populate('kids.user', 'name')
- .populate('kids.post', 'title', { title: 'woot' })
- .exec(function(err, o) {
- assert.ifError(err);
- assert.strictEqual(o.kids.length, 2);
- const k1 = o.kids[0];
- const k2 = o.kids[1];
- assert.strictEqual(true, !k2.post);
- assert.strictEqual(k1.user.name, 'Fan 1');
- assert.strictEqual(k1.user.email, undefined);
- assert.strictEqual(k1.post.title, 'woot');
- assert.strictEqual(k2.user.name, 'Fan 2');
-
- done();
- });
- });
- });
+ const [post1, post2] = await BlogPost.create({
+ title: 'woot'
+ }, {
+ title: 'yay'
+ });
+
+ const m1 = await M.create({
+ kids: [
+ { user: fan1, post: post1, y: 5 },
+ { user: fan2, post: post2, y: 8 }
+ ],
+ x: 4
});
+
+ const o = await M.findById(m1)
+ .populate('kids.user', 'name')
+ .populate('kids.post', 'title', { title: 'woot' })
+ .exec();
+
+ assert.strictEqual(o.kids.length, 2);
+ const k1 = o.kids[0];
+ const k2 = o.kids[1];
+ assert.strictEqual(true, !k2.post);
+ assert.strictEqual(k1.user.name, 'Fan 1');
+ assert.strictEqual(k1.user.email, undefined);
+ assert.strictEqual(k1.post.title, 'woot');
+ assert.strictEqual(k2.user.name, 'Fan 2');
});
- it('passing sort options to the populate method', function(done) {
+ it('passing sort options to the populate method', async function() {
const P = db.model('BlogPost', blogPostSchema);
const User = db.model('User', userSchema);
- User.create(
+ const [fan1, fan2, fan3, fan4] = await User.create(
{ name: 'aaron', age: 10 },
{ name: 'fan2', age: 8 },
{ name: 'someone else', age: 3 },
- { name: 'val', age: 3 },
- function(err, fan1, fan2, fan3, fan4) {
- assert.ifError(err);
-
- P.create({ fans: [fan4, fan2, fan3, fan1] }, function(err, post) {
- assert.ifError(err);
-
- P.findById(post)
- .populate('fans', null, null, { sort: { age: 1, name: 1 } })
- .exec(function(err, post) {
- assert.ifError(err);
-
- assert.equal(post.fans.length, 4);
- assert.equal(post.fans[0].name, 'someone else');
- assert.equal(post.fans[1].name, 'val');
- assert.equal(post.fans[2].name, 'fan2');
- assert.equal(post.fans[3].name, 'aaron');
-
- P.findById(post)
- .populate('fans', 'name', null, { sort: { name: -1 } })
- .exec(function(err, post) {
- assert.ifError(err);
-
- assert.equal(post.fans.length, 4);
- assert.equal(post.fans[3].name, 'aaron');
- assert.strictEqual(undefined, post.fans[3].age);
- assert.equal(post.fans[2].name, 'fan2');
- assert.strictEqual(undefined, post.fans[2].age);
- assert.equal(post.fans[1].name, 'someone else');
- assert.strictEqual(undefined, post.fans[1].age);
- assert.equal(post.fans[0].name, 'val');
- assert.strictEqual(undefined, post.fans[0].age);
-
- P.findById(post)
- .populate('fans', 'age', { age: { $gt: 3 } }, { sort: { name: 'desc' } })
- .exec(function(err, post) {
- assert.ifError(err);
-
- assert.equal(post.fans.length, 2);
- assert.equal(post.fans[1].age.valueOf(), 10);
- assert.equal(post.fans[0].age.valueOf(), 8);
-
- done();
- });
- });
- });
- });
- });
+ { name: 'val', age: 3 }
+ );
+
+ const post = await P.create({ fans: [fan4, fan2, fan3, fan1] });
+
+ let populatedPost = await P.findById(post)
+ .populate('fans', null, null, { sort: { age: 1, name: 1 } });
+
+ assert.equal(populatedPost.fans.length, 4);
+ assert.equal(populatedPost.fans[0].name, 'someone else');
+ assert.equal(populatedPost.fans[1].name, 'val');
+ assert.equal(populatedPost.fans[2].name, 'fan2');
+ assert.equal(populatedPost.fans[3].name, 'aaron');
+
+ populatedPost = await P.findById(post)
+ .populate('fans', 'name', null, { sort: { name: -1 } });
+
+ assert.equal(populatedPost.fans.length, 4);
+ assert.equal(populatedPost.fans[3].name, 'aaron');
+ assert.strictEqual(undefined, populatedPost.fans[3].age);
+ assert.equal(populatedPost.fans[2].name, 'fan2');
+ assert.strictEqual(undefined, populatedPost.fans[2].age);
+ assert.equal(populatedPost.fans[1].name, 'someone else');
+ assert.strictEqual(undefined, populatedPost.fans[1].age);
+ assert.equal(populatedPost.fans[0].name, 'val');
+ assert.strictEqual(undefined, populatedPost.fans[0].age);
+
+ populatedPost = await P.findById(post)
+ .populate('fans', 'age', { age: { $gt: 3 } }, { sort: { name: 'desc' } });
+
+ assert.equal(populatedPost.fans.length, 2);
+ assert.equal(populatedPost.fans[1].age.valueOf(), 10);
+ assert.equal(populatedPost.fans[0].age.valueOf(), 8);
});
- it('limit should apply to each returned doc, not in aggregate (gh-1490)', function(done) {
+ it('limit should apply to each returned doc, not in aggregate (gh-1490)', async function() {
const sB = new Schema({
name: String
});
@@ -1859,31 +1510,18 @@ describe('model: populate:', function() {
const j1 = new J({ b: [b1.id, b2.id, b5.id] });
const j2 = new J({ b: [b3.id, b4.id, b5.id] });
- let count = 7;
-
- b1.save(cb);
- b2.save(cb);
- b3.save(cb);
- b4.save(cb);
- b5.save(cb);
- j1.save(cb);
- j2.save(cb);
-
- function cb(err) {
- if (err) {
- throw err;
- }
- --count || next();
- }
-
- function next() {
- J.find().populate({ path: 'b', options: { limit: 2 } }).exec(function(err, j) {
- assert.equal(j.length, 2);
- assert.equal(j[0].b.length, 2);
- assert.equal(j[1].b.length, 2);
- done();
- });
- }
+ await b1.save();
+ await b2.save();
+ await b3.save();
+ await b4.save();
+ await b5.save();
+ await j1.save();
+ await j2.save();
+
+ const j = await J.find().populate({ path: 'b', options: { limit: 2 } });
+ assert.equal(j.length, 2);
+ assert.equal(j[0].b.length, 2);
+ assert.equal(j[1].b.length, 2);
});
it('refs should cast to ObjectId from hexstrings', function(done) {
@@ -1897,7 +1535,7 @@ describe('model: populate:', function() {
done();
});
- it('populate should work on String _ids', function(done) {
+ it('populate should work on String _ids', async function() {
const UserSchema = new Schema({
_id: String,
name: String
@@ -1913,69 +1551,29 @@ describe('model: populate:', function() {
const alice = new User({ _id: 'alice', name: 'Alice' });
- alice.save(function(err) {
- assert.ifError(err);
+ await alice.save();
- const note = new Note({ author: 'alice', body: 'Buy Milk' });
- note.save(function(err) {
- assert.ifError(err);
+ const note = new Note({ author: 'alice', body: 'Buy Milk' });
+ await note.save();
- Note.findById(note.id).populate('author').exec(function(err, note) {
- assert.ifError(err);
- assert.equal(note.body, 'Buy Milk');
- assert.ok(note.author);
- assert.equal(note.author.name, 'Alice');
- done();
- });
- });
- });
- });
+ const populatedNote = await Note.findById(note.id).populate('author').exec();
- it('populate should work on Number _ids', function(done) {
- const UserSchema = new Schema({
- _id: Number,
- name: String
- });
+ assert.equal(populatedNote.body, 'Buy Milk');
+ assert.ok(populatedNote.author);
+ assert.equal(populatedNote.author.name, 'Alice');
+ });
- const NoteSchema = new Schema({
- author: { type: Number, ref: 'User' },
- body: String
+ it('required works on ref fields (gh-577)', async function() {
+ const userSchema = new Schema({
+ email: { type: String, required: true }
});
+ const User = db.model('User', userSchema);
- const User = db.model('User', UserSchema);
- const Note = db.model('Test', NoteSchema);
-
- const alice = new User({ _id: 2359, name: 'Alice' });
-
- alice.save(function(err) {
- assert.ifError(err);
+ const numSchema = new Schema({ _id: Number, val: Number });
+ const Num = db.model('Test', numSchema);
- const note = new Note({ author: 2359, body: 'Buy Milk' });
- note.save(function(err) {
- assert.ifError(err);
-
- Note.findById(note.id).populate('author').exec(function(err, note) {
- assert.ifError(err);
- assert.equal(note.body, 'Buy Milk');
- assert.ok(note.author);
- assert.equal(note.author.name, 'Alice');
- done();
- });
- });
- });
- });
-
- it('required works on ref fields (gh-577)', function(done) {
- const userSchema = new Schema({
- email: { type: String, required: true }
- });
- const User = db.model('User', userSchema);
-
- const numSchema = new Schema({ _id: Number, val: Number });
- const Num = db.model('Test', numSchema);
-
- const strSchema = new Schema({ _id: String, val: String });
- const Str = db.model('Test1', strSchema);
+ const strSchema = new Schema({ _id: String, val: String });
+ const Str = db.model('Test1', strSchema);
const commentSchema = new Schema({
user: { type: ObjectId, ref: 'User', required: true },
@@ -1985,64 +1583,69 @@ describe('model: populate:', function() {
});
const Comment = db.model('Comment', commentSchema);
- let pending = 3;
+ let comment = new Comment({
+ text: 'test'
+ });
+
+ const err = await comment.save().then(() => null, err => err);
+ assert.ok(err);
+ assert.ok(err.message.indexOf('Comment validation failed') === 0, err.message);
+ assert.ok('num' in err.errors);
+ assert.ok('str' in err.errors);
+ assert.ok('user' in err.errors);
const string = new Str({ _id: 'my string', val: 'hello' });
const number = new Num({ _id: 1995, val: 234 });
const user = new User({ email: 'test' });
- string.save(next);
- number.save(next);
- user.save(next);
+ await Promise.all([string.save(), number.save(), user.save()]);
- function next(err) {
- assert.strictEqual(err, null);
- if (--pending) {
- return;
- }
+ comment.user = user;
+ comment.num = 1995;
+ comment.str = 'my string';
+ await comment.save();
- const comment = new Comment({
- text: 'test'
- });
+ comment = await Comment
+ .findById(comment.id)
+ .populate('user')
+ .populate('num')
+ .populate('str');
+
+ assert.equal(comment.user.email, 'test');
+ assert.equal(comment.num.val, 234);
+ assert.equal(comment.str.val, 'hello');
+
+ comment.set({ text: 'test2' });
+ await comment.save();
+ });
- comment.save(function(err) {
- assert.ok(err);
- assert.ok(err.message.indexOf('Comment validation failed') === 0, err.message);
- assert.ok('num' in err.errors);
- assert.ok('str' in err.errors);
- assert.ok('user' in err.errors);
- assert.equal(err.errors.num.kind, 'required');
- assert.equal(err.errors.str.kind, 'required');
- assert.equal(err.errors.user.kind, 'required');
+ it('populate should work on Number _ids', async function() {
+ const UserSchema = new Schema({
+ _id: Number,
+ name: String
+ });
- comment.user = user;
- comment.num = 1995;
- comment.str = 'my string';
+ const NoteSchema = new Schema({
+ author: { type: Number, ref: 'User' },
+ body: String
+ });
- comment.save(function(err, comment) {
- assert.strictEqual(err, null);
+ const User = db.model('User', UserSchema);
+ const Note = db.model('Test', NoteSchema);
- Comment
- .findById(comment.id)
- .populate('user')
- .populate('num')
- .populate('str')
- .exec(function(err, comment) {
- assert.ifError(err);
+ const alice = new User({ _id: 2359, name: 'Alice' });
- comment.set({ text: 'test2' });
+ await alice.save();
+ let note = new Note({ author: 2359, body: 'Buy Milk' });
+ await note.save();
- comment.save(function(err) {
- assert.ifError(err);
- done();
- });
- });
- });
- });
- }
+ note = await Note.findById(note.id).populate('author');
+ assert.equal(note.body, 'Buy Milk');
+ assert.ok(note.author);
+ assert.equal(note.author.name, 'Alice');
});
- it('populate works with schemas with both id and _id defined', function(done) {
+ it('populate works with schemas with both id and _id defined', async function() {
const S1 = new Schema({ id: String });
const S2 = new Schema({ things: [{ type: ObjectId, ref: 'Test' }] });
@@ -2050,62 +1653,46 @@ describe('model: populate:', function() {
const M2 = db.model('Test1', S2);
db.model('Test2', Schema({ _id: String, val: String }));
- M1.create(
- { id: 'The Tiger That Isn\'t' }
- , { id: 'Users Guide To The Universe' }
- , function(err, a, b) {
- assert.ifError(err);
-
- const m2 = new M2({ things: [a, b] });
- m2.save(function(err) {
- assert.ifError(err);
- M2.findById(m2).populate('things').exec(function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.things.length, 2);
- assert.equal(doc.things[0].id, 'The Tiger That Isn\'t');
- assert.equal(doc.things[1].id, 'Users Guide To The Universe');
- done();
- });
- });
- });
+ const [a, b] = await M1.create([
+ { id: 'The Tiger That Isn\'t' },
+ { id: 'Users Guide To The Universe' }
+ ]);
+
+ const m2 = new M2({ things: [a, b] });
+ await m2.save();
+ const doc = await M2.findById(m2).populate('things');
+ assert.equal(doc.things.length, 2);
+ assert.equal(doc.things[0].id, 'The Tiger That Isn\'t');
+ assert.equal(doc.things[1].id, 'Users Guide To The Universe');
});
- it('Update works with populated arrays (gh-602)', function(done) {
+ it('Update works with populated arrays (gh-602)', async function() {
const BlogPost = db.model('BlogPost', blogPostSchema);
const User = db.model('User', userSchema);
- User.create({ name: 'aphex' }, { name: 'twin' }, function(err, u1, u2) {
- assert.ifError(err);
+ const [u1, u2] = await User.create([{ name: 'aphex' }, { name: 'twin' }]);
- BlogPost.create({
- title: 'Woot',
- fans: []
- }, function(err, post) {
- assert.ifError(err);
-
- const update = { fans: [u1, u2] };
- BlogPost.updateOne({ _id: post }, update, function(err) {
- assert.ifError(err);
-
- // the original update doc should not be modified
- assert.ok('fans' in update);
- assert.ok(!('$set' in update));
- assert.ok(update.fans[0] instanceof mongoose.Document);
- assert.ok(update.fans[1] instanceof mongoose.Document);
-
- BlogPost.findById(post, function(err, post) {
- assert.ifError(err);
- assert.equal(post.fans.length, 2);
- assert.ok(post.fans[0] instanceof DocObjectId);
- assert.ok(post.fans[1] instanceof DocObjectId);
- done();
- });
- });
- });
+ let post = await BlogPost.create({
+ title: 'Woot',
+ fans: []
});
+ const update = { fans: [u1, u2] };
+ await BlogPost.updateOne({ _id: post }, update);
+
+ // the original update doc should not be modified
+ assert.ok('fans' in update);
+ assert.ok(!('$set' in update));
+ assert.ok(update.fans[0] instanceof mongoose.Document);
+ assert.ok(update.fans[1] instanceof mongoose.Document);
+
+ post = await BlogPost.findById(post);
+ assert.equal(post.fans.length, 2);
+ assert.ok(post.fans[0] instanceof DocObjectId);
+ assert.ok(post.fans[1] instanceof DocObjectId);
+
});
- it('toJSON should also be called for refs (gh-675)', function(done) {
+ it('toJSON should also be called for refs (gh-675)', async function() {
const BlogPost = db.model('BlogPost', blogPostSchema);
const User = db.model('User', userSchema);
@@ -2123,34 +1710,26 @@ describe('model: populate:', function() {
return res;
};
- User.create({
+ const creator = await User.create({
name: 'Jerem',
email: 'jerem@jolicloud.com'
- }, function(err, creator) {
- assert.ifError(err);
+ });
- BlogPost.create({
- title: 'Ping Pong',
- _creator: creator
- }, function(err, post) {
- assert.ifError(err);
-
- BlogPost
- .findById(post._id)
- .populate('_creator')
- .exec(function(err, post) {
- assert.ifError(err);
-
- const json = post.toJSON();
- assert.equal(true, json.was_in_to_json);
- assert.equal(json._creator.was_in_to_json, true);
- done();
- });
- });
+ let post = await BlogPost.create({
+ title: 'Ping Pong',
+ _creator: creator
});
+
+ post = await BlogPost
+ .findById(post._id)
+ .populate('_creator');
+
+ const json = post.toJSON();
+ assert.equal(true, json.was_in_to_json);
+ assert.equal(json._creator.was_in_to_json, true);
});
- it('populate should work on Buffer _ids (gh-686)', function(done) {
+ it('populate should work on Buffer _ids (gh-686)', async() => {
const UserSchema = new Schema({
_id: Buffer,
name: String
@@ -2166,25 +1745,19 @@ describe('model: populate:', function() {
const alice = new User({ _id: new mongoose.Types.Buffer('YWxpY2U=', 'base64'), name: 'Alice' });
- alice.save(function(err) {
- assert.ifError(err);
+ await alice.save();
- const note = new Note({ author: 'alice', body: 'Buy Milk' });
- note.save(function(err) {
- assert.ifError(err);
+ const note = new Note({ author: 'alice', body: 'Buy Milk' });
+ await note.save();
- Note.findById(note.id).populate('author').exec(function(err, note) {
- assert.ifError(err);
- assert.equal(note.body, 'Buy Milk');
- assert.ok(note.author);
- assert.equal(note.author.name, 'Alice');
- done();
- });
- });
- });
+ const foundNote = await Note.findById(note.id).populate('author').exec();
+
+ assert.equal(foundNote.body, 'Buy Milk');
+ assert.ok(foundNote.author);
+ assert.equal(foundNote.author.name, 'Alice');
});
- it('populated Buffer _ids should be requireable', function(done) {
+ it('populated Buffer _ids should be requireable', async function() {
const UserSchema = new Schema({
_id: Buffer,
name: String
@@ -2200,140 +1773,72 @@ describe('model: populate:', function() {
const alice = new User({ _id: new mongoose.Types.Buffer('YWxpY2U=', 'base64'), name: 'Alice' });
- alice.save(function(err) {
- assert.ifError(err);
+ await alice.save();
- const note = new Note({ author: 'alice', body: 'Buy Milk' });
- note.save(function(err) {
- assert.ifError(err);
+ const note = new Note({ author: 'alice', body: 'Buy Milk' });
+ await note.save();
- Note.findById(note.id).populate('author').exec(function(err, note) {
- assert.ifError(err);
- note.save(function(err) {
- assert.ifError(err);
- done();
- });
- });
- });
- });
+ const notePopulated = await Note.findById(note.id).populate('author').exec();
+ await notePopulated.save();
});
- it('populating with custom model selection (gh-773)', function(done) {
+ it('populating with custom model selection (gh-773)', async function() {
const BlogPost = db.model('BlogPost', blogPostSchema);
const User = db.model('Test', userSchema);
- User.create({
+ const creator = await User.create({
name: 'Daniel',
email: 'daniel.baulig@gmx.de'
- }, function(err, creator) {
- assert.ifError(err);
-
- BlogPost.create({
- title: 'woot',
- _creator: creator
- }, function(err, post) {
- assert.ifError(err);
+ });
- BlogPost
- .findById(post._id)
- .populate('_creator', 'email', 'Test')
- .exec(function(err, post) {
- assert.ifError(err);
+ const post = await BlogPost.create({
+ title: 'woot',
+ _creator: creator
+ });
- assert.ok(post._creator instanceof User);
- assert.equal(post._creator.isInit('name'), false);
- assert.equal(post._creator.email, 'daniel.baulig@gmx.de');
+ const post2 = await BlogPost.findById(post._id).populate('_creator', 'email', 'Test').exec();
- done();
- });
- });
- });
+ assert.ok(post2._creator instanceof User);
+ assert.equal(post2._creator.isInit('name'), false);
+ assert.equal(post2._creator.email, 'daniel.baulig@gmx.de');
});
describe('specifying a custom model without specifying a ref in schema', function() {
- it('with String _id', function(done) {
+ it('with String _id', async function() {
const A = db.model('Test', { name: String, _id: String });
const B = db.model('Test1', { other: String });
- A.create({ name: 'hello', _id: 'first' }, function(err, a) {
- if (err) {
- return done(err);
- }
- B.create({ other: a._id }, function(err, b) {
- if (err) {
- return done(err);
- }
- B.findById(b._id).populate({ path: 'other', model: 'Test' }).exec(function(err, b) {
- if (err) {
- return done(err);
- }
- assert.equal(b.other.name, 'hello');
- done();
- });
- });
- });
+ const a = await A.create({ name: 'hello', _id: 'first' });
+ let b = await B.create({ other: a._id });
+ b = await B.findById(b._id).populate({ path: 'other', model: 'Test' });
+ assert.equal(b.other.name, 'hello');
+
});
- it('with Number _id', function(done) {
+ it('with Number _id', async function() {
const A = db.model('Test', { name: String, _id: Number });
const B = db.model('Test1', { other: Number });
- A.create({ name: 'hello', _id: 3 }, function(err, a) {
- if (err) {
- return done(err);
- }
- B.create({ other: a._id }, function(err, b) {
- if (err) {
- return done(err);
- }
- B.findById(b._id).populate({ path: 'other', model: 'Test' }).exec(function(err, b) {
- if (err) {
- return done(err);
- }
- assert.equal(b.other.name, 'hello');
- done();
- });
- });
- });
+ const a = await A.create({ name: 'hello', _id: 3 });
+ let b = await B.create({ other: a._id });
+ b = await B.findById(b._id).populate({ path: 'other', model: 'Test' });
+ assert.equal(b.other.name, 'hello');
+
});
- it('with Buffer _id', function(done) {
+ it('with Buffer _id', async function() {
const A = db.model('Test', { name: String, _id: Buffer });
const B = db.model('Test1', { other: Buffer });
- A.create({ name: 'hello', _id: Buffer.from('x') }, function(err, a) {
- if (err) {
- return done(err);
- }
- B.create({ other: a._id }, function(err, b) {
- if (err) {
- return done(err);
- }
- B.findById(b._id).populate({ path: 'other', model: 'Test' }).exec(function(err, b) {
- if (err) {
- return done(err);
- }
- assert.equal(b.other.name, 'hello');
- done();
- });
- });
- });
+ const a = await A.create({ name: 'hello', _id: Buffer.from('x') });
+ let b = await B.create({ other: a._id });
+ b = await B.findById(b._id).populate({ path: 'other', model: 'Test' });
+ assert.equal(b.other.name, 'hello');
+
});
- it('with ObjectId _id', function(done) {
+ it('with ObjectId _id', async function() {
const A = db.model('Test', { name: String });
const B = db.model('Test1', { other: Schema.ObjectId });
- A.create({ name: 'hello' }, function(err, a) {
- if (err) {
- return done(err);
- }
- B.create({ other: a._id }, function(err, b) {
- if (err) {
- return done(err);
- }
- B.findById(b._id).populate({ path: 'other', model: 'Test' }).exec(function(err, b) {
- if (err) {
- return done(err);
- }
- assert.equal(b.other.name, 'hello');
- done();
- });
- });
- });
+ const a = await A.create({ name: 'hello' });
+ let b = await B.create({ other: a._id });
+ b = await B.findById(b._id).populate({ path: 'other', model: 'Test' });
+ assert.equal(b.other.name, 'hello');
+
});
});
@@ -2359,31 +1864,27 @@ describe('model: populate:', function() {
then(_post => { post = _post; });
});
- it('works', function(done) {
- B.findById(post._id)
+ it('works', async function() {
+ post = await B.findById(post._id)
.populate({
path: 'fans',
select: 'name',
model: 'Test',
match: { name: /u/ },
options: { sort: { name: -1 } }
- })
- .exec(function(err, post) {
- assert.ifError(err);
+ });
+ assert.ok(Array.isArray(post.fans));
+ assert.equal(post.fans.length, 2);
+ assert.ok(post.fans[0] instanceof User);
+ assert.ok(post.fans[1] instanceof User);
+ assert.equal(post.fans[0].isInit('name'), true);
+ assert.equal(post.fans[1].isInit('name'), true);
+ assert.equal(post.fans[0].isInit('email'), false);
+ assert.equal(post.fans[1].isInit('email'), false);
+ assert.equal(post.fans[0].name, 'yup');
+ assert.equal(post.fans[1].name, 'use an object');
- assert.ok(Array.isArray(post.fans));
- assert.equal(post.fans.length, 2);
- assert.ok(post.fans[0] instanceof User);
- assert.ok(post.fans[1] instanceof User);
- assert.equal(post.fans[0].isInit('name'), true);
- assert.equal(post.fans[1].isInit('name'), true);
- assert.equal(post.fans[0].isInit('email'), false);
- assert.equal(post.fans[1].isInit('email'), false);
- assert.equal(post.fans[0].name, 'yup');
- assert.equal(post.fans[1].name, 'use an object');
- done();
- });
});
});
@@ -2391,13 +1892,13 @@ describe('model: populate:', function() {
let B, User;
let user1, user2, post1, post2, _id;
- beforeEach(function(done) {
+ beforeEach(async function() {
B = db.model('BlogPost', blogPostSchema);
User = db.model('User', userSchema);
_id = new mongoose.Types.ObjectId();
- User.create({
+ const [u1, u2] = await User.create({
name: 'Phoenix',
email: 'phx@az.com',
blogposts: [_id]
@@ -2405,32 +1906,28 @@ describe('model: populate:', function() {
name: 'Newark',
email: 'ewr@nj.com',
blogposts: [_id]
- }, function(err, u1, u2) {
- assert.ifError(err);
+ });
- user1 = u1;
- user2 = u2;
+ user1 = u1;
+ user2 = u2;
- B.create({
- title: 'the how and why',
- _creator: user1,
- fans: [user1, user2]
- }, {
- title: 'green eggs and ham',
- _creator: user2,
- fans: [user2, user1]
- }, function(err, p1, p2) {
- assert.ifError(err);
- post1 = p1;
- post2 = p2;
- done();
- });
+ const [p1, p2] = await B.create({
+ title: 'the how and why',
+ _creator: user1,
+ fans: [user1, user2]
+ }, {
+ title: 'green eggs and ham',
+ _creator: user2,
+ fans: [user2, user1]
});
+ post1 = p1;
+ post2 = p2;
+
});
it('returns a promise', function(done) {
const p = B.populate(post1, '_creator');
- assert.ok(p instanceof mongoose.Promise);
+ assert.ok(p instanceof Promise);
p.then(success, done);
function success(doc) {
assert.ok(doc);
@@ -2438,126 +1935,104 @@ describe('model: populate:', function() {
}
});
- it('of individual document works', function(done) {
- B.findById(post1._id, function(error, post1) {
- const ret = utils.populate({ path: '_creator', model: User });
- B.populate(post1, ret, function(err, post) {
- assert.ifError(err);
- assert.ok(post);
- assert.ok(post._creator instanceof User);
- assert.equal(post._creator.name, 'Phoenix');
- done();
- });
- });
+ it('of individual document works', async function() {
+ post1 = await B.findById(post1._id);
+ const ret = utils.populate({ path: '_creator', model: User });
+ const post = await B.populate(post1, ret);
+ assert.ok(post);
+ assert.ok(post._creator instanceof User);
+ assert.equal(post._creator.name, 'Phoenix');
+
});
describe('a document already populated', function() {
describe('when paths are not modified', function() {
- it('works', function(done) {
+ it('works', async function() {
db.deleteModel(/User/);
const User = db.model('User', userSchema);
- B.findById(post1._id, function(err, doc) {
- assert.ifError(err);
- B.populate(doc, [{ path: '_creator', model: 'User' }, { path: 'fans', model: 'User' }], function(err, post) {
- assert.ifError(err);
- assert.ok(post);
- assert.ok(post._creator instanceof User);
- assert.equal('Phoenix', post._creator.name);
- assert.equal(post.fans.length, 2);
- assert.equal(post.fans[0].name, user1.name);
- assert.equal(post.fans[1].name, user2.name);
-
- assert.equal(String(post._creator._id), String(post.populated('_creator')));
- assert.ok(Array.isArray(post.populated('fans')));
-
- B.populate(doc, [{ path: '_creator', model: 'User' }, { path: 'fans', model: 'User' }], function(err, post) {
- assert.ifError(err);
- assert.ok(post);
- assert.ok(post._creator instanceof User);
- assert.equal(post._creator.name, 'Phoenix');
- assert.equal(post.fans.length, 2);
- assert.equal(post.fans[0].name, user1.name);
- assert.equal(post.fans[1].name, user2.name);
- assert.ok(Array.isArray(post.populated('fans')));
- assert.equal(String(post.fans[0]._id), String(post.populated('fans')[0]));
- assert.equal(String(post.fans[1]._id), String(post.populated('fans')[1]));
-
- done();
- });
- });
- });
+ const doc = await B.findById(post1._id);
+ let post = await B.populate(doc, [{ path: '_creator', model: 'User' }, { path: 'fans', model: 'User' }]);
+ assert.ok(post);
+ assert.ok(post._creator instanceof User);
+ assert.equal('Phoenix', post._creator.name);
+ assert.equal(post.fans.length, 2);
+ assert.equal(post.fans[0].name, user1.name);
+ assert.equal(post.fans[1].name, user2.name);
+
+ assert.equal(String(post._creator._id), String(post.populated('_creator')));
+ assert.ok(Array.isArray(post.populated('fans')));
+
+ post = await B.populate(doc, [{ path: '_creator', model: 'User' }, { path: 'fans', model: 'User' }]);
+ assert.ok(post);
+ assert.ok(post._creator instanceof User);
+ assert.equal(post._creator.name, 'Phoenix');
+ assert.equal(post.fans.length, 2);
+ assert.equal(post.fans[0].name, user1.name);
+ assert.equal(post.fans[1].name, user2.name);
+ assert.ok(Array.isArray(post.populated('fans')));
+ assert.equal(String(post.fans[0]._id), String(post.populated('fans')[0]));
+ assert.equal(String(post.fans[1]._id), String(post.populated('fans')[1]));
+
+
});
});
describe('when paths are modified', function() {
- it('works', function(done) {
+ it('works', async function() {
db.deleteModel(/User/);
const User = db.model('User', userSchema);
- B.findById(post1._id, function(err, doc) {
- assert.ifError(err);
- B.populate(doc, [{ path: '_creator', model: 'User' }, { path: 'fans', model: 'User' }], function(err, post) {
- assert.ifError(err);
- assert.ok(post);
- assert.ok(post._creator instanceof User);
- assert.equal(post._creator.name, 'Phoenix');
- assert.equal(post.fans.length, 2);
- assert.equal(post.fans[0].name, user1.name);
- assert.equal(post.fans[1].name, user2.name);
-
- assert.equal(String(post._creator._id), String(post.populated('_creator')));
- assert.ok(Array.isArray(post.populated('fans')));
-
- // modify the paths
- doc.markModified('_creator');
- doc.markModified('fans');
-
- B.populate(doc, [{ path: '_creator', model: 'User' }, { path: 'fans', model: 'User' }], function(err, post) {
- assert.ifError(err);
- assert.ok(post);
- assert.ok(post._creator instanceof User);
- assert.equal(post._creator.name, 'Phoenix');
- assert.equal(post.fans.length, 2);
- assert.equal(post.fans[0].name, user1.name);
- assert.equal(post.fans[1].name, user2.name);
- assert.ok(Array.isArray(post.populated('fans')));
- assert.equal(
- String(post.fans[0]._id)
- , String(post.populated('fans')[0]));
- assert.equal(
- String(post.fans[1]._id)
- , String(post.populated('fans')[1]));
-
- done();
- });
- });
- });
+ const doc = await B.findById(post1._id);
+ let post = await B.populate(doc, [{ path: '_creator', model: 'User' }, { path: 'fans', model: 'User' }]);
+ assert.ok(post);
+ assert.ok(post._creator instanceof User);
+ assert.equal(post._creator.name, 'Phoenix');
+ assert.equal(post.fans.length, 2);
+ assert.equal(post.fans[0].name, user1.name);
+ assert.equal(post.fans[1].name, user2.name);
+
+ assert.equal(String(post._creator._id), String(post.populated('_creator')));
+ assert.ok(Array.isArray(post.populated('fans')));
+
+ // modify the paths
+ doc.markModified('_creator');
+ doc.markModified('fans');
+
+ post = await B.populate(doc, [{ path: '_creator', model: 'User' }, { path: 'fans', model: 'User' }]);
+ assert.ok(post);
+ assert.ok(post._creator instanceof User);
+ assert.equal(post._creator.name, 'Phoenix');
+ assert.equal(post.fans.length, 2);
+ assert.equal(post.fans[0].name, user1.name);
+ assert.equal(post.fans[1].name, user2.name);
+ assert.ok(Array.isArray(post.populated('fans')));
+ assert.equal(
+ String(post.fans[0]._id)
+ , String(post.populated('fans')[0]));
+ assert.equal(
+ String(post.fans[1]._id)
+ , String(post.populated('fans')[1]));
+
});
});
});
describe('of multiple documents', function() {
- it('works', function(done) {
+ it('works', async function() {
db.model('User', userSchema);
- B.findById(post1._id, function(error, post1) {
- assert.ifError(error);
- B.findById(post2._id, function(error, post2) {
- assert.ifError(error);
- const ret = utils.populate({ path: '_creator', model: 'User' });
- B.populate([post1, post2], ret, function(err, posts) {
- assert.ifError(err);
- assert.ok(posts);
- assert.equal(posts.length, 2);
- const p1 = posts[0];
- const p2 = posts[1];
- assert.ok(p1._creator instanceof User);
- assert.equal(p1._creator.name, 'Phoenix');
- assert.ok(p2._creator instanceof User);
- assert.equal(p2._creator.name, 'Newark');
- done();
- });
- });
- });
+ post1 = await B.findById(post1._id);
+ post2 = await B.findById(post2._id);
+ const ret = utils.populate({ path: '_creator', model: 'User' });
+ const posts = await B.populate([post1, post2], ret);
+ assert.ok(posts);
+ assert.equal(posts.length, 2);
+ const p1 = posts[0];
+ const p2 = posts[1];
+ assert.ok(p1._creator instanceof User);
+ assert.equal(p1._creator.name, 'Phoenix');
+ assert.ok(p2._creator instanceof User);
+ assert.equal(p2._creator.name, 'Newark');
+
});
});
});
@@ -2589,52 +2064,42 @@ describe('model: populate:', function() {
assert.equal(typeof post._creator.update, 'undefined');
});
- it('with find', function(done) {
+ it('with find', async function() {
const BlogPost = db.model('BlogPost', blogPostSchema);
const User = db.model('User', userSchema);
- User.create({
+ const [fan1, fan2] = await User.create([{
name: 'Fan 1',
email: 'fan1@learnboost.com'
}, {
name: 'Fan 2',
email: 'fan2@learnboost.com'
- }, function(err, fan1, fan2) {
- assert.ifError(err);
-
- BlogPost.create({
- title: 'Woot',
- fans: [fan1, fan2]
- }, {
- title: 'Woot2',
- fans: [fan2, fan1]
- }, function(err, post1, post2) {
- assert.ifError(err);
-
- BlogPost
- .find({ _id: { $in: [post1._id, post2._id] } })
- .populate('fans')
- .lean()
- .exec(function(err, blogposts) {
- assert.ifError(err);
-
- assert.equal(blogposts[0].fans[0].name, 'Fan 1');
- assert.equal(blogposts[0].fans[0].email, 'fan1@learnboost.com');
- assert.equal(typeof blogposts[0].fans[0].update, 'undefined');
- assert.equal(blogposts[0].fans[1].name, 'Fan 2');
- assert.equal(blogposts[0].fans[1].email, 'fan2@learnboost.com');
- assert.equal(typeof blogposts[0].fans[1].update, 'undefined');
-
- assert.equal(blogposts[1].fans[0].name, 'Fan 2');
- assert.equal(blogposts[1].fans[0].email, 'fan2@learnboost.com');
- assert.equal(typeof blogposts[1].fans[0].update, 'undefined');
- assert.equal(blogposts[1].fans[1].name, 'Fan 1');
- assert.equal(blogposts[1].fans[1].email, 'fan1@learnboost.com');
- assert.equal(typeof blogposts[1].fans[1].update, 'undefined');
- done();
- });
- });
+ }]);
+ const [post1, post2] = await BlogPost.create({
+ title: 'Woot',
+ fans: [fan1, fan2]
+ }, {
+ title: 'Woot2',
+ fans: [fan2, fan1]
});
+
+ const blogposts = await BlogPost
+ .find({ _id: { $in: [post1._id, post2._id] } })
+ .populate('fans')
+ .lean();
+ assert.equal(blogposts[0].fans[0].name, 'Fan 1');
+ assert.equal(blogposts[0].fans[0].email, 'fan1@learnboost.com');
+ assert.equal(typeof blogposts[0].fans[0].update, 'undefined');
+ assert.equal(blogposts[0].fans[1].name, 'Fan 2');
+ assert.equal(blogposts[0].fans[1].email, 'fan2@learnboost.com');
+ assert.equal(typeof blogposts[0].fans[1].update, 'undefined');
+
+ assert.equal(blogposts[1].fans[0].name, 'Fan 2');
+ assert.equal(blogposts[1].fans[0].email, 'fan2@learnboost.com');
+ assert.equal(typeof blogposts[1].fans[0].update, 'undefined');
+ assert.equal(blogposts[1].fans[1].name, 'Fan 1');
+ assert.equal(blogposts[1].fans[1].email, 'fan1@learnboost.com');
+ assert.equal(typeof blogposts[1].fans[1].update, 'undefined');
});
});
@@ -2732,91 +2197,87 @@ describe('model: populate:', function() {
});
describe('in a subdocument', function() {
- it('works', function(done) {
- U.find({ name: 'u1' }).populate('comments', { _id: 0 }).exec(function(err, docs) {
- assert.ifError(err);
-
- const doc = docs[0];
- assert.ok(Array.isArray(doc.comments), 'comments should be an array: ' + JSON.stringify(doc));
- assert.equal(doc.comments.length, 2, 'invalid comments length for ' + JSON.stringify(doc));
- doc.comments.forEach(function(d) {
- assert.equal(d._id, undefined);
- assert.equal(Object.keys(d._doc).indexOf('_id'), -1);
- assert.ok(d.body.length);
- assert.equal(typeof d._doc.__v, 'number');
- });
+ it('works (gh-14231)', async function() {
+ const docs = await U.find({ name: 'u1' }).populate('comments', { _id: 0 });
- U.findOne({ name: 'u1' }).populate('comments', 'title -_id').exec(function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.comments.length, 2);
- doc.comments.forEach(function(d) {
- assert.equal(d._id, undefined);
- assert.equal(Object.keys(d._doc).indexOf('_id'), -1);
- assert.ok(d.title.length);
- assert.equal(d.body, undefined);
- assert.equal(typeof d._doc.__v, 'undefined');
- });
- U.findOne({ name: 'u1' }).populate('comments', '-_id').exec(function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.comments.length, 2);
- doc.comments.forEach(function(d) {
- assert.equal(d._id, undefined);
- assert.equal(Object.keys(d._doc).indexOf('_id'), -1);
- assert.ok(d.title.length);
- assert.ok(d.body.length);
- assert.equal(typeof d._doc.__v, 'number');
- });
- done();
- });
- });
+ let doc = docs[0];
+ assert.ok(Array.isArray(doc.comments), 'comments should be an array: ' + JSON.stringify(doc));
+ assert.equal(doc.comments.length, 2, 'invalid comments length for ' + JSON.stringify(doc));
+ doc.comments.forEach(function(d) {
+ assert.equal(d._id, undefined);
+ assert.equal(Object.keys(d._doc).indexOf('_id'), -1);
+ assert.ok(d.body.length);
+ assert.equal(typeof d._doc.__v, 'number');
+ });
+
+ doc = await U.findOne({ name: 'u1' }).populate('comments', 'title -_id');
+ assert.equal(doc.comments.length, 2);
+ doc.comments.forEach(function(d) {
+ assert.equal(d._id, undefined);
+ assert.equal(Object.keys(d._doc).indexOf('_id'), -1);
+ assert.ok(d.title.length);
+ assert.equal(d.body, undefined);
+ assert.equal(typeof d._doc.__v, 'undefined');
+ });
+ doc = await U.findOne({ name: 'u1' }).populate('comments', '-_id');
+ assert.equal(doc.comments.length, 2);
+ doc.comments.forEach(function(d) {
+ assert.equal(d._id, undefined);
+ assert.equal(Object.keys(d._doc).indexOf('_id'), -1);
+ assert.ok(d.title.length);
+ assert.ok(d.body.length);
+ assert.equal(typeof d._doc.__v, 'number');
+ });
+
+ doc = await U.findOne({ name: 'u1' }).populate('comments', ['-_id']);
+ assert.equal(doc.comments.length, 2);
+ doc.comments.forEach(function(d) {
+ assert.equal(d._id, undefined);
+ assert.equal(Object.keys(d._doc).indexOf('_id'), -1);
+ assert.ok(d.title.length);
+ assert.ok(d.body.length);
+ assert.equal(typeof d._doc.__v, 'number');
});
});
- it('with lean', function(done) {
- U.find({ name: 'u1' }).lean().populate({ path: 'comments', select: { _id: 0 }, options: { lean: true } }).exec(function(err, docs) {
- assert.ifError(err);
+ it('with lean', async function() {
+ const docs = await U.find({ name: 'u1' }).lean().populate({ path: 'comments', select: { _id: 0 }, options: { lean: true } });
- const doc = docs[0];
- assert.equal(doc.comments.length, 2);
- doc.comments.forEach(function(d) {
- assert.ok(!('_id' in d));
- assert.ok(d.body.length);
- assert.equal(typeof d.__v, 'number');
- });
+ let doc = docs[0];
+ assert.equal(doc.comments.length, 2);
+ doc.comments.forEach(function(d) {
+ assert.ok(!('_id' in d));
+ assert.ok(d.body.length);
+ assert.equal(typeof d.__v, 'number');
+ });
- U.findOne({ name: 'u1' }).lean().populate('comments', '-_id', null, { lean: true }).exec(function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.comments.length, 2);
- doc.comments.forEach(function(d) {
- assert.ok(!('_id' in d));
- assert.ok(d.body.length);
- assert.equal(typeof d.__v, 'number');
- });
- done();
- });
+ doc = await U.findOne({ name: 'u1' }).lean().populate('comments', '-_id', null, { lean: true });
+ assert.equal(doc.comments.length, 2);
+ doc.comments.forEach(function(d) {
+ assert.ok(!('_id' in d));
+ assert.ok(d.body.length);
+ assert.equal(typeof d.__v, 'number');
});
+
});
});
describe('of documents being populated', function() {
- it('still works (gh-1441)', function(done) {
- U.find()
+ it('still works (gh-1441)', async function() {
+ const docs = await U.find()
.select('-_id comment name')
- .populate('comment', { _id: 0 }).exec(function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 2);
-
- docs.forEach(function(doc) {
- assert.ok(doc.comment && doc.comment.body);
- if (doc.name === 'u1') {
- assert.equal(doc.comment.body, 'comment 1');
- } else {
- assert.equal(doc.comment.body, 'comment 2');
- }
- });
+ .populate('comment', { _id: 0 });
+ assert.equal(docs.length, 2);
+
+ docs.forEach(function(doc) {
+ assert.ok(doc.comment && doc.comment.body);
+ if (doc.name === 'u1') {
+ assert.equal(doc.comment.body, 'comment 1');
+ } else {
+ assert.equal(doc.comment.body, 'comment 2');
+ }
+ });
- done();
- });
});
});
});
@@ -2882,29 +2343,24 @@ describe('model: populate:', function() {
await Review.create(review);
});
- it('Simple populate', function(done) {
- Review.find({}).populate('item.id').exec(function(err, results) {
- assert.ifError(err);
- assert.equal(results.length, 1);
- const result = results[0];
- assert.equal(result.item.id.name, 'Val');
- done();
- });
+ it('Simple populate', async function() {
+ const results = await Review.find({}).populate('item.id');
+ assert.equal(results.length, 1);
+ const result = results[0];
+ assert.equal(result.item.id.name, 'Val');
+
});
- it('Array populate', function(done) {
- Review.find({}).populate('items.id').exec(function(err, results) {
- assert.ifError(err);
- assert.equal(results.length, 1);
- const result = results[0];
- assert.equal(result.items.length, 2);
- assert.equal(result.items[0].id.name, 'Val');
- assert.equal(result.items[1].id.otherName, 'Val');
- done();
- });
+ it('Array populate', async function() {
+ const results = await Review.find({}).populate('items.id');
+ assert.equal(results.length, 1);
+ const result = results[0];
+ assert.equal(result.items.length, 2);
+ assert.equal(result.items[0].id.name, 'Val');
+ assert.equal(result.items[1].id.otherName, 'Val');
});
- it('with nonexistant refPath (gh-4637)', function(done) {
+ it('with nonexistant refPath (gh-4637)', function() {
db.deleteModel(/Test/);
const baseballSchema = mongoose.Schema({
seam: String
@@ -2925,7 +2381,7 @@ describe('model: populate:', function() {
});
const Basket = db.model('Test2', basketSchema);
- new Baseball({ seam: 'yarn' }).
+ return new Baseball({ seam: 'yarn' }).
save().
then(function(baseball) {
return new Basket({
@@ -2948,12 +2404,10 @@ describe('model: populate:', function() {
assert.equal(basket.balls[0].ball.seam, 'yarn');
assert.ok(!basket.balls[1].kind);
assert.ok(!basket.balls[1].ball);
- done();
- }).
- catch(done);
+ });
});
- it('array with empty refPath (gh-5377)', function(done) {
+ it('array with empty refPath (gh-5377)', async function() {
db.deleteModel(/Test/);
const modelASchema = new mongoose.Schema({
name: String
@@ -2984,42 +2438,31 @@ describe('model: populate:', function() {
});
const Parent = db.model('Test', ParentSchema);
- ModelA.create({ name: 'model-A' }, function(error, toyA) {
- assert.ifError(error);
- ModelB.create({ name: 'model-B' }, function(error, toyB) {
- assert.ifError(error);
- Parent.create({
- children: [
- {
- name: 'Child 1',
- toy: { kind: 'Test1', value: toyA._id }
- },
- {
- name: 'Child 2'
- },
- {
- name: 'Child 3',
- toy: { kind: 'Test2', value: toyB._id }
- }
- ]
- }, function(error, doc) {
- assert.ifError(error);
- test(doc._id);
- });
- });
+ const toyA = await ModelA.create({ name: 'model-A' });
+ const toyB = await ModelB.create({ name: 'model-B' });
+ let doc = await Parent.create({
+ children: [
+ {
+ name: 'Child 1',
+ toy: { kind: 'Test1', value: toyA._id }
+ },
+ {
+ name: 'Child 2'
+ },
+ {
+ name: 'Child 3',
+ toy: { kind: 'Test2', value: toyB._id }
+ }
+ ]
});
- function test(id) {
- Parent.findById(id, function(error, doc) {
- assert.ifError(error);
- doc.populate('children.toy.value').then(function(doc) {
- assert.equal(doc.children[0].toy.value.name, 'model-A');
- assert.equal(doc.children[1].toy.value, null);
- assert.equal(doc.children[2].toy.value.name, 'model-B');
- done();
- }).catch(done);
- });
- }
+
+ doc = await Parent.findById(doc._id);
+ await doc.populate('children.toy.value');
+ assert.equal(doc.children[0].toy.value.name, 'model-A');
+ assert.equal(doc.children[1].toy.value, null);
+ assert.equal(doc.children[2].toy.value.name, 'model-B');
+
});
it('with non-arrays (gh-5114)', function(done) {
@@ -3139,26 +2582,111 @@ describe('model: populate:', function() {
assert.strictEqual(doc.parts[0].contents[1].item.url, 'https://youtube.com');
});
- it('with nested nonexistant refPath (gh-6457)', async function() {
- const CommentSchema = new Schema({
- text: String,
- references: {
- type: [{
- item: {
- type: Schema.Types.ObjectId,
- refPath: 'comments.references.kind'
- },
- kind: String
- }]
- }
- });
-
- const PostSchema = new Schema({
- text: String,
- comments: [CommentSchema]
- });
-
- const Post = db.model('Test', PostSchema);
+ it('with refPath and array of ids with parent refPath', async function() {
+ const Child = db.model(
+ 'Child',
+ new mongoose.Schema({
+ fetched: Boolean
+ })
+ );
+
+ const Parent = db.model(
+ 'Parent',
+ new mongoose.Schema({
+ docArray: [
+ {
+ type: {
+ type: String,
+ enum: ['Child', 'OtherModel']
+ },
+ ids: [
+ {
+ type: mongoose.Schema.ObjectId,
+ refPath: 'docArray.type'
+ }
+ ]
+ }
+ ]
+ })
+ );
+ await Child.insertMany([
+ { _id: new mongoose.Types.ObjectId('6671a008596112f0729c2045'), fetched: true },
+ { _id: new mongoose.Types.ObjectId('667195f3596112f0728abe24'), fetched: true },
+ { _id: new mongoose.Types.ObjectId('6671bd39596112f072cda69c'), fetched: true },
+ { _id: new mongoose.Types.ObjectId('6672c351596112f072868565'), fetched: true },
+ { _id: new mongoose.Types.ObjectId('66734edd596112f0727304a2'), fetched: true },
+ { _id: new mongoose.Types.ObjectId('66726eff596112f072f8e834'), fetched: true },
+ { _id: new mongoose.Types.ObjectId('667267ff596112f072ed56b1'), fetched: true }
+ ]);
+ const { _id } = await Parent.create(
+ {
+ docArray: [
+ {},
+ {
+ type: 'Child',
+ ids: [
+ new mongoose.Types.ObjectId('6671a008596112f0729c2045'),
+ new mongoose.Types.ObjectId('667195f3596112f0728abe24'),
+ new mongoose.Types.ObjectId('6671bd39596112f072cda69c'),
+ new mongoose.Types.ObjectId('6672c351596112f072868565')
+ ]
+ },
+ {
+ type: 'Child',
+ ids: [new mongoose.Types.ObjectId('66734edd596112f0727304a2')]
+ },
+ {},
+ {
+ type: 'Child',
+ ids: [new mongoose.Types.ObjectId('66726eff596112f072f8e834')]
+ },
+ {},
+ {
+ type: 'Child',
+ ids: [new mongoose.Types.ObjectId('667267ff596112f072ed56b1')]
+ }
+ ]
+ }
+ );
+
+ const doc = await Parent.findById(_id).populate('docArray.ids').orFail();
+ assert.strictEqual(doc.docArray.length, 7);
+ assert.strictEqual(doc.docArray[0].ids.length, 0);
+ assert.strictEqual(doc.docArray[1].ids.length, 4);
+ assert.ok(doc.docArray[1].ids[0].fetched);
+ assert.ok(doc.docArray[1].ids[1].fetched);
+ assert.ok(doc.docArray[1].ids[2].fetched);
+ assert.ok(doc.docArray[1].ids[3].fetched);
+ assert.strictEqual(doc.docArray[2].ids.length, 1);
+ assert.ok(doc.docArray[2].ids[0].fetched);
+ assert.strictEqual(doc.docArray[3].ids.length, 0);
+ assert.strictEqual(doc.docArray[4].ids.length, 1);
+ assert.ok(doc.docArray[4].ids[0].fetched);
+ assert.strictEqual(doc.docArray[5].ids.length, 0);
+ assert.strictEqual(doc.docArray[6].ids.length, 1);
+ assert.ok(doc.docArray[6].ids[0].fetched);
+ });
+
+ it('with nested nonexistant refPath (gh-6457)', async function() {
+ const CommentSchema = new Schema({
+ text: String,
+ references: {
+ type: [{
+ item: {
+ type: Schema.Types.ObjectId,
+ refPath: 'comments.references.kind'
+ },
+ kind: String
+ }]
+ }
+ });
+
+ const PostSchema = new Schema({
+ text: String,
+ comments: [CommentSchema]
+ });
+
+ const Post = db.model('Test', PostSchema);
await Post.create({
@@ -3309,98 +2837,63 @@ describe('model: populate:', function() {
Litter = db.model('Test', litterSchema);
});
- it('when saving new docs', function(done) {
- Cat.create({ name: 'new1' }, { name: 'new2' }, { name: 'new3' }, function(err, a, b, c) {
- if (err) {
- return done(err);
- }
+ it('when saving new docs', async function() {
+ const [a, b, c] = await Cat.create([{ name: 'new1' }, { name: 'new2' }, { name: 'new3' }]);
- Litter.create({
- name: 'new',
- cats: [a],
- o: b,
- a: [c]
- }, confirm(done));
+ await Litter.create({
+ name: 'new',
+ cats: [a],
+ o: b,
+ a: [c]
});
});
- it('when saving existing docs 5T5', function(done) {
- Cat.create({ name: 'ex1' }, { name: 'ex2' }, { name: 'ex3' }, function(err, a, b, c) {
- if (err) {
- return done(err);
- }
+ it('when saving existing docs 5T5', async function() {
+ const [a, b, c] = await Cat.create([{ name: 'ex1' }, { name: 'ex2' }, { name: 'ex3' }]);
- Litter.create({ name: 'existing' }, function(err, doc) {
- doc.cats = [a];
- doc.o = b;
- doc.a = [c];
- doc.save(confirm(done));
- });
- });
+ const doc = await Litter.create({ name: 'existing' });
+ doc.cats = [a];
+ doc.o = b;
+ doc.a = [c];
+ await doc.save();
});
-
- function confirm(done) {
- return function(err, litter) {
- if (err) {
- return done(err);
- }
- Litter.findById(litter).lean().exec(function(err, doc) {
- if (err) {
- return done(err);
- }
- assert.ok(doc.o._id);
- assert.ok(doc.cats[0]);
- assert.ok(doc.cats[0]._id);
- assert.ok(doc.a[0]);
- assert.ok(doc.a[0]._id);
- done();
- });
- };
- }
});
describe('github issues', function() {
- it('populating an array of refs, slicing, and fetching many (gh-5737)', function(done) {
+ it('populating an array of refs, slicing, and fetching many (gh-5737)', async function() {
const BlogPost = db.model('BlogPost', new Schema({
title: String,
fans: [{ type: ObjectId, ref: 'User' }]
}));
const User = db.model('User', new Schema({ name: String }));
- User.create([{ name: 'Fan 1' }, { name: 'Fan 2' }], function(error, fans) {
- assert.ifError(error);
- const posts = [
- { title: 'Test 1', fans: [fans[0]._id, fans[1]._id] },
- { title: 'Test 2', fans: [fans[1]._id, fans[0]._id] }
- ];
- BlogPost.create(posts, function(error) {
- assert.ifError(error);
- BlogPost.
- find({}).
- slice('fans', [0, 5]).
- populate('fans').
- exec(function(err, blogposts) {
- assert.ifError(err);
-
- const titles = blogposts.map(bp => bp.title).sort();
-
- assert.equal(titles[0], 'Test 1');
- assert.equal(titles[1], 'Test 2');
-
- const test1 = blogposts.find(bp => bp.title === 'Test 1');
- assert.equal(test1.fans[0].name, 'Fan 1');
- assert.equal(test1.fans[1].name, 'Fan 2');
-
- const test2 = blogposts.find(bp => bp.title === 'Test 2');
- assert.equal(test2.fans[0].name, 'Fan 2');
- assert.equal(test2.fans[1].name, 'Fan 1');
- done();
- });
- });
- });
+ const fans = await User.create([{ name: 'Fan 1' }, { name: 'Fan 2' }]);
+ const posts = [
+ { title: 'Test 1', fans: [fans[0]._id, fans[1]._id] },
+ { title: 'Test 2', fans: [fans[1]._id, fans[0]._id] }
+ ];
+ await BlogPost.create(posts);
+ const blogposts = await BlogPost.
+ find({}).
+ slice('fans', [0, 5]).
+ populate('fans');
+
+ const titles = blogposts.map(bp => bp.title).sort();
+
+ assert.equal(titles[0], 'Test 1');
+ assert.equal(titles[1], 'Test 2');
+
+ const test1 = blogposts.find(bp => bp.title === 'Test 1');
+ assert.equal(test1.fans[0].name, 'Fan 1');
+ assert.equal(test1.fans[1].name, 'Fan 2');
+
+ const test2 = blogposts.find(bp => bp.title === 'Test 2');
+ assert.equal(test2.fans[0].name, 'Fan 2');
+ assert.equal(test2.fans[1].name, 'Fan 1');
+
});
- it('populate + slice (gh-5737a)', function(done) {
+ it('populate + slice (gh-5737a)', async function() {
const BlogPost = db.model('BlogPost', new Schema({
title: String,
user: { type: ObjectId, ref: 'User' },
@@ -3408,26 +2901,40 @@ describe('model: populate:', function() {
}));
const User = db.model('User', new Schema({ name: String }));
- User.create([{ name: 'Fan 1' }], function(error, fans) {
- assert.ifError(error);
- const posts = [
- { title: 'Test 1', user: fans[0]._id, fans: [fans[0]._id] }
- ];
- BlogPost.create(posts, function(error) {
- assert.ifError(error);
- BlogPost.
- find({}).
- slice('fans', [0, 2]).
- populate('user').
- exec(function(err, blogposts) {
- assert.ifError(error);
-
- assert.equal(blogposts[0].user.name, 'Fan 1');
- assert.equal(blogposts[0].title, 'Test 1');
- done();
- });
- });
- });
+ const fans = await User.create([{ name: 'Fan 1' }]);
+ const posts = [
+ { title: 'Test 1', user: fans[0]._id, fans: [fans[0]._id] }
+ ];
+ await BlogPost.create(posts);
+ const blogposts = await BlogPost.
+ find({}).
+ slice('fans', [0, 2]).
+ populate('user');
+
+ assert.equal(blogposts[0].user.name, 'Fan 1');
+ assert.equal(blogposts[0].title, 'Test 1');
+ });
+
+ it('handles multiple spaces in between paths to populate (gh-13951)', async function() {
+ const BlogPost = db.model('BlogPost', new Schema({
+ title: String,
+ user: { type: ObjectId, ref: 'User' },
+ fans: [{ type: ObjectId, ref: 'User' }]
+ }));
+ const User = db.model('User', new Schema({ name: String }));
+
+ const fans = await User.create([{ name: 'Fan 1' }]);
+ const posts = [
+ { title: 'Test 1', user: fans[0]._id, fans: [fans[0]._id] }
+ ];
+ await BlogPost.create(posts);
+ const blogPost = await BlogPost.
+ findOne({ title: 'Test 1' }).
+ populate('user \t fans');
+
+ assert.equal(blogPost.user.name, 'Fan 1');
+ assert.equal(blogPost.fans[0].name, 'Fan 1');
+ assert.equal(blogPost.title, 'Test 1');
});
it('maps results back to correct document (gh-1444)', async function() {
@@ -3539,7 +3046,7 @@ describe('model: populate:', function() {
assert.notEqual(err.message.indexOf('subproperty of a document array'), -1);
});
- it('handles toObject() (gh-3279)', function(done) {
+ it('handles toObject() (gh-3279)', async function() {
const teamSchema = new Schema({
members: [{
user: { type: ObjectId, ref: 'User' },
@@ -3554,37 +3061,28 @@ describe('model: populate:', function() {
return ret;
}
});
-
-
const Team = db.model('Test', teamSchema);
const userSchema = new Schema({
username: String
});
-
userSchema.set('toJSON', {
transform: function(doc, ret) {
return ret;
}
});
-
const User = db.model('User', userSchema);
const user = new User({ username: 'Test' });
+ await user.save();
- user.save(function(err) {
- assert.ifError(err);
- const team = new Team({ members: [{ user: user }] });
+ const team = new Team({ members: [{ user: user }] });
+ await team.save();
+ await team.populate('members.user');
- team.save(function(err) {
- assert.ifError(err);
- team.populate('members.user', function() {
- team.toJSON();
- assert.equal(calls, 1);
- done();
- });
- });
- });
+ assert.equal(calls, 0);
+ team.toJSON();
+ assert.equal(calls, 1);
});
it('populate option (gh-2321)', async function() {
@@ -4008,7 +3506,7 @@ describe('model: populate:', function() {
catch(done);
});
- it('empty array (gh-4284)', function(done) {
+ it('empty array (gh-4284)', async function() {
const PersonSchema = new Schema({
name: { type: String }
});
@@ -4022,19 +3520,15 @@ describe('model: populate:', function() {
const Person = db.model('Person', PersonSchema);
const Band = db.model('Test', BandSchema);
- const band = { people: [new mongoose.Types.ObjectId()] };
- Band.create(band, function(error, band) {
- assert.ifError(error);
- const opts = { path: 'people', model: Person };
- Band.findById(band).populate(opts).exec(function(error, band) {
- assert.ifError(error);
- assert.equal(band.people.length, 0);
- done();
- });
- });
+ let band = { people: [new mongoose.Types.ObjectId()] };
+ band = await Band.create(band);
+ const opts = { path: 'people', model: Person };
+ band = await Band.findById(band).populate(opts);
+ assert.equal(band.people.length, 0);
+
});
- it('empty populate string is a no-op (gh-4702)', function(done) {
+ it('empty populate string is a no-op (gh-4702)', async function() {
const BandSchema = new Schema({
people: [{
type: mongoose.Schema.Types.ObjectId
@@ -4043,18 +3537,14 @@ describe('model: populate:', function() {
const Band = db.model('Test', BandSchema);
- const band = { people: [new mongoose.Types.ObjectId()] };
- Band.create(band, function(error, band) {
- assert.ifError(error);
- Band.findById(band).populate('').exec(function(error, band) {
- assert.ifError(error);
- assert.equal(band.people.length, 1);
- done();
- });
- });
+ let band = { people: [new mongoose.Types.ObjectId()] };
+ band = await Band.create(band);
+ band = await Band.findById(band).populate('');
+ assert.equal(band.people.length, 1);
+
});
- it('checks field name correctly with nested arrays (gh-4365)', function(done) {
+ it('checks field name correctly with nested arrays (gh-4365)', async function() {
const UserSchema = new mongoose.Schema({
name: {
type: String,
@@ -4077,34 +3567,25 @@ describe('model: populate:', function() {
});
const OrganizationModel = db.model('Test', OrganizationSchema);
- const org = {
+ let org = {
members: [],
groups: []
};
- OrganizationModel.create(org, function(error) {
- assert.ifError(error);
- OrganizationModel.
- findOne({}).
- populate('members', 'name').
- exec(function(error, org) {
- assert.ifError(error);
- org.groups.push({ name: 'Team Rocket' });
- org.save(function(error) {
- assert.ifError(error);
- org.groups[0].members.push('Jessie');
- assert.equal(org.groups[0].members[0], 'Jessie');
- org.save(function(error) {
- assert.ifError(error);
- assert.equal(org.groups[0].members[0], 'Jessie');
- done();
- });
- });
- });
- });
+ await OrganizationModel.create(org);
+ org = await OrganizationModel.
+ findOne({}).
+ populate('members', 'name');
+ org.groups.push({ name: 'Team Rocket' });
+ await org.save();
+ org.groups[0].members.push('Jessie');
+ assert.equal(org.groups[0].members[0], 'Jessie');
+ await org.save();
+ assert.equal(org.groups[0].members[0], 'Jessie');
+
});
describe('populate virtuals (gh-2562)', function() {
- it('basic populate virtuals', function(done) {
+ it('basic populate virtuals', async function() {
const PersonSchema = new Schema({
name: String,
band: String
@@ -4125,18 +3606,12 @@ describe('model: populate:', function() {
const people = ['Axl Rose', 'Slash'].map(function(v) {
return { name: v, band: 'Guns N\' Roses' };
});
- Person.create(people, function(error) {
- assert.ifError(error);
- Band.create({ name: 'Guns N\' Roses' }, function(error) {
- assert.ifError(error);
- const query = { name: 'Guns N\' Roses' };
- Band.findOne(query).populate('members').exec(function(error, gnr) {
- assert.ifError(error);
- assert.equal(gnr.members.length, 2);
- done();
- });
- });
- });
+ await Person.create(people);
+ await Band.create({ name: 'Guns N\' Roses' });
+ const query = { name: 'Guns N\' Roses' };
+ const gnr = await Band.findOne(query).populate('members');
+ assert.equal(gnr.members.length, 2);
+
});
it('match (gh-6787)', async function() {
@@ -4166,7 +3641,66 @@ describe('model: populate:', function() {
assert.deepEqual(band.members.map(b => b.name).sort(), ['AA', 'AB']);
});
- it('multiple source docs', function(done) {
+ it('match prevents using $where', async function() {
+ const ParentSchema = new Schema({
+ name: String,
+ child: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: 'Child'
+ },
+ children: [{
+ type: mongoose.Schema.Types.ObjectId,
+ ref: 'Child'
+ }]
+ });
+
+ const ChildSchema = new Schema({
+ name: String
+ });
+ ChildSchema.virtual('parent', {
+ ref: 'Parent',
+ localField: '_id',
+ foreignField: 'parent'
+ });
+
+ const Parent = db.model('Parent', ParentSchema);
+ const Child = db.model('Child', ChildSchema);
+
+ const child = await Child.create({ name: 'Luke' });
+ const parent = await Parent.create({ name: 'Anakin', child: child._id });
+
+ await assert.rejects(
+ () => Parent.findOne().populate({ path: 'child', match: () => ({ $where: 'typeof console !== "undefined" ? doesNotExist("foo") : true;' }) }),
+ /Cannot use \$where filter with populate\(\) match/
+ );
+ await assert.rejects(
+ () => Parent.find().populate({ path: 'child', match: () => ({ $where: 'typeof console !== "undefined" ? doesNotExist("foo") : true;' }) }),
+ /Cannot use \$where filter with populate\(\) match/
+ );
+ await assert.rejects(
+ () => parent.populate({ path: 'child', match: () => ({ $where: 'typeof console !== "undefined" ? doesNotExist("foo") : true;' }) }),
+ /Cannot use \$where filter with populate\(\) match/
+ );
+ await assert.rejects(
+ () => Child.find().populate({ path: 'parent', match: () => ({ $where: 'typeof console !== "undefined" ? doesNotExist("foo") : true;' }) }),
+ /Cannot use \$where filter with populate\(\) match/
+ );
+ await assert.rejects(
+ () => Child.find().populate({ path: 'parent', match: () => ({ $or: [{ $where: 'typeof console !== "undefined" ? doesNotExist("foo") : true;' }] }) }),
+ /Cannot use \$where filter with populate\(\) match/
+ );
+ await assert.rejects(
+ () => Child.find().populate({ path: 'parent', match: () => ({ $and: [{ $where: 'typeof console !== "undefined" ? doesNotExist("foo") : true;' }] }) }),
+ /Cannot use \$where filter with populate\(\) match/
+ );
+
+ class MyClass {}
+ MyClass.prototype.$where = 'typeof console !== "undefined" ? doesNotExist("foo") : true;';
+ // OK because sift only looks through own properties
+ await Child.find().populate({ path: 'parent', match: () => new MyClass() });
+ });
+
+ it('multiple source docs', async function() {
const PersonSchema = new Schema({
name: String,
band: String
@@ -4190,35 +3724,28 @@ describe('model: populate:', function() {
people = people.concat(['Vince Neil', 'Nikki Sixx'].map(function(v) {
return { name: v, band: 'Motley Crue' };
}));
- Person.create(people, function(error) {
- assert.ifError(error);
- const bands = [
- { name: 'Guns N\' Roses' },
- { name: 'Motley Crue' }
- ];
- Band.create(bands, function(error) {
- assert.ifError(error);
- Band.
- find({}).
- sort({ name: 1 }).
- populate({ path: 'members', options: { sort: { name: 1 } } }).
- exec(function(error, bands) {
- assert.ifError(error);
-
- assert.equal(bands.length, 2);
- assert.equal(bands[0].name, 'Guns N\' Roses');
- assert.equal(bands[0].members.length, 2);
- assert.deepEqual(bands[0].members.map(v => v.name),
- ['Axl Rose', 'Slash']);
-
- assert.equal(bands[1].name, 'Motley Crue');
- assert.equal(bands[1].members.length, 2);
- assert.deepEqual(bands[1].members.map(v => v.name),
- ['Nikki Sixx', 'Vince Neil']);
- done();
- });
- });
- });
+ await Person.create(people);
+ let bands = [
+ { name: 'Guns N\' Roses' },
+ { name: 'Motley Crue' }
+ ];
+ await Band.create(bands);
+ bands = await Band.
+ find({}).
+ sort({ name: 1 }).
+ populate({ path: 'members', options: { sort: { name: 1 } } });
+
+ assert.equal(bands.length, 2);
+ assert.equal(bands[0].name, 'Guns N\' Roses');
+ assert.equal(bands[0].members.length, 2);
+ assert.deepEqual(bands[0].members.map(v => v.name),
+ ['Axl Rose', 'Slash']);
+
+ assert.equal(bands[1].name, 'Motley Crue');
+ assert.equal(bands[1].members.length, 2);
+ assert.deepEqual(bands[1].members.map(v => v.name),
+ ['Nikki Sixx', 'Vince Neil']);
+
});
it('catchable error if localField or foreignField not specified (gh-6767)', function() {
@@ -4236,7 +3763,7 @@ describe('model: populate:', function() {
});
});
- it('source array', function(done) {
+ it('source array', async function() {
const PersonSchema = new Schema({
name: String
});
@@ -4254,7 +3781,7 @@ describe('model: populate:', function() {
const Person = db.model('Person', PersonSchema);
const Band = db.model('Test', BandSchema);
- const bands = [
+ let bands = [
{ name: 'Guns N\' Roses', people: ['Axl Rose', 'Slash'] },
{ name: 'Motley Crue', people: ['Vince Neil', 'Nikki Sixx'] }
];
@@ -4265,35 +3792,28 @@ describe('model: populate:', function() {
{ name: 'Nikki Sixx' }
];
- Person.create(people, function(error) {
- assert.ifError(error);
- Band.insertMany(bands, function(error) {
- assert.ifError(error);
- Band.
- find({}).
- sort({ name: 1 }).
- populate({ path: 'members', options: { sort: { name: 1 } } }).
- exec(function(error, bands) {
- assert.ifError(error);
-
- assert.equal(bands.length, 2);
- assert.equal(bands[0].name, 'Guns N\' Roses');
- assert.equal(bands[0].members.length, 2);
- assert.deepEqual(bands[0].members.map(v => v.name),
- ['Axl Rose', 'Slash']);
-
- assert.equal(bands[1].name, 'Motley Crue');
- assert.equal(bands[1].members.length, 2);
- assert.deepEqual(bands[1].members.map(v => v.name),
- ['Nikki Sixx', 'Vince Neil']);
-
- done();
- });
- });
- });
+ await Person.create(people);
+ await Band.insertMany(bands);
+ bands = await Band.
+ find({}).
+ sort({ name: 1 }).
+ populate({ path: 'members', options: { sort: { name: 1 } } });
+
+ assert.equal(bands.length, 2);
+ assert.equal(bands[0].name, 'Guns N\' Roses');
+ assert.equal(bands[0].members.length, 2);
+ assert.deepEqual(bands[0].members.map(v => v.name),
+ ['Axl Rose', 'Slash']);
+
+ assert.equal(bands[1].name, 'Motley Crue');
+ assert.equal(bands[1].members.length, 2);
+ assert.deepEqual(bands[1].members.map(v => v.name),
+ ['Nikki Sixx', 'Vince Neil']);
+
+
});
- it('multiple paths (gh-4234)', function(done) {
+ it('multiple paths (gh-4234)', async function() {
const PersonSchema = new Schema({
name: String,
authored: [Number],
@@ -4321,23 +3841,16 @@ describe('model: populate:', function() {
const blogPosts = [{ _id: 0, title: 'Bacon is Great' }];
const people = [{ name: 'Val', authored: [0], favorites: [0] }];
- Person.create(people, function(error) {
- assert.ifError(error);
- BlogPost.create(blogPosts, function(error) {
- assert.ifError(error);
- BlogPost.
- findOne({ _id: 0 }).
- populate('authors favoritedBy').
- exec(function(error, post) {
- assert.ifError(error);
- assert.equal(post.authors.length, 1);
- assert.equal(post.authors[0].name, 'Val');
- assert.equal(post.favoritedBy.length, 1);
- assert.equal(post.favoritedBy[0].name, 'Val');
- done();
- });
- });
- });
+ await Person.create(people);
+ await BlogPost.create(blogPosts);
+ const post = await BlogPost.
+ findOne({ _id: 0 }).
+ populate('authors favoritedBy');
+ assert.equal(post.authors.length, 1);
+ assert.equal(post.authors[0].name, 'Val');
+ assert.equal(post.favoritedBy.length, 1);
+ assert.equal(post.favoritedBy[0].name, 'Val');
+
});
it('in embedded array (gh-4928)', function(done) {
@@ -4479,7 +3992,7 @@ describe('model: populate:', function() {
assert.deepEqual(app.modules[1].menu.map(i => i.title), ['Redo']);
});
- it('justOne option (gh-4263)', function(done) {
+ it('justOne option (gh-4263)', async function() {
const PersonSchema = new Schema({
name: String,
authored: [Number]
@@ -4505,21 +4018,14 @@ describe('model: populate:', function() {
{ name: 'Test', authored: [0] }
];
- Person.create(people, function(error) {
- assert.ifError(error);
- BlogPost.create(blogPosts, function(error) {
- assert.ifError(error);
- BlogPost.
- findOne({ _id: 0 }).
- populate('author').
- exec(function(error, post) {
- assert.ifError(error);
- assert.strictEqual(Array.isArray(post.author), false);
- assert.ok(post.author.name.match(/^(Val|Test)$/));
- done();
- });
- });
- });
+ await Person.create(people);
+ await BlogPost.create(blogPosts);
+ const post = await BlogPost.
+ findOne({ _id: 0 }).
+ populate('author');
+ assert.strictEqual(Array.isArray(post.author), false);
+ assert.ok(post.author.name.match(/^(Val|Test)$/));
+
});
it('justOne + lean (gh-6234)', async function() {
@@ -4626,7 +4132,7 @@ describe('model: populate:', function() {
assert.ok(!doc.items[1].itemDetail);
});
- it('with no results and justOne (gh-4284)', function(done) {
+ it('with no results and justOne (gh-4284)', async function() {
const PersonSchema = new Schema({
name: String,
authored: [Number]
@@ -4654,25 +4160,18 @@ describe('model: populate:', function() {
{ name: 'Val', authored: [0] }
];
- Person.create(people, function(error) {
- assert.ifError(error);
- BlogPost.create(blogPosts, function(error) {
- assert.ifError(error);
- BlogPost.
- find({}).
- sort({ title: 1 }).
- populate('author').
- exec(function(error, posts) {
- assert.ifError(error);
- assert.equal(posts[0].author.name, 'Val');
- assert.strictEqual(posts[1].author, null);
- done();
- });
- });
- });
+ await Person.create(people);
+ await BlogPost.create(blogPosts);
+ const posts = await BlogPost.
+ find({}).
+ sort({ title: 1 }).
+ populate('author');
+ assert.equal(posts[0].author.name, 'Val');
+ assert.strictEqual(posts[1].author, null);
+
});
- it('with multiple results and justOne (gh-4329)', function(done) {
+ it('with multiple results and justOne (gh-4329)', async function() {
const UserSchema = new Schema({
openId: String
});
@@ -4690,27 +4189,20 @@ describe('model: populate:', function() {
const User = db.model('User', UserSchema);
const Comment = db.model('Comment', CommentSchema);
- User.create({ openId: 'user1' }, { openId: 'user2' }, function(error) {
- assert.ifError(error);
- Comment.create({ openId: 'user1' }, { openId: 'user2' }, function(error) {
- assert.ifError(error);
- Comment.
- find().
- sort({ openId: 1 }).
- populate('user').
- exec(function(error, tasks) {
- assert.ifError(error);
-
- assert.ok(tasks[0].user);
- assert.ok(tasks[1].user);
- const users = tasks.map(function(task) {
- return task.user.openId;
- });
- assert.deepEqual(users, ['user1', 'user2']);
- done();
- });
- });
+ await User.create({ openId: 'user1' }, { openId: 'user2' });
+ await Comment.create({ openId: 'user1' }, { openId: 'user2' });
+ const tasks = await Comment.
+ find().
+ sort({ openId: 1 }).
+ populate('user');
+
+ assert.ok(tasks[0].user);
+ assert.ok(tasks[1].user);
+ const users = tasks.map(function(task) {
+ return task.user.openId;
});
+ assert.deepEqual(users, ['user1', 'user2']);
+
});
it('virtuals with getters (gh-9343)', async function() {
@@ -5049,7 +4541,7 @@ describe('model: populate:', function() {
assert.strictEqual(doc.agents[0].item.vendor, 'chrome');
});
- it('with no results (gh-4284)', function(done) {
+ it('with no results (gh-4284)', async function() {
const PersonSchema = new Schema({
name: String,
authored: [Number]
@@ -5078,29 +4570,22 @@ describe('model: populate:', function() {
{ name: 'Test', authored: [0, 1] }
];
- Person.create(people, function(error) {
- assert.ifError(error);
- BlogPost.create(blogPosts, function(error) {
- assert.ifError(error);
- BlogPost.
- find({}).
- sort({ _id: 1 }).
- populate('authors').
- exec(function(error, posts) {
- assert.ifError(error);
- const arr = posts[0].toObject({ virtuals: true }).authors.
- map(function(v) {
- return v.name;
- }).
- sort();
- assert.deepEqual(arr, ['Test', 'Val']);
- assert.equal(posts[1].authors.length, 1);
- assert.equal(posts[1].authors[0].name, 'Test');
- assert.equal(posts[2].authors.length, 0);
- done();
- });
- });
- });
+ await Person.create(people);
+ await BlogPost.create(blogPosts);
+ const posts = await BlogPost.
+ find({}).
+ sort({ _id: 1 }).
+ populate('authors');
+ const arr = posts[0].toObject({ virtuals: true }).authors.
+ map(function(v) {
+ return v.name;
+ }).
+ sort();
+ assert.deepEqual(arr, ['Test', 'Val']);
+ assert.equal(posts[1].authors.length, 1);
+ assert.equal(posts[1].authors[0].name, 'Test');
+ assert.equal(posts[2].authors.length, 0);
+
});
it('virtual is undefined when not populated (gh-7795)', async function() {
@@ -5123,7 +4608,7 @@ describe('model: populate:', function() {
assert.strictEqual(doc.authors, void 0);
});
- it('deep populate virtual -> conventional (gh-4261)', function(done) {
+ it('deep populate virtual -> conventional (gh-4261)', async function() {
const PersonSchema = new Schema({
name: String
});
@@ -5143,40 +4628,33 @@ describe('model: populate:', function() {
const Person = db.model('Person', PersonSchema);
const BlogPost = db.model('BlogPost', BlogPostSchema);
- const people = [
+ let people = [
{ name: 'Val' },
{ name: 'Test' }
];
- Person.create(people, function(error, people) {
- assert.ifError(error);
- const post = {
- title: 'Test1',
- author: people[0]._id,
- comments: [{ author: people[1]._id }]
- };
- BlogPost.create(post, function(error) {
- assert.ifError(error);
- Person.findById(people[0]._id).
- populate({
- path: 'blogPosts',
- model: BlogPost,
- populate: {
- path: 'comments.author',
- model: Person
- }
- }).
- exec(function(error, person) {
- assert.ifError(error);
- assert.equal(person.blogPosts[0].comments[0].author.name,
- 'Test');
- done();
- });
+ people = await Person.create(people);
+ const post = {
+ title: 'Test1',
+ author: people[0]._id,
+ comments: [{ author: people[1]._id }]
+ };
+ await BlogPost.create(post);
+ const person = await Person.findById(people[0]._id).
+ populate({
+ path: 'blogPosts',
+ model: BlogPost,
+ populate: {
+ path: 'comments.author',
+ model: Person
+ }
});
- });
+ assert.equal(person.blogPosts[0].comments[0].author.name,
+ 'Test');
+
});
- it('deep populate virtual -> virtual (gh-4278)', function(done) {
+ it('deep populate virtual -> virtual (gh-4278)', async function() {
const ASchema = new Schema({
name: String
});
@@ -5205,29 +4683,21 @@ describe('model: populate:', function() {
const B = db.model('Test2', BSchema);
const C = db.model('Test3', CSchema);
- A.create({ name: 'A1' }, function(error, a) {
- assert.ifError(error);
- B.create({ name: 'B1', a: a._id }, function(error, b) {
- assert.ifError(error);
- C.create({ name: 'C1', b: b._id }, function(error) {
- assert.ifError(error);
- const options = {
- path: 'bs',
- populate: {
- path: 'cs'
- }
- };
- A.findById(a).populate(options).exec(function(error, res) {
- assert.ifError(error);
- assert.equal(res.bs.length, 1);
- assert.equal(res.bs[0].name, 'B1');
- assert.equal(res.bs[0].cs.length, 1);
- assert.equal(res.bs[0].cs[0].name, 'C1');
- done();
- });
- });
- });
- });
+ const a = await A.create({ name: 'A1' });
+ const b = await B.create({ name: 'B1', a: a._id });
+ await C.create({ name: 'C1', b: b._id });
+ const options = {
+ path: 'bs',
+ populate: {
+ path: 'cs'
+ }
+ };
+ const res = await A.findById(a).populate(options);
+ assert.equal(res.bs.length, 1);
+ assert.equal(res.bs[0].name, 'B1');
+ assert.equal(res.bs[0].cs.length, 1);
+ assert.equal(res.bs[0].cs[0].name, 'C1');
+
});
it('source array (gh-4585)', function(done) {
@@ -5286,7 +4756,7 @@ describe('model: populate:', function() {
catch(done);
});
- it('lean with single result and no justOne (gh-4288)', function(done) {
+ it('lean with single result and no justOne (gh-4288)', async function() {
const PersonSchema = new Schema({
name: String,
authored: [Number]
@@ -5313,25 +4783,18 @@ describe('model: populate:', function() {
{ name: 'Val', authored: [0] }
];
- Person.create(people, function(error) {
- assert.ifError(error);
- BlogPost.create(blogPosts, function(error) {
- assert.ifError(error);
- BlogPost.
- findOne({}).
- lean().
- populate({ path: 'authors', model: Person }).
- exec(function(error, post) {
- assert.ifError(error);
- assert.equal(post.authors.length, 1);
- assert.equal(post.authors[0].name, 'Val');
- done();
- });
- });
- });
+ await Person.create(people);
+ await BlogPost.create(blogPosts);
+ const post = await BlogPost.
+ findOne({}).
+ lean().
+ populate({ path: 'authors', model: Person });
+ assert.equal(post.authors.length, 1);
+ assert.equal(post.authors[0].name, 'Val');
+
});
- it('gh-4923', function(done) {
+ it('gh-4923', function() {
const ClusterSchema = new Schema({
name: String
});
@@ -5364,7 +4827,7 @@ describe('model: populate:', function() {
DocSchema.set('toJSON', { virtuals: true });
const Doc = db.model('Test2', DocSchema);
- Cluster.create([{ name: 'c1' }, { name: 'c2' }, { name: 'c3' }]).
+ return Cluster.create([{ name: 'c1' }, { name: 'c2' }, { name: 'c3' }]).
then(function(c) {
return Zone.create([
{ name: 'z1', clusters: [c[0]._id, c[1]._id, c[2]._id] },
@@ -5384,26 +4847,23 @@ describe('model: populate:', function() {
findOne({}).
populate('activity.cluster').
populate('activity.zones', 'name clusters').
- exec(function(error, res) {
- assert.ifError(error);
- // Fails if this `.toObject()` is omitted, issue #4926
- res = res.toObject({ virtuals: true });
- const compare = function(a, b) {
- if (a.name < b.name) {
- return -1;
- } else if (b.name < a.name) {
- return 1;
- }
- return 0;
- };
- res.activity[0].zones.sort(compare);
- res.activity[1].zones.sort(compare);
- assert.equal(res.activity[0].zones[0].name, 'z1');
- assert.equal(res.activity[1].zones[0].name, 'z1');
- done();
- });
+ exec();
}).
- catch(done);
+ then(res => {
+ res = res.toObject({ virtuals: true });
+ const compare = function(a, b) {
+ if (a.name < b.name) {
+ return -1;
+ } else if (b.name < a.name) {
+ return 1;
+ }
+ return 0;
+ };
+ res.activity[0].zones.sort(compare);
+ res.activity[1].zones.sort(compare);
+ assert.equal(res.activity[0].zones[0].name, 'z1');
+ assert.equal(res.activity[1].zones[0].name, 'z1');
+ });
});
it('supports setting default options in schema (gh-4741)', function(done) {
@@ -5448,17 +4908,14 @@ describe('model: populate:', function() {
catch(done);
});
- it('handles populate with 0 args (gh-5036)', function(done) {
+ it('handles populate with 0 args (gh-5036)', async function() {
const userSchema = new Schema({
name: String
});
const User = db.model('User', userSchema);
- User.findOne().populate().exec(function(error) {
- assert.ifError(error);
- done();
- });
+ await User.findOne().populate();
});
it('attaches `_id` property to ref ids (gh-6359) (gh-6115)', function() {
@@ -5496,7 +4953,7 @@ describe('model: populate:', function() {
delete mongoose.options.selectPopulatedPaths;
});
- it('auto select populated fields (gh-5669) (gh-5685)', function(done) {
+ it('auto select populated fields (gh-5669) (gh-5685)', async function() {
const ProductSchema = new mongoose.Schema({
name: {
type: String
@@ -5514,32 +4971,22 @@ describe('model: populate:', function() {
const Product = db.model('Product', ProductSchema);
const Category = db.model('Test', CategorySchema);
- Category.create({ name: 'Books' }, function(error, doc) {
- assert.ifError(error);
- const product = {
- name: 'Professional AngularJS',
- categories: [doc._id]
- };
- Product.create(product, function(error, product) {
- assert.ifError(error);
- Product.findById(product._id).populate('categories').exec(function(error, product) {
- assert.ifError(error);
- assert.equal(product.categories.length, 1);
- assert.equal(product.categories[0].name, 'Books');
- Product.findById(product._id).populate('categories').select({ categories: 0 }).exec(function(error, product) {
- assert.ifError(error);
- assert.ok(!product.categories);
- Product.findById(product._id).select({ name: 0 }).populate('categories').exec(function(error, product) {
- assert.ifError(error);
- assert.equal(product.categories.length, 1);
- assert.equal(product.categories[0].name, 'Books');
- assert.ok(!product.name);
- done();
- });
- });
- });
- });
- });
+ const doc = await Category.create({ name: 'Books' });
+ let product = {
+ name: 'Professional AngularJS',
+ categories: [doc._id]
+ };
+ product = await Product.create(product);
+ product = await Product.findById(product._id).populate('categories');
+ assert.equal(product.categories.length, 1);
+ assert.equal(product.categories[0].name, 'Books');
+ product = await Product.findById(product._id).populate('categories').select({ categories: 0 });
+ assert.ok(!product.categories);
+ product = await Product.findById(product._id).select({ name: 0 }).populate('categories');
+ assert.equal(product.categories.length, 1);
+ assert.equal(product.categories[0].name, 'Books');
+ assert.ok(!product.name);
+
});
it('disabling at schema level (gh-6546)', async function() {
@@ -5709,7 +5156,7 @@ describe('model: populate:', function() {
catch(done);
});
- it('nested populate, virtual -> normal (gh-4631)', function(done) {
+ it('nested populate, virtual -> normal (gh-4631)', async function() {
const PersonSchema = new Schema({
name: String
});
@@ -5729,38 +5176,31 @@ describe('model: populate:', function() {
const Person = db.model('Person', PersonSchema);
const BlogPost = db.model('BlogPost', BlogPostSchema);
- const people = [
+ let people = [
{ name: 'Val' },
{ name: 'Test' }
];
- Person.create(people, function(error, people) {
- assert.ifError(error);
- const post = {
- title: 'Test1',
- author: people[0]._id,
- comments: [{ author: people[1]._id }]
- };
- BlogPost.create(post, function(error) {
- assert.ifError(error);
+ people = await Person.create(people);
+ const post = {
+ title: 'Test1',
+ author: people[0]._id,
+ comments: [{ author: people[1]._id }]
+ };
+ await BlogPost.create(post);
- Person.findById(people[0]._id).
- populate({
- path: 'blogPosts',
- model: BlogPost,
- populate: {
- path: 'author',
- model: Person
- }
- }).
- exec(function(error, doc) {
- assert.ifError(error);
- assert.equal(doc.blogPosts.length, 1);
- assert.equal(doc.blogPosts[0].author.name, 'Val');
- done();
- });
+ const doc = await Person.findById(people[0]._id).
+ populate({
+ path: 'blogPosts',
+ model: BlogPost,
+ populate: {
+ path: 'author',
+ model: Person
+ }
});
- });
+ assert.equal(doc.blogPosts.length, 1);
+ assert.equal(doc.blogPosts[0].author.name, 'Val');
+
});
it('populate with Decimal128 as ref (gh-4759)', async function() {
@@ -5946,7 +5386,7 @@ describe('model: populate:', function() {
catch(done);
});
- it('no ref + cursor (gh-5334)', function(done) {
+ it('no ref + cursor (gh-5334)', async function() {
const parentSchema = new Schema({
name: String,
child: mongoose.Schema.Types.ObjectId
@@ -5958,20 +5398,13 @@ describe('model: populate:', function() {
const Parent = db.model('Parent', parentSchema);
const Child = db.model('Child', childSchema);
- Child.create({ name: 'Luke' }, function(error, child) {
- assert.ifError(error);
- Parent.create({ name: 'Vader', child: child._id }, function(error) {
- assert.ifError(error);
- Parent.find().populate({ path: 'child', model: 'Child' }).cursor().next(function(error, doc) {
- assert.ifError(error);
- assert.equal(doc.child.name, 'Luke');
- done();
- });
- });
- });
+ const child = await Child.create({ name: 'Luke' });
+ await Parent.create({ name: 'Vader', child: child._id });
+ const doc = await Parent.find().populate({ path: 'child', model: 'Child' }).cursor().next();
+ assert.equal(doc.child.name, 'Luke');
});
- it('retains limit when using cursor (gh-5468)', function(done) {
+ it('retains limit when using cursor (gh-5468)', async function() {
const refSchema = new mongoose.Schema({
_id: Number,
name: String
@@ -5987,29 +5420,27 @@ describe('model: populate:', function() {
const docs = [1, 2, 3, 4, 5, 6].map(function(i) {
return { _id: i };
});
- Ref.create(docs, function(error) {
- assert.ifError(error);
- const docs = [
- { _id: 1, prevnxt: [1, 2, 3] },
- { _id: 2, prevnxt: [4, 5, 6] }
- ];
- Test.create(docs, function(error) {
- assert.ifError(error);
-
- const cursor = Test.
- find().
- populate({ path: 'prevnxt', options: { limit: 2 } }).
- cursor();
-
- cursor.on('data', function(doc) {
- assert.equal(doc.prevnxt.length, 2);
- });
- cursor.on('error', done);
- cursor.on('end', function() {
- done();
- });
- });
- });
+ await Ref.create(docs);
+
+ const docs2 = [
+ { _id: 1, prevnxt: [1, 2, 3] },
+ { _id: 2, prevnxt: [4, 5, 6] }
+ ];
+ await Test.create(docs2);
+
+ const cursor = Test.
+ find().
+ populate({ path: 'prevnxt', options: { limit: 2 } }).
+ cursor();
+
+ let count = 0;
+
+ for (let doc = await cursor.next(); doc != null; doc = await cursor.next()) {
+ assert.equal(doc.prevnxt.length, 2);
+ ++count;
+ }
+
+ assert.equal(count, 2);
});
it('virtuals + doc.populate() (gh-5311)', function(done) {
@@ -6046,7 +5477,7 @@ describe('model: populate:', function() {
catch(done);
});
- it('empty virtual with Model.populate (gh-5331)', function(done) {
+ it('empty virtual with Model.populate (gh-5331)', async function() {
const myModelSchema = new Schema({
virtualRefKey: { type: String, ref: 'Test' }
});
@@ -6064,15 +5495,11 @@ describe('model: populate:', function() {
const MyModel = db.model('Test', myModelSchema);
db.model('Test1', otherModelSchema);
- MyModel.create({ virtualRefKey: 'test' }, function(error, doc) {
- assert.ifError(error);
- MyModel.populate(doc, 'populatedVirtualRef', function(error, doc) {
- assert.ifError(error);
- assert.ok(doc.populatedVirtualRef);
- assert.ok(Array.isArray(doc.populatedVirtualRef));
- done();
- });
- });
+ const doc = await MyModel.create({ virtualRefKey: 'test' });
+ const populatedDoc = await MyModel.populate(doc, 'populatedVirtualRef');
+
+ assert.ok(populatedDoc.populatedVirtualRef);
+ assert.ok(Array.isArray(populatedDoc.populatedVirtualRef));
});
it('virtual populate in single nested doc (gh-4715)', function(done) {
@@ -6114,7 +5541,7 @@ describe('model: populate:', function() {
catch(done);
});
- it('populate with missing schema (gh-5364)', function(done) {
+ it('populate with missing schema (gh-5364)', async function() {
const Foo = db.model('Test', new mongoose.Schema({
bar: {
type: mongoose.Schema.Types.ObjectId,
@@ -6122,17 +5549,12 @@ describe('model: populate:', function() {
}
}));
- Foo.create({ bar: new mongoose.Types.ObjectId() }, function(error) {
- assert.ifError(error);
- Foo.find().populate('bar').exec(function(error) {
- assert.ok(error);
- assert.equal(error.name, 'MissingSchemaError');
- done();
- });
- });
+ await Foo.create({ bar: new mongoose.Types.ObjectId() });
+ const error = await Foo.find().populate('bar').exec().then(() => null, err => err);
+ assert.equal(error.name, 'MissingSchemaError');
});
- it('populate with missing schema (gh-5460)', function(done) {
+ it('populate with missing schema (gh-5460)', async function() {
const refSchema = new mongoose.Schema({
name: String
});
@@ -6148,7 +5570,6 @@ describe('model: populate:', function() {
const q = Model.find().read('secondaryPreferred').populate('ref');
assert.equal(q._mongooseOptions.populate['ref'].options.readPreference.mode,
'secondaryPreferred');
- done();
});
it('array underneath non-existent array (gh-6245)', async function() {
@@ -6587,7 +6008,7 @@ describe('model: populate:', function() {
});
});
- it('specify model in populate (gh-4264)', function(done) {
+ it('specify model in populate (gh-4264)', async function() {
const PersonSchema = new Schema({
name: String,
authored: [Number]
@@ -6611,21 +6032,13 @@ describe('model: populate:', function() {
{ name: 'Val', authored: [0] }
];
- Person.create(people, function(error) {
- assert.ifError(error);
- BlogPost.create(blogPosts, function(error) {
- assert.ifError(error);
- BlogPost.
- findOne({ _id: 0 }).
- populate({ path: 'authors', model: Person }).
- exec(function(error, post) {
- assert.ifError(error);
- assert.equal(post.authors.length, 1);
- assert.equal(post.authors[0].name, 'Val');
- done();
- });
- });
- });
+ await Person.create(people);
+ await BlogPost.create(blogPosts);
+ const post = await BlogPost.
+ findOne({ _id: 0 }).
+ populate({ path: 'authors', model: Person });
+ assert.equal(post.authors.length, 1);
+ assert.equal(post.authors[0].name, 'Val');
});
});
@@ -8689,13 +8102,19 @@ describe('model: populate:', function() {
assert.equal(res.nested.events[0].nestedLayer.users_$[0].name, 'test');
});
- it('accessing populate virtual prop (gh-8198)', async function() {
+ it('accessing populate virtual prop (gh-13189) (gh-8198)', async function() {
const FooSchema = new Schema({
name: String,
children: [{
barId: { type: Schema.Types.ObjectId, ref: 'Test' },
quantity: Number
- }]
+ }],
+ child: new Schema({
+ barId: {
+ type: 'ObjectId',
+ ref: 'Test'
+ }
+ })
});
FooSchema.virtual('children.bar', {
ref: 'Test',
@@ -8703,6 +8122,11 @@ describe('model: populate:', function() {
foreignField: '_id',
justOne: true
});
+ FooSchema.virtual('child.bars', {
+ ref: 'Test',
+ localField: 'child.barId',
+ foreignField: '_id'
+ });
const BarSchema = Schema({ name: String });
const Foo = db.model('Test1', FooSchema);
const Bar = db.model('Test', BarSchema);
@@ -8710,10 +8134,18 @@ describe('model: populate:', function() {
const bar = await Bar.create({ name: 'bar' });
const foo = await Foo.create({
name: 'foo',
- children: [{ barId: bar._id, quantity: 1 }]
+ children: [{ barId: bar._id, quantity: 1 }],
+ child: {
+ barId: bar._id
+ }
});
- const foo2 = await Foo.findById(foo._id).populate('children.bar');
+ const foo2 = await Foo.findById(foo._id).populate('children.bar child.bars');
assert.equal(foo2.children[0].bar.name, 'bar');
+ assert.equal(foo2.child.bars[0].name, 'bar');
+
+ const asObject = foo2.toObject({ virtuals: true });
+ assert.equal(asObject.children[0].bar.name, 'bar');
+ assert.equal(asObject.child.bars[0].name, 'bar');
});
describe('gh-8247', function() {
@@ -8974,6 +8406,58 @@ describe('model: populate:', function() {
assert.deepEqual(populatedRides[1].files, []);
});
+ it('doesnt insert empty document when lean populating a path within an underneath non-existent document array (gh-14098)', async function() {
+ const userSchema = new mongoose.Schema({
+ fullName: String,
+ company: String
+ });
+ const User = db.model('User', userSchema);
+
+ const fileSchema = new mongoose.Schema({
+ _id: String,
+ uploaderId: {
+ type: mongoose.ObjectId,
+ ref: 'User'
+ }
+ }, { toObject: { virtuals: true }, toJSON: { virtuals: true } });
+ fileSchema.virtual('uploadedBy', {
+ ref: 'User',
+ localField: 'uploaderId',
+ foreignField: '_id',
+ justOne: true
+ });
+
+ const contentSchema = new mongoose.Schema({
+ memo: String,
+ files: { type: [fileSchema], default: [] }
+ }, { toObject: { virtuals: true }, toJSON: { virtuals: true }, _id: false });
+
+ const postSchema = new mongoose.Schema({
+ title: String,
+ content: { type: contentSchema }
+ }, { toObject: { virtuals: true }, toJSON: { virtuals: true } });
+ const Post = db.model('Test1', postSchema);
+
+ const user = await User.create({ fullName: 'John Doe', company: 'GitHub' });
+ await Post.create([
+ { title: 'London-Paris' },
+ {
+ title: 'Berlin-Moscow',
+ content: {
+ memo: 'Not Easy',
+ files: [{ _id: '123', uploaderId: user._id }]
+ }
+ }
+ ]);
+ await Post.updateMany({}, { $unset: { 'content.files': 1 } });
+ const populatedRides = await Post.find({}).populate({
+ path: 'content.files.uploadedBy',
+ justOne: true
+ }).lean();
+ assert.equal(populatedRides[0].content.files, undefined);
+ assert.equal(populatedRides[1].content.files, undefined);
+ });
+
it('sets empty array if populating undefined path (gh-8455)', async function() {
const TestSchema = new Schema({
thingIds: [mongoose.ObjectId]
@@ -10109,7 +9593,6 @@ describe('model: populate:', function() {
children: [{ type: 'ObjectId', ref: 'Child' }]
}));
-
const children = await Child.create([{ name: 'Luke' }, { name: 'Leia' }]);
let doc = await Parent.create({ children, child: children[0] });
@@ -10830,7 +10313,7 @@ describe('model: populate:', function() {
sortOrder: 1,
values: {
[createList._id]: {
- valueObject: mongoose.Types.ObjectId(createUser._id),
+ valueObject: new mongoose.Types.ObjectId(createUser._id),
refp: 'User'
}
}
@@ -10966,8 +10449,145 @@ describe('model: populate:', function() {
assert.equal(person.stories[0].title, 'Casino Royale');
});
+ it('supports removing and then recreating populate virtual using schema clone (gh-13085)', async function() {
+ const personSch = new mongoose.Schema(
+ {
+ firstName: { type: mongoose.SchemaTypes.String, required: true },
+ surname: { type: mongoose.SchemaTypes.String, trim: true },
+ nat: { type: mongoose.SchemaTypes.String, required: true, uppercase: true, minLength: 2, maxLength: 2 }
+ },
+ { strict: true, timestamps: true }
+ );
+ personSch.virtual('nationality', {
+ localField: 'nat',
+ foreignField: 'key',
+ ref: 'Nat',
+ justOne: true
+ });
+ let Person = db.model('Person', personSch.clone(), 'people');
+
+ const natSch = new mongoose.Schema(
+ {
+ key: { type: mongoose.SchemaTypes.String, uppercase: true, index: true, minLength: 2, maxLength: 2 },
+ desc: { type: mongoose.SchemaTypes.String, trim: true }
+ },
+ { strict: true }
+ );
+ const Nat = db.model('Nat', natSch);
+ let n = new Nat({ key: 'ES', desc: 'Spain' });
+ await n.save();
+ n = new Nat({ key: 'IT', desc: 'Italy' });
+ await n.save();
+ n = new Nat({ key: 'FR', desc: 'French' });
+ await n.save();
+
+ let p = new Person({ firstName: 'Pepe', surname: 'Pérez', nat: 'it' });
+ await p.save();
+ p = new Person({ firstName: 'Paco', surname: 'Matinez', nat: 'es' });
+ await p.save();
+ p = new Person({ firstName: 'John', surname: 'Doe', nat: 'us' });
+ await p.save();
+
+ personSch.removeVirtual('nationality');
+ personSch.virtual('nationality', {
+ localField: 'nat',
+ foreignField: 'key',
+ ref: 'Nat',
+ justOne: true
+ });
+ Person = db.model('Person', personSch.clone(), 'people', { overwriteModels: true });
+
+ const peopleList = await Person.find().
+ sort({ firstName: 1 }).
+ populate({ path: 'nationality', match: { desc: 'Spain' } });
+ assert.deepStrictEqual(peopleList.map(p => p.nationality?.key), [undefined, 'ES', undefined]);
+
+ });
+
+ it('handles populating underneath document arrays that have null (gh-13839)', async function() {
+ const schema = new Schema({
+ children: [
+ {
+ doc: { type: mongoose.Schema.Types.ObjectId, ref: 'Child' },
+ name: String
+ }
+ ]
+ });
+ const Parent = db.model('Parent', schema);
+ const Child = db.model('Child', new Schema({ name: String }));
+
+ const child = new Child({ name: 'gh-13839-test' });
+ await child.save();
+ let parent = new Parent({ children: [{ doc: child._id, name: 'foo' }, null] });
+ await parent.save();
+
+ parent = await Parent.findById(parent._id).populate('children.doc');
+ assert.equal(parent.children.length, 2);
+ assert.equal(parent.children[0].doc.name, 'gh-13839-test');
+ assert.equal(parent.children[1], null);
+ });
describe('strictPopulate', function() {
+ it('does not throw an error when using strictPopulate on a nested path (gh-13863)', async function() {
+ const l4Schema = new mongoose.Schema({
+ name: String
+ });
+
+ const l3aSchema = new mongoose.Schema({
+ l4: {
+ type: 'ObjectId',
+ ref: 'L4'
+ }
+ });
+ const l3bSchema = new mongoose.Schema({
+ otherProp: String
+ });
+
+ const l2Schema = new mongoose.Schema({
+ l3a: {
+ type: 'ObjectId',
+ ref: 'L3A'
+ },
+ l3b: {
+ type: 'ObjectId',
+ ref: 'L3B'
+ }
+ });
+
+ const l1Schema = new mongoose.Schema({
+ l2: {
+ type: 'ObjectId',
+ ref: 'L2'
+ }
+ });
+
+ const L1 = db.model('L1', l1Schema);
+ const L2 = db.model('L2', l2Schema);
+ const L3A = db.model('L3A', l3aSchema);
+ const L3B = db.model('L3B', l3bSchema);
+ const L4 = db.model('L4', l4Schema);
+
+ const { _id: l4 } = await L4.create({ name: 'test l4' });
+ const { _id: l3a } = await L3A.create({ l4 });
+ const { _id: l3b } = await L3B.create({ name: 'test l3' });
+ const { _id: l2 } = await L2.create({ l3a, l3b });
+ const { _id: l1 } = await L1.create({ l2 });
+
+ const res = await L1.findById(l1).populate({
+ path: 'l2',
+ populate: {
+ path: 'l3a l3b',
+ populate: {
+ path: 'l4',
+ options: {
+ strictPopulate: false
+ }
+ }
+ }
+ });
+ assert.equal(res.l2.l3a.l4.name, 'test l4');
+ assert.equal(res.l2.l3b.l4, undefined);
+ });
it('reports full path when throwing `strictPopulate` error with deep populate (gh-10923)', async function() {
const L2 = db.model('Test', new Schema({ name: String }));
@@ -10989,6 +10609,51 @@ describe('model: populate:', function() {
assert.ok(err.message.indexOf('l1.l22') !== -1, err.message);
});
+ it('propagates toObject options to populate virtuals (gh-13325)', async function() {
+ const userSchema = Schema({
+ firstName: String,
+ companies: {
+ type: [{ companyId: { type: Schema.Types.ObjectId }, companyName: String }]
+ }
+ }, {
+ toObject: { virtuals: true },
+ toJSON: { virtuals: true }
+ });
+
+ userSchema.virtual('companies.details', {
+ ref: 'company',
+ localField: 'companies.companyId',
+ foreignField: '_id',
+ justOne: true
+ });
+
+ const User = db.model('User', userSchema);
+ const companySchema = Schema({
+ name: {
+ type: String
+ },
+ legalName: {
+ type: String,
+ required: true
+ }
+ });
+ const Company = db.model('company', companySchema);
+
+ const comp = await Company.create({
+ name: 'Google',
+ legalName: 'Alphabet Inc'
+ });
+ await User.create({
+ firstName: 'Test',
+ companies: [{ companyId: comp._id, companyName: 'Google' }]
+ });
+ const doc = await User.findOne().populate('companies.details');
+ let obj = doc.toObject();
+ assert.equal(obj.companies[0].details.name, 'Google');
+ obj = doc.toJSON();
+ assert.equal(obj.companies[0].details.name, 'Google');
+ });
+
it('respects strictPopulate schema option (gh-11290)', async function() {
const kittySchema = Schema({ name: String }, { strictPopulate: false });
@@ -11020,11 +10685,11 @@ describe('model: populate:', function() {
const entry = await Test.create({
name: 'Test',
- uuid: mongoose.Types.ObjectId()
+ uuid: new mongoose.Types.ObjectId()
});
const otherEntry = await Test.create({
name: 'Other Test',
- uuid: mongoose.Types.ObjectId()
+ uuid: new mongoose.Types.ObjectId()
});
await User.create({
name: 'User',
@@ -11041,11 +10706,11 @@ describe('model: populate:', function() {
// =================localField======================
const localEntry = await Test.create({
name: 'local test',
- uuid: mongoose.Types.ObjectId()
+ uuid: new mongoose.Types.ObjectId()
});
const otherLocalEntry = await Test.create({
name: 'other local test',
- uuid: mongoose.Types.ObjectId()
+ uuid: new mongoose.Types.ObjectId()
});
await User.create({
@@ -11068,8 +10733,8 @@ describe('model: populate:', function() {
populate({ path: 'test' });
assert.equal(check.test.length, 0);
// ============localFieldAndForeignField============
- const bothEntry = await Test.create({ name: 'Both', uuid: mongoose.Types.ObjectId() });
- const otherBothEntry = await Test.create({ name: 'Other Both', uuid: mongoose.Types.ObjectId() });
+ const bothEntry = await Test.create({ name: 'Both', uuid: new mongoose.Types.ObjectId() });
+ const otherBothEntry = await Test.create({ name: 'Other Both', uuid: new mongoose.Types.ObjectId() });
await User.create({
name: 'both user',
field: bothEntry.uuid,
@@ -11090,4 +10755,672 @@ describe('model: populate:', function() {
assert.equal(normal.test.length, 0);
});
});
+
+ it('calls match function with virtual as parameter (gh-12443)', async function() {
+ const parentSchema = mongoose.Schema({ name: String });
+ parentSchema.virtual('children', {
+ ref: 'Child',
+ localField: '_id',
+ foreignField: 'parentId',
+ justOne: false,
+ match: {
+ isDeleted: false
+ }
+ });
+ const Parent = db.model('Parent', parentSchema);
+
+ const childSchema = mongoose.Schema({
+ name: String,
+ parentId: 'ObjectId',
+ isDeleted: Boolean
+ });
+ const Child = db.model('Child', childSchema);
+
+ const { _id } = await Parent.create({ name: 'Darth Vader' });
+ await Child.create([
+ { name: 'Luke', parentId: _id, isDeleted: false },
+ { name: 'Leia', parentId: _id, isDeleted: false },
+ { name: 'Chad', parentId: _id, isDeleted: true }
+ ]);
+
+ let doc = await Parent.findById(_id).populate({
+ path: 'children'
+ });
+ assert.deepStrictEqual(doc.children.map(c => c.name).sort(), ['Leia', 'Luke']);
+
+ doc = await Parent.findById(_id).populate({
+ path: 'children',
+ match: (_doc, virtual) => ({
+ ...virtual.options.match,
+ name: /(Luke|Chad)/
+ })
+ });
+ assert.deepStrictEqual(doc.children.map(c => c.name).sort(), ['Luke']);
+
+ doc = await Parent.findById(_id).populate({
+ path: 'children',
+ match: () => ({
+ name: /(Luke|Chad)/
+ })
+ });
+ assert.deepStrictEqual(doc.children.map(c => c.name).sort(), ['Chad', 'Luke']);
+ });
+
+ it('allows pushing to model populated in a query cursor (gh-13575)', async function() {
+ const Company = db.model('Company', Schema({ name: String }));
+ const User = db.model(
+ 'User',
+ Schema({ name: String, companies: [{ type: Schema.Types.ObjectId, ref: 'Company' }] })
+ );
+
+ await User.deleteMany({});
+ await Company.insertMany([{ name: 'Company 1' }, { name: 'Company 2' }]);
+ const company = await Company.findOne({ name: 'Company 1' });
+ const company2 = await Company.findOne({ name: 'Company 2' });
+
+ await User.insertMany([{ name: 'User 1', companies: [company.id] }]);
+
+ await User.find()
+ .populate('companies')
+ .cursor()
+ .eachAsync(async(user) => {
+ if (!user.populated('companies')) {
+ await user.populate('companies');
+ }
+
+ user.companies.push(company2);
+
+ await user.save();
+ });
+
+ const user = await User.findOne();
+ assert.equal(user.name, 'User 1');
+ assert.deepEqual(
+ user.toObject().companies.sort().map(v => v.toString()),
+ [company._id.toString(), company2._id.toString()]
+ );
+ });
+
+ it('sets populated docs in correct order when populating virtual underneath document array with justOne (gh-14018)', async function() {
+ const shiftSchema = new mongoose.Schema({
+ employeeId: mongoose.Types.ObjectId,
+ startedAt: Date,
+ endedAt: Date
+ });
+ const Shift = db.model('Shift', shiftSchema);
+
+ const employeeSchema = new mongoose.Schema({
+ name: String
+ });
+ employeeSchema.virtual('mostRecentShift', {
+ ref: Shift,
+ localField: '_id',
+ foreignField: 'employeeId',
+ options: {
+ sort: { startedAt: -1 }
+ },
+ justOne: true
+ });
+ const storeSchema = new mongoose.Schema({
+ location: String,
+ employees: [employeeSchema]
+ });
+ const Store = db.model('Store', storeSchema);
+
+ const store = await Store.create({
+ location: 'Tashbaan',
+ employees: [
+ { _id: '0'.repeat(24), name: 'Aravis' },
+ { _id: '1'.repeat(24), name: 'Shasta' }
+ ]
+ });
+
+ const employeeAravis = store.employees
+ .find(({ name }) => name === 'Aravis');
+ const employeeShasta = store.employees
+ .find(({ name }) => name === 'Shasta');
+
+ await Shift.insertMany([
+ { employeeId: employeeAravis._id, startedAt: new Date('2011-06-01'), endedAt: new Date('2011-06-02') },
+ { employeeId: employeeAravis._id, startedAt: new Date('2013-06-01'), endedAt: new Date('2013-06-02') },
+ { employeeId: employeeShasta._id, startedAt: new Date('2015-06-01'), endedAt: new Date('2015-06-02') }
+ ]);
+
+ const storeWithMostRecentShifts = await Store.findOne({ location: 'Tashbaan' })
+ .populate('employees.mostRecentShift')
+ .select('-__v')
+ .exec();
+ assert.equal(
+ storeWithMostRecentShifts.employees[0].mostRecentShift.employeeId.toHexString(),
+ '0'.repeat(24)
+ );
+ assert.equal(
+ storeWithMostRecentShifts.employees[1].mostRecentShift.employeeId.toHexString(),
+ '1'.repeat(24)
+ );
+
+ await Shift.findOne({ employeeId: employeeAravis._id }).sort({ startedAt: 1 }).then((s) => s.deleteOne());
+
+ const storeWithMostRecentShiftsNew = await Store.findOne({ location: 'Tashbaan' })
+ .populate('employees.mostRecentShift')
+ .select('-__v')
+ .exec();
+ assert.equal(
+ storeWithMostRecentShiftsNew.employees[0].mostRecentShift.employeeId.toHexString(),
+ '0'.repeat(24)
+ );
+ assert.equal(
+ storeWithMostRecentShiftsNew.employees[0].mostRecentShift.startedAt.toString(),
+ new Date('2013-06-01').toString()
+ );
+ assert.equal(
+ storeWithMostRecentShiftsNew.employees[1].mostRecentShift.employeeId.toHexString(),
+ '1'.repeat(24)
+ );
+ assert.equal(
+ storeWithMostRecentShiftsNew.employees[1].mostRecentShift.startedAt.toString(),
+ new Date('2015-06-01').toString()
+ );
+ });
+
+ it('calls transform with single ObjectId when populating justOne path underneath array (gh-14073)', async function() {
+ const mySchema = mongoose.Schema({
+ name: { type: String },
+ items: [{
+ _id: false,
+ name: { type: String },
+ brand: { type: mongoose.Schema.Types.ObjectId, ref: 'Brand' }
+ }]
+ });
+
+ const brandSchema = mongoose.Schema({
+ name: 'String',
+ quantity: Number
+ });
+
+ const myModel = db.model('MyModel', mySchema);
+ const brandModel = db.model('Brand', brandSchema);
+ const { _id: id1 } = await brandModel.create({
+ name: 'test',
+ quantity: 1
+ });
+ const { _id: id2 } = await brandModel.create({
+ name: 'test1',
+ quantity: 1
+ });
+ const { _id: id3 } = await brandModel.create({
+ name: 'test2',
+ quantity: 2
+ });
+ const brands = await brandModel.find();
+ const test = new myModel({ name: 'Test Model' });
+ for (let i = 0; i < brands.length; i++) {
+ test.items.push({ name: `${i}`, brand: brands[i]._id });
+ }
+
+ const id4 = new mongoose.Types.ObjectId();
+ test.items.push({ name: '4', brand: id4 });
+ await test.save();
+
+ const ids = [];
+ await myModel
+ .findOne()
+ .populate([
+ {
+ path: 'items.brand',
+ transform: (doc, id) => {
+ ids.push(id);
+ return doc;
+ }
+ }
+ ]);
+ assert.equal(ids.length, 4);
+ assert.deepStrictEqual(
+ ids.map(id => id?.toHexString()),
+ [id1.toString(), id2.toString(), id3.toString(), id4.toString()]
+ );
+ });
+
+ it('allows deselecting discriminator key when populating (gh-3230) (gh-13760) (gh-13679)', async function() {
+ const Test = db.model(
+ 'Test',
+ Schema({ name: String, arr: [{ testRef: { type: 'ObjectId', ref: 'Test2' } }] })
+ );
+
+ const schema = Schema({ name: String });
+ const Test2 = db.model('Test2', schema);
+ const D = Test2.discriminator('D', Schema({ prop: String }));
+
+
+ await Test.deleteMany({});
+ await Test2.deleteMany({});
+ const { _id } = await D.create({ name: 'foo', prop: 'bar' });
+ const test = await Test.create({ name: 'test', arr: [{ testRef: _id }] });
+
+ const doc = await Test
+ .findById(test._id)
+ .populate('arr.testRef', { name: 1, prop: 1, _id: 0, __t: 0 });
+ assert.deepStrictEqual(
+ doc.toObject().arr[0].testRef,
+ { name: 'foo', prop: 'bar' }
+ );
+ });
+
+ it('calls setter on virtual populated path with populated doc (gh-14285)', async function() {
+ const userSchema = new Schema({
+ email: String,
+ name: 'String'
+ });
+
+ const User = db.model('User', userSchema);
+
+ const user = await User.create({
+ email: 'admin@example.com',
+ name: 'Admin'
+ });
+
+ const personSchema = new Schema({
+ userId: ObjectId,
+ userType: String
+ });
+
+ personSchema.
+ virtual('user', {
+ ref() {
+ return this.userType;
+ },
+ localField: 'userId',
+ foreignField: '_id',
+ justOne: true
+ }).
+ set(function(user) {
+ if (user) {
+ this.userId = user._id;
+ this.userType = user.constructor.modelName;
+ } else {
+ this.userId = null;
+ this.userType = null;
+ }
+
+ return user;
+ });
+
+ const Person = db.model('Person', personSchema);
+
+ const person = new Person({
+ userId: user._id,
+ userType: 'User'
+ });
+
+ await person.save();
+
+ const personFromDb = await Person.findById(person._id).populate('user');
+ assert.equal(personFromDb.user.name, 'Admin');
+ assert.equal(personFromDb.userType, 'User');
+ assert.equal(personFromDb.userId.toHexString(), user._id.toHexString());
+ });
+
+ it('handles ref() function that returns a model (gh-14249)', async function() {
+ const aSchema = new Schema({
+ name: String
+ });
+
+ const bSchema = new Schema({
+ name: String
+ });
+
+ const CategoryAModel = db.model('Test', aSchema);
+ const CategoryBModel = db.model('Test1', bSchema);
+
+ const testSchema = new Schema({
+ category: String,
+ subdoc: {
+ type: Schema.Types.ObjectId,
+ ref: function() {
+ return this.category === 'catA' ? CategoryAModel : CategoryBModel;
+ }
+ }
+ });
+
+ const parentSchema = new Schema({
+ name: String,
+ children: [testSchema]
+ });
+ const Parent = db.model('Parent', parentSchema);
+
+ const A = await CategoryAModel.create({
+ name: 'A'
+ });
+ const B = await CategoryBModel.create({
+ name: 'B'
+ });
+
+ const doc = await Parent.create({
+ name: 'Parent',
+ children: [{ category: 'catA', subdoc: A._id }, { category: 'catB', subdoc: B._id }]
+ });
+
+ const res = await Parent.findById(doc._id).populate('children.subdoc');
+ assert.equal(res.children.length, 2);
+ assert.equal(res.children[0].subdoc.name, 'A');
+ assert.equal(res.children[1].subdoc.name, 'B');
+ });
+
+ it('avoids filtering out `null` values when applying match function (gh-14494)', async function() {
+ const gradeSchema = new mongoose.Schema({
+ studentId: mongoose.Types.ObjectId,
+ classId: mongoose.Types.ObjectId,
+ grade: String
+ });
+
+ const Grade = db.model('Test', gradeSchema);
+
+ const studentSchema = new mongoose.Schema({
+ name: String
+ });
+
+ studentSchema.virtual('grade', {
+ ref: Grade,
+ localField: '_id',
+ foreignField: 'studentId',
+ match: (doc) => ({
+ classId: doc._id
+ }),
+ justOne: true
+ });
+
+ const classSchema = new mongoose.Schema({
+ name: String,
+ students: [studentSchema]
+ });
+
+ const Class = db.model('Test2', classSchema);
+
+ const newClass = await Class.create({
+ name: 'History',
+ students: [{ name: 'Henry' }, { name: 'Robert' }]
+ });
+
+ const studentRobert = newClass.students.find(
+ ({ name }) => name === 'Robert'
+ );
+
+ await Grade.create({
+ studentId: studentRobert._id,
+ classId: newClass._id,
+ grade: 'B'
+ });
+
+ const latestClass = await Class.findOne({ name: 'History' }).populate('students.grade');
+
+ assert.equal(latestClass.students[0].name, 'Henry');
+ assert.equal(latestClass.students[0].grade, null);
+ assert.equal(latestClass.students[1].name, 'Robert');
+ assert.equal(latestClass.students[1].grade.grade, 'B');
+ });
+
+ it('avoids depopulating manually populated doc as getter value (gh-14759)', async function() {
+ const ownerSchema = new mongoose.Schema({
+ _id: {
+ type: 'ObjectId',
+ get(value) {
+ return value == null ? value : value.toString();
+ }
+ },
+ name: 'String'
+ });
+ const petSchema = new mongoose.Schema({
+ name: 'String',
+ owner: { type: 'ObjectId', ref: 'Owner' }
+ });
+
+ const Owner = db.model('Owner', ownerSchema);
+ const Pet = db.model('Pet', petSchema);
+
+ const ownerId = new mongoose.Types.ObjectId();
+ const owner = await Owner.create({
+ _id: ownerId,
+ name: 'Alice'
+ });
+ await Pet.create({ name: 'Kitty', owner: owner });
+
+ const fromDb = await Pet.findOne({ owner: ownerId }).lean().orFail();
+ assert.ok(fromDb.owner instanceof mongoose.Types.ObjectId);
+
+ const pet1 = new Pet({ name: 'Kitty1', owner: owner });
+ const pet2 = new Pet({ name: 'Kitty2', owner: owner });
+ assert.equal(pet1.owner.name, 'Alice');
+ assert.equal(pet2.owner.name, 'Alice');
+ });
+
+ it('avoids populating manually populated doc as getter value (gh-14827)', async function() {
+ const ownerSchema = new mongoose.Schema({
+ _id: {
+ type: 'ObjectId',
+ get(value) {
+ return value == null ? value : value.toString();
+ }
+ },
+ name: 'String'
+ });
+ const petSchema = new mongoose.Schema({
+ name: 'String',
+ owner: { type: 'ObjectId', ref: 'Owner' }
+ });
+
+ const Owner = db.model('Owner', ownerSchema);
+ const Pet = db.model('Pet', petSchema);
+
+ const _id = new mongoose.Types.ObjectId();
+ const owner = new Owner({ _id, name: 'Alice' });
+ const pet = new Pet({ name: 'Kitty', owner: owner });
+
+ await owner.save();
+
+ assert.equal(typeof pet._doc.owner.$__.wasPopulated.value, 'object');
+ await pet.populate('owner');
+ assert.equal(typeof pet._doc.owner.$__.wasPopulated.value, 'object');
+
+ await pet.save();
+
+
+ const fromDb = await Pet.findOne({ owner: _id }).lean().orFail();
+ assert.ok(fromDb.owner instanceof mongoose.Types.ObjectId);
+ });
+
+ it('makes sure that populate works correctly with duplicate foreignField with lean(); (gh-14794)', async function() {
+ const authorSchema = new mongoose.Schema({
+ group: String,
+ name: String
+ });
+
+ const postSchema = new mongoose.Schema({
+ authorGroup: String,
+ title: String,
+ content: String
+ });
+
+ const Author = db.model('Author', authorSchema);
+ const Post = db.model('Post', postSchema);
+
+ await Author.create({ group: 'AUTH1', name: 'John Doe' });
+ await Author.create({ group: 'AUTH2', name: 'Jane Smith' });
+ await Author.create({ group: 'AUTH2', name: 'Will Jons' });
+
+ await Post.create({
+ authorGroup: 'AUTH1',
+ title: 'First Post',
+ content: 'Content 1'
+ });
+
+ await Post.create({
+ authorGroup: 'AUTH2',
+ title: 'Second Post',
+ content: 'Content 2'
+ });
+
+ const posts = await Post.find()
+ .populate({
+ path: 'authorGroup',
+ model: 'Author',
+ select: { _id: 1, name: 1 },
+ foreignField: 'group'
+ })
+ .select({ _id: 1, authorGroup: 1, title: 1 })
+ .lean();
+
+ for (const post of posts) {
+ assert.ok(post.authorGroup._id instanceof mongoose.Types.ObjectId);
+ assert.ok(typeof post.authorGroup.name === 'string');
+ }
+ assert.equal(posts.length, 2);
+ });
+
+ it('depopulates if pushing ObjectId to a populated array (gh-1635)', async function() {
+ const ParentModel = db.model('Test', mongoose.Schema({
+ name: String,
+ children: [{ type: 'ObjectId', ref: 'Child' }]
+ }));
+ const ChildModel = db.model('Child', mongoose.Schema({ name: String }));
+
+ const children = await ChildModel.create([{ name: 'Luke' }, { name: 'Leia' }]);
+ const newChild = await ChildModel.create({ name: 'Taco' });
+ const { _id } = await ParentModel.create({ name: 'Anakin', children });
+
+ const doc = await ParentModel.findById(_id).populate('children');
+ doc.children.push(newChild._id);
+
+ assert.ok(doc.children[0] instanceof mongoose.Types.ObjectId);
+ assert.ok(doc.children[1] instanceof mongoose.Types.ObjectId);
+ assert.ok(doc.children[2] instanceof mongoose.Types.ObjectId);
+
+ await doc.save();
+
+ const fromDb = await ParentModel.findById(_id);
+ assert.equal(fromDb.children[0].toHexString(), children[0]._id.toHexString());
+ assert.equal(fromDb.children[1].toHexString(), children[1]._id.toHexString());
+ assert.equal(fromDb.children[2].toHexString(), newChild._id.toHexString());
+ });
+
+ it('handles converting uuid documents to strings when calling toObject() (gh-14869)', async function() {
+ const nodeSchema = new Schema({ _id: { type: 'UUID' }, name: 'String' });
+ const rootSchema = new Schema({
+ _id: { type: 'UUID' },
+ status: 'String',
+ node: [{ type: 'UUID', ref: 'Child' }]
+ });
+
+ const Node = db.model('Child', nodeSchema);
+ const Root = db.model('Parent', rootSchema);
+
+ const node = new Node({
+ _id: '65c7953e-c6e9-4c2f-8328-fe2de7df560d',
+ name: 'test'
+ });
+ await node.save();
+
+ const root = new Root({
+ _id: '05c7953e-c6e9-4c2f-8328-fe2de7df560d',
+ status: 'ok',
+ node: [node._id]
+ });
+ await root.save();
+
+ const foundRoot = await Root.findById(root._id).populate('node');
+
+ let doc = foundRoot.toJSON({ getters: true });
+ assert.strictEqual(doc._id, '05c7953e-c6e9-4c2f-8328-fe2de7df560d');
+ assert.strictEqual(doc.node.length, 1);
+ assert.strictEqual(doc.node[0]._id, '65c7953e-c6e9-4c2f-8328-fe2de7df560d');
+
+ doc = foundRoot.toObject({ getters: true });
+ assert.strictEqual(doc._id, '05c7953e-c6e9-4c2f-8328-fe2de7df560d');
+ assert.strictEqual(doc.node.length, 1);
+ assert.strictEqual(doc.node[0]._id, '65c7953e-c6e9-4c2f-8328-fe2de7df560d');
+ });
+
+ it('avoids repopulating if forceRepopulate is disabled (gh-14979)', async function() {
+ const ChildSchema = new Schema({ name: String });
+ const ParentSchema = new Schema({
+ children: [{ type: Schema.Types.ObjectId, ref: 'Child' }],
+ child: { type: 'ObjectId', ref: 'Child' }
+ });
+
+ const Child = db.model('Child', ChildSchema);
+ const Parent = db.model('Parent', ParentSchema);
+
+ const child = await Child.create({ name: 'Child test' });
+ let parent = await Parent.create({ child: child._id, children: [child._id] });
+
+ parent = await Parent.findOne({ _id: parent._id }).populate(['child', 'children']).orFail();
+ child.name = 'Child test updated 1';
+ await child.save();
+
+ await parent.populate({ path: 'child', forceRepopulate: false });
+ await parent.populate({ path: 'children', forceRepopulate: false });
+ assert.equal(parent.child.name, 'Child test');
+ assert.equal(parent.children[0].name, 'Child test');
+
+ await Parent.populate([parent], { path: 'child', forceRepopulate: false });
+ await Parent.populate([parent], { path: 'children', forceRepopulate: false });
+ assert.equal(parent.child.name, 'Child test');
+ assert.equal(parent.children[0].name, 'Child test');
+
+ parent.depopulate('child');
+ parent.depopulate('children');
+ await parent.populate({ path: 'child', forceRepopulate: false });
+ await parent.populate({ path: 'children', forceRepopulate: false });
+ assert.equal(parent.child.name, 'Child test updated 1');
+ assert.equal(parent.children[0].name, 'Child test updated 1');
+ });
+
+ it('handles forceRepopulate as a global option (gh-14979)', async function() {
+ const m = new mongoose.Mongoose();
+ m.set('forceRepopulate', false);
+ await m.connect(start.uri);
+ const ChildSchema = new m.Schema({ name: String });
+ const ParentSchema = new m.Schema({
+ children: [{ type: Schema.Types.ObjectId, ref: 'Child' }],
+ child: { type: 'ObjectId', ref: 'Child' }
+ });
+
+ const Child = m.model('Child', ChildSchema);
+ const Parent = m.model('Parent', ParentSchema);
+
+ const child = await Child.create({ name: 'Child test' });
+ let parent = await Parent.create({ child: child._id, children: [child._id] });
+
+ parent = await Parent.findOne({ _id: parent._id }).populate(['child', 'children']).orFail();
+ child.name = 'Child test updated 1';
+ await child.save();
+
+ await parent.populate({ path: 'child' });
+ await parent.populate({ path: 'children' });
+ assert.equal(parent.child.name, 'Child test');
+ assert.equal(parent.children[0].name, 'Child test');
+
+ await Parent.populate([parent], { path: 'child' });
+ await Parent.populate([parent], { path: 'children' });
+ assert.equal(parent.child.name, 'Child test');
+ assert.equal(parent.children[0].name, 'Child test');
+
+ parent.depopulate('child');
+ parent.depopulate('children');
+ await parent.populate({ path: 'child' });
+ await parent.populate({ path: 'children' });
+ assert.equal(parent.child.name, 'Child test updated 1');
+ assert.equal(parent.children[0].name, 'Child test updated 1');
+
+ child.name = 'Child test updated 2';
+ await child.save();
+
+ parent.depopulate('child');
+ parent.depopulate('children');
+ await parent.populate({ path: 'child', forceRepopulate: true });
+ await parent.populate({ path: 'children', forceRepopulate: true });
+ assert.equal(parent.child.name, 'Child test updated 2');
+ assert.equal(parent.children[0].name, 'Child test updated 2');
+
+ await m.disconnect();
+ });
});
diff --git a/test/model.query.casting.test.js b/test/model.query.casting.test.js
index 83d1e866b44..7f0e863bfe7 100644
--- a/test/model.query.casting.test.js
+++ b/test/model.query.casting.test.js
@@ -71,136 +71,88 @@ describe('model query casting', function() {
geoSchemaObject.index({ loc: '2d' });
});
- it('works', function(done) {
+ it('works', async function() {
const title = 'Loki ' + random();
const post = new BlogPostB();
const id = post.get('_id').toString();
post.set('title', title);
-
- post.save(function(err) {
- assert.ifError(err);
-
- BlogPostB.findOne({ _id: id }, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.get('title'), title);
- done();
- });
- });
+ await post.save();
+ const doc = await BlogPostB.findOne({ _id: id });
+ assert.equal(doc.get('title'), title);
});
- it('returns cast errors', function(done) {
- BlogPostB.find({ date: 'invalid date' }, function(err) {
- assert.ok(err instanceof Error);
- assert.ok(err instanceof CastError);
- done();
- });
+ it('returns cast errors', async function() {
+ const err = await BlogPostB.find({ date: 'invalid date' }).then(() => null, err => err);
+ assert.ok(err instanceof Error);
+ assert.ok(err instanceof CastError);
});
- it('casts $modifiers', function(done) {
+ it('casts $modifiers', async function() {
const post = new BlogPostB({
meta: {
visitors: -75
}
});
-
- post.save(function(err) {
- assert.ifError(err);
-
- BlogPostB.find({ 'meta.visitors': { $gt: '-100', $lt: -50 } },
- function(err, found) {
- assert.ifError(err);
-
- assert.ok(found);
- assert.equal(found.length, 1);
- assert.equal(found[0].get('_id').toString(), post.get('_id'));
- assert.equal(found[0].get('meta.visitors').valueOf(), post.get('meta.visitors').valueOf());
- done();
- });
- });
+ await post.save();
+ const found = await BlogPostB.find({ 'meta.visitors': { $gt: '-100', $lt: -50 } });
+ assert.ok(found);
+ assert.equal(found.length, 1);
+ assert.equal(found[0].get('_id').toString(), post.get('_id'));
+ assert.equal(found[0].get('meta.visitors').valueOf(), post.get('meta.visitors').valueOf());
});
- it('casts $in values of arrays (gh-199)', function(done) {
+ it('casts $in values of arrays (gh-199)', async function() {
const post = new BlogPostB();
const id = post._id.toString();
-
- post.save(function(err) {
- assert.ifError(err);
-
- BlogPostB.findOne({ _id: { $in: [id] } }, function(err, doc) {
- assert.ifError(err);
-
- assert.equal(doc._id.toString(), id);
- done();
- });
- });
+ await post.save();
+ const doc = await BlogPostB.findOne({ _id: { $in: [id] } });
+ assert.equal(doc._id.toString(), id);
});
- it('casts $in values of arrays with single item instead of array (jrl-3238)', function(done) {
+ it('casts $in values of arrays with single item instead of array (gh-3238)', async function() {
const post = new BlogPostB();
const id = post._id.toString();
- post.save(function(err) {
- assert.ifError(err);
-
- BlogPostB.findOne({ _id: { $in: id } }, function(err, doc) {
- assert.ifError(err);
+ await post.save();
- assert.equal(doc._id.toString(), id);
- done();
- });
- });
+ const doc = await BlogPostB.findOne({ _id: { $in: id } });
+ assert.equal(doc._id.toString(), id);
});
- it('casts $nin values of arrays (gh-232)', function(done) {
+ it('casts $nin values of arrays (gh-232)', async function() {
const NinSchema = new Schema({
num: Number
});
const Nin = db.model('Test', NinSchema);
- Nin.create({ num: 1 }, function(err) {
- assert.ifError(err);
- Nin.create({ num: 2 }, function(err) {
- assert.ifError(err);
- Nin.create({ num: 3 }, function(err) {
- assert.ifError(err);
- Nin.find({ num: { $nin: [2] } }, function(err, found) {
- assert.ifError(err);
- assert.equal(found.length, 2);
- done();
- });
- });
- });
- });
+ await Nin.create([
+ { num: 1 },
+ { num: 2 },
+ { num: 3 }
+ ]);
+
+ const found = await Nin.find({ num: { $nin: [2] } });
+ assert.equal(found.length, 2);
});
- it('works when finding by Date (gh-204)', function(done) {
+ it('works when finding by Date (gh-204)', async function() {
const P = BlogPostB;
const post = new P();
post.meta.date = new Date();
+ await post.save();
- post.save(function(err) {
- assert.ifError(err);
-
- P.findOne({ _id: post._id, 'meta.date': { $lte: Date.now() } }, function(err, doc) {
- assert.ifError(err);
-
- assert.equal(doc._id.toString(), post._id.toString());
- doc.meta.date = null;
- doc.save(function(err) {
- assert.ifError(err);
- P.findById(doc._id, function(err, doc) {
- assert.ifError(err);
- assert.strictEqual(doc.meta.date, null);
- done();
- });
- });
- });
- });
+ let doc = await P.findOne({ _id: post._id, 'meta.date': { $lte: Date.now() } });
+ assert.equal(doc._id.toString(), post._id.toString());
+ doc.meta.date = null;
+ await doc.save();
+
+ doc = await P.findById(doc._id);
+ assert.strictEqual(doc.meta.date, null);
});
it('works with $type matching', async function() {
@@ -222,220 +174,101 @@ describe('model query casting', function() {
assert.equal(posts.length, 2);
});
- it('works when finding Boolean with $in (gh-998)', function(done) {
+ it('works when finding Boolean with $in (gh-998)', async function() {
const B = BlogPostB;
const b = new B({ published: true });
- b.save(function(err) {
- assert.ifError(err);
- B.find({ _id: b._id, boolean: { $in: [null, true] } }, function(err, doc) {
- assert.ifError(err);
- assert.ok(doc);
- assert.equal(doc[0].id, b.id);
- done();
- });
- });
+ await b.save();
+ const doc = await B.find({ _id: b._id, boolean: { $in: [null, true] } });
+ assert.ok(doc);
+ assert.equal(doc[0].id, b.id);
});
- it('works when finding Boolean with $ne (gh-1093)', function(done) {
+ it('works when finding Boolean with $ne (gh-1093)', async function() {
const B = BlogPostB;
const b = new B({ published: false });
- b.save(function(err) {
- assert.ifError(err);
- B.find().ne('published', true).exec(function(err, doc) {
- assert.ifError(err);
- assert.ok(doc);
- assert.equal(doc[0].id, b.id);
- done();
- });
- });
+ await b.save();
+ const doc = await B.find().ne('published', true).exec();
+ assert.ok(doc);
+ assert.equal(doc[0].id, b.id);
});
- it('properly casts $and (gh-1180)', function(done) {
+ it('properly casts $and (gh-1180)', function() {
const B = BlogPostB;
const result = B.find({}).cast(B, { $and: [{ date: '1987-03-17T20:00:00.000Z' }, { _id: '000000000000000000000000' }] });
assert.ok(result.$and[0].date instanceof Date);
assert.ok(result.$and[1]._id instanceof DocumentObjectId);
- done();
});
describe('$near', function() {
this.slow(60);
- it('with arrays', function(done) {
+ it('with arrays', async function() {
const Test = db.model('Test', geoSchemaArray);
+ await Test.init();
+ await Test.create({ loc: [10, 20] }, { loc: [40, 90] });
- Test.once('index', complete);
- Test.create({ loc: [10, 20] }, { loc: [40, 90] }, complete);
-
- let pending = 2;
-
- function complete(err) {
- if (complete.ran) {
- return;
- }
- if (err) {
- return done(complete.ran = err);
- }
- --pending || test();
- }
-
- function test() {
- Test.find({ loc: { $near: ['30', '40'] } }, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 2);
- done();
- });
- }
+ const docs = await Test.find({ loc: { $near: ['30', '40'] } });
+ assert.equal(docs.length, 2);
});
- it('with objects', function(done) {
+ it('with objects', async function() {
const Test = db.model('Test', geoSchemaObject);
+ await Test.init();
+ await Test.create({ loc: { long: 10, lat: 20 } }, { loc: { long: 40, lat: 90 } });
- let pending = 2;
-
- function complete(err) {
- if (complete.ran) {
- return;
- }
- if (err) {
- return done(complete.ran = err);
- }
- --pending || test();
- }
-
- function test() {
- Test.find({ loc: { $near: ['30', '40'], $maxDistance: 51 } }, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 2);
- done();
- });
- }
-
- Test.create({ loc: { long: 10, lat: 20 } }, { loc: { long: 40, lat: 90 } }, complete);
- Test.once('index', complete);
+ const docs = await Test.find({ loc: { $near: ['30', '40'], $maxDistance: 51 } });
+ assert.equal(docs.length, 2);
});
- it('with nested objects', function(done) {
+ it('with nested objects', async function() {
const geoSchemaObject = new Schema({ loc: { nested: { long: Number, lat: Number } } });
geoSchemaObject.index({ 'loc.nested': '2d' });
const Test = db.model('Test', geoSchemaObject);
-
- let pending = 2;
-
- function complete(err) {
- if (complete.ran) {
- return;
- }
- if (err) {
- return done(complete.ran = err);
- }
- --pending || test();
- }
-
- function test() {
- Test.find({ 'loc.nested': { $near: ['30', '40'], $maxDistance: '50' } }, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 1);
- done();
- });
- }
-
- Test.once('index', complete);
- Test.create(
+ await Test.init();
+ await Test.create(
{ loc: { nested: { long: 10, lat: 20 } } },
- { loc: { nested: { long: 40, lat: 90 } } },
- complete);
+ { loc: { nested: { long: 40, lat: 90 } } }
+ );
+
+ const docs = await Test.find({ 'loc.nested': { $near: ['30', '40'], $maxDistance: '50' } });
+ assert.equal(docs.length, 1);
});
});
describe('$nearSphere', function() {
this.slow(70);
- it('with arrays', function(done) {
+ it('with arrays', async function() {
const Test = db.model('Test', geoSchemaArray);
+ await Test.init();
+ await Test.create({ loc: [10, 20] }, { loc: [40, 90] });
- let pending = 2;
-
- function complete(err) {
- if (complete.ran) {
- return;
- }
- if (err) {
- return done(complete.err = err);
- }
- --pending || test();
- }
-
- Test.on('index', complete);
- Test.create({ loc: [10, 20] }, { loc: [40, 90] }, complete);
-
- function test() {
- Test.find({ loc: { $nearSphere: ['30', '40'] } }, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 2);
- done();
- });
- }
+ const docs = await Test.find({ loc: { $nearSphere: ['30', '40'] } });
+ assert.equal(docs.length, 2);
});
- it('with objects', function(done) {
+ it('with objects', async function() {
const Test = db.model('Test', geoSchemaObject);
+ await Test.init();
+ await Test.create({ loc: { long: 10, lat: 20 } }, { loc: { long: 40, lat: 90 } });
- let pending = 2;
-
- function complete(err) {
- if (complete.ran) {
- return;
- }
- if (err) {
- return done(complete.err = err);
- }
- --pending || test();
- }
-
- Test.on('index', complete);
- Test.create({ loc: { long: 10, lat: 20 } }, { loc: { long: 40, lat: 90 } }, complete);
-
- function test() {
- Test.find({ loc: { $nearSphere: ['30', '40'], $maxDistance: 1 } }, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 2);
- done();
- });
- }
+ const docs = await Test.find({ loc: { $nearSphere: ['30', '40'], $maxDistance: 1 } });
+ assert.equal(docs.length, 2);
});
- it('with nested objects', function(done) {
+ it('with nested objects', async function() {
const geoSchemaObject = new Schema({ loc: { nested: { long: Number, lat: Number } } });
geoSchemaObject.index({ 'loc.nested': '2d' });
const Test = db.model('Test', geoSchemaObject);
+ await Test.init();
+ await Test.create({ loc: { nested: { long: 10, lat: 20 } } }, { loc: { nested: { long: 40, lat: 90 } } });
- let pending = 2;
-
- function complete(err) {
- if (complete.ran) {
- return;
- }
- if (err) {
- return done(complete.err = err);
- }
- --pending || test();
- }
-
- Test.on('index', complete);
- Test.create({ loc: { nested: { long: 10, lat: 20 } } }, { loc: { nested: { long: 40, lat: 90 } } }, complete);
-
- function test() {
- Test.find({ 'loc.nested': { $nearSphere: ['30', '40'], $maxDistance: 1 } }, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 2);
- done();
- });
- }
+ const docs = await Test.find({ 'loc.nested': { $nearSphere: ['30', '40'], $maxDistance: 1 } });
+ assert.equal(docs.length, 2);
});
});
@@ -443,352 +276,142 @@ describe('model query casting', function() {
this.slow(60);
describe('$centerSphere', function() {
- it('with arrays', function(done) {
+ it('with arrays', async function() {
const Test = db.model('Test', geoSchemaArray);
+ await Test.init();
+ await Test.create({ loc: [10, 20] }, { loc: [40, 90] });
- let pending = 2;
-
- function complete(err) {
- if (complete.ran) {
- return;
- }
- if (err) {
- return done(complete.err = err);
- }
- --pending || test();
- }
-
- Test.on('index', complete);
- Test.create({ loc: [10, 20] }, { loc: [40, 90] }, complete);
-
- function test() {
- Test.find({ loc: { $within: { $centerSphere: [['11', '20'], '0.4'] } } }, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 1);
- done();
- });
- }
+ const docs = await Test.find({ loc: { $within: { $centerSphere: [['11', '20'], '0.4'] } } });
+ assert.equal(docs.length, 1);
});
- it('with objects', function(done) {
+ it('with objects', async function() {
const Test = db.model('Test', geoSchemaObject);
+ await Test.init();
+ await Test.create({ loc: { long: 10, lat: 20 } }, { loc: { long: 40, lat: 90 } });
- let pending = 2;
-
- function complete(err) {
- if (complete.ran) {
- return;
- }
- if (err) {
- return done(complete.err = err);
- }
- --pending || test();
- }
-
- Test.on('index', complete);
- Test.create({ loc: { long: 10, lat: 20 } }, { loc: { long: 40, lat: 90 } }, complete);
-
- function test() {
- Test.find({ loc: { $within: { $centerSphere: [['11', '20'], '0.4'] } } }, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 1);
- done();
- });
- }
+ const docs = await Test.find({ loc: { $within: { $centerSphere: [['11', '20'], '0.4'] } } });
+ assert.equal(docs.length, 1);
});
- it('with nested objects', function(done) {
+ it('with nested objects', async function() {
const geoSchemaObject = new Schema({ loc: { nested: { long: Number, lat: Number } } });
geoSchemaObject.index({ 'loc.nested': '2d' });
const Test = db.model('Test', geoSchemaObject);
+ await Test.init();
+ await Test.create({ loc: { nested: { long: 10, lat: 20 } } }, { loc: { nested: { long: 40, lat: 90 } } });
- let pending = 2;
-
- function complete(err) {
- if (complete.ran) {
- return;
- }
- if (err) {
- return done(complete.err = err);
- }
- --pending || test();
- }
-
- Test.on('index', complete);
- Test.create({ loc: { nested: { long: 10, lat: 20 } } }, { loc: { nested: { long: 40, lat: 90 } } }, complete);
-
- function test() {
- Test.find({ 'loc.nested': { $within: { $centerSphere: [['11', '20'], '0.4'] } } }, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 1);
- done();
- });
- }
+ const docs = await Test.find({ 'loc.nested': { $within: { $centerSphere: [['11', '20'], '0.4'] } } });
+ assert.equal(docs.length, 1);
});
});
describe('$center', function() {
- it('with arrays', function(done) {
+ it('with arrays', async function() {
const Test = db.model('Test', geoSchemaArray);
+ await Test.init();
+ await Test.create({ loc: [10, 20] }, { loc: [40, 90] });
- let pending = 2;
-
- function complete(err) {
- if (complete.ran) {
- return;
- }
- if (err) {
- return done(complete.err = err);
- }
- --pending || test();
- }
-
- Test.on('index', complete);
- Test.create({ loc: [10, 20] }, { loc: [40, 90] }, complete);
-
- function test() {
- Test.find({ loc: { $within: { $center: [['11', '20'], '1'] } } }, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 1);
- done();
- });
- }
+ const docs = await Test.find({ loc: { $within: { $center: [['11', '20'], '1'] } } });
+ assert.equal(docs.length, 1);
});
- it('with objects', function(done) {
+ it('with objects', async function() {
const Test = db.model('Test', geoSchemaObject);
+ await Test.init();
+ await Test.create({ loc: { long: 10, lat: 20 } }, { loc: { long: 40, lat: 90 } });
- let pending = 2;
-
- function complete(err) {
- if (complete.ran) {
- return;
- }
- if (err) {
- return done(complete.err = err);
- }
- --pending || test();
- }
-
- Test.on('index', complete);
- Test.create({ loc: { long: 10, lat: 20 } }, { loc: { long: 40, lat: 90 } }, complete);
-
- function test() {
- Test.find({ loc: { $within: { $center: [['11', '20'], '1'] } } }, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 1);
- done();
- });
- }
+ const docs = await Test.find({ loc: { $within: { $center: [['11', '20'], '1'] } } });
+ assert.equal(docs.length, 1);
});
- it('with nested objects', function(done) {
+ it('with nested objects', async function() {
const geoSchemaObject = new Schema({ loc: { nested: { long: Number, lat: Number } } });
geoSchemaObject.index({ 'loc.nested': '2d' });
const Test = db.model('Test', geoSchemaObject);
+ await Test.init();
- let pending = 2;
-
- function complete(err) {
- if (complete.ran) {
- return;
- }
- if (err) {
- return done(complete.err = err);
- }
- --pending || test();
- }
-
- Test.on('index', complete);
- Test.create({ loc: { nested: { long: 10, lat: 20 } } }, { loc: { nested: { long: 40, lat: 90 } } }, complete);
-
- function test() {
- Test.find({ 'loc.nested': { $within: { $center: [['11', '20'], '1'] } } }, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 1);
- done();
- });
- }
+ await Test.create({ loc: { nested: { long: 10, lat: 20 } } }, { loc: { nested: { long: 40, lat: 90 } } });
+ const docs = await Test.find({ 'loc.nested': { $within: { $center: [['11', '20'], '1'] } } });
+ assert.equal(docs.length, 1);
});
});
describe('$polygon', function() {
- it('with arrays', function(done) {
+ it('with arrays', async function() {
const Test = db.model('Test', geoSchemaArray);
+ await Test.init();
- let pending = 2;
-
- function complete(err) {
- if (complete.ran) {
- return;
- }
- if (err) {
- return done(complete.err = err);
- }
- --pending || test();
- }
-
- Test.on('index', complete);
- Test.create({ loc: [10, 20] }, { loc: [40, 90] }, complete);
-
- function test() {
- Test.find({ loc: { $within: { $polygon: [['8', '1'], ['8', '100'], ['50', '100'], ['50', '1']] } } }, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 2);
- done();
- });
- }
+ await Test.create({ loc: [10, 20] }, { loc: [40, 90] });
+ const docs = await Test.find({ loc: { $within: { $polygon: [['8', '1'], ['8', '100'], ['50', '100'], ['50', '1']] } } });
+ assert.equal(docs.length, 2);
});
- it('with objects', function(done) {
+ it('with objects', async function() {
const Test = db.model('Test', geoSchemaObject);
+ await Test.init();
- let pending = 2;
-
- function complete(err) {
- if (complete.ran) {
- return;
- }
- if (err) {
- return done(complete.err = err);
- }
- --pending || test();
- }
-
- Test.on('index', complete);
- Test.create({ loc: { long: 10, lat: 20 } }, { loc: { long: 40, lat: 90 } }, complete);
+ await Test.create({ loc: { long: 10, lat: 20 } }, { loc: { long: 40, lat: 90 } });
- function test() {
- Test.find({ loc: { $within: { $polygon: [['8', '1'], ['8', '100'], ['50', '100'], ['50', '1']] } } }, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 2);
- done();
- });
- }
+ const docs = await Test.find({ loc: { $within: { $polygon: [['8', '1'], ['8', '100'], ['50', '100'], ['50', '1']] } } });
+ assert.equal(docs.length, 2);
});
- it('with nested objects', function(done) {
+ it('with nested objects', async function() {
const geoSchemaObject = new Schema({ loc: { nested: { long: Number, lat: Number } } });
geoSchemaObject.index({ 'loc.nested': '2d' });
const Test = db.model('Test', geoSchemaObject);
+ await Test.init();
+ await Test.create({ loc: { nested: { long: 10, lat: 20 } } }, { loc: { nested: { long: 40, lat: 90 } } });
- let pending = 2;
-
- function complete(err) {
- if (complete.ran) {
- return;
- }
- if (err) {
- return done(complete.err = err);
- }
- --pending || test();
- }
-
- Test.on('index', complete);
- Test.create({ loc: { nested: { long: 10, lat: 20 } } }, { loc: { nested: { long: 40, lat: 90 } } }, complete);
-
- function test() {
- Test.find({ 'loc.nested': { $within: { $polygon: [['8', '1'], ['8', '100'], ['50', '100'], ['50', '1']] } } }, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 2);
- done();
- });
- }
+ const docs = await Test.find({ 'loc.nested': { $within: { $polygon: [['8', '1'], ['8', '100'], ['50', '100'], ['50', '1']] } } });
+ assert.equal(docs.length, 2);
});
});
describe('$box', function() {
- it('with arrays', function(done) {
+ it('with arrays', async function() {
const Test = db.model('Test', geoSchemaArray);
+ await Test.init();
- let pending = 2;
-
- function complete(err) {
- if (complete.ran) {
- return;
- }
- if (err) {
- return done(complete.err = err);
- }
- --pending || test();
- }
-
- Test.on('index', complete);
- Test.create({ loc: [10, 20] }, { loc: [40, 90] }, complete);
-
- function test() {
- Test.find({ loc: { $within: { $box: [['8', '1'], ['50', '100']] } } }, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 2);
- done();
- });
- }
+ await Test.create({ loc: [10, 20] }, { loc: [40, 90] });
+ const docs = await Test.find({ loc: { $within: { $box: [['8', '1'], ['50', '100']] } } });
+ assert.equal(docs.length, 2);
});
- it('with objects', function(done) {
+ it('with objects', async function() {
const Test = db.model('Test', geoSchemaObject);
+ await Test.init();
- let pending = 2;
-
- function complete(err) {
- if (complete.ran) {
- return;
- }
- if (err) {
- return done(complete.err = err);
- }
- --pending || test();
- }
-
- Test.on('index', complete);
- Test.create({ loc: { long: 10, lat: 20 } }, { loc: { long: 40, lat: 90 } }, complete);
+ await Test.create({ loc: { long: 10, lat: 20 } }, { loc: { long: 40, lat: 90 } });
- function test() {
- Test.find({ loc: { $within: { $box: [['8', '1'], ['50', '100']] } } }, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 2);
- done();
- });
- }
+ const docs = await Test.find({ loc: { $within: { $box: [['8', '1'], ['50', '100']] } } });
+ assert.equal(docs.length, 2);
});
- it('with nested objects', function(done) {
+ it('with nested objects', async function() {
const geoSchemaObject = new Schema({ loc: { nested: { long: Number, lat: Number } } });
geoSchemaObject.index({ 'loc.nested': '2d' });
const Test = db.model('Test', geoSchemaObject);
+ await Test.init();
- let pending = 2;
-
- function complete(err) {
- if (complete.ran) {
- return;
- }
- if (err) {
- return done(complete.err = err);
- }
- --pending || test();
- }
-
- Test.on('index', complete);
- Test.create({ loc: { nested: { long: 10, lat: 20 } } }, { loc: { nested: { long: 40, lat: 90 } } }, complete);
+ await Test.create(
+ { loc: { nested: { long: 10, lat: 20 } } },
+ { loc: { nested: { long: 40, lat: 90 } } }
+ );
- function test() {
- Test.find({ 'loc.nested': { $within: { $box: [['8', '1'], ['50', '100']] } } }, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 2);
- done();
- });
- }
+ const docs = await Test.find({ 'loc.nested': { $within: { $box: [['8', '1'], ['50', '100']] } } });
+ assert.equal(docs.length, 2);
});
});
});
describe('$options', function() {
- it('works on arrays gh-1462', function(done) {
+ it('works on arrays gh-1462', function() {
const opts = {};
opts.toString = function() {
return 'img';
@@ -798,9 +421,8 @@ describe('model query casting', function() {
const result = B.find({}).cast(B, { tags: { $regex: /a/, $options: opts } });
assert.equal(result.tags.$options, 'img');
- done();
});
- it('does not cast with uppercase (gh-7800)', function(done) {
+ it('does not cast with uppercase (gh-7800)', function() {
const testSchema = new Schema({
name: { type: String, uppercase: true }
});
@@ -809,48 +431,36 @@ describe('model query casting', function() {
const result = Model.find({}).cast(Model, { name: { $regex: /a/, $options: 'i' } });
assert.equal(result.name.$options, 'i');
- done();
});
});
describe('$elemMatch', function() {
- it('should cast String to ObjectId in $elemMatch', function(done) {
+ it('should cast String to ObjectId in $elemMatch', async function() {
const commentId = new mongoose.Types.ObjectId(111);
const post = new BlogPostB({ comments: [{ _id: commentId }] });
const id = post._id.toString();
- post.save(function(err) {
- assert.ifError(err);
-
- BlogPostB.findOne({ _id: id, comments: { $elemMatch: { _id: commentId.toString() } } }, function(err, doc) {
- assert.ifError(err);
-
- assert.equal(doc._id.toString(), id);
- done();
- });
- });
+ await post.save();
+ const doc = await BlogPostB.findOne({ _id: id, comments: { $elemMatch: { _id: commentId.toString() } } });
+ assert.equal(doc._id.toString(), id);
});
- it('should cast String to ObjectId in $elemMatch inside $not', function(done) {
+ it('should cast String to ObjectId in $elemMatch inside $not', async function() {
const commentId = new mongoose.Types.ObjectId(111);
const post = new BlogPostB({ comments: [{ _id: commentId }] });
const id = post._id.toString();
- post.save(function(err) {
- assert.ifError(err);
-
- BlogPostB.findOne({ _id: id, comments: { $not: { $elemMatch: { _id: commentId.toString() } } } }, function(err, doc) {
- assert.ifError(err);
-
- assert.equal(doc, null);
- done();
- });
+ await post.save();
+ const doc = await BlogPostB.findOne({
+ _id: id,
+ comments: { $not: { $elemMatch: { _id: commentId.toString() } } }
});
+ assert.equal(doc, null);
});
- it('should cast subdoc _id typed as String to String in $elemMatch gh3719', function(done) {
+ it('should cast subdoc _id typed as String to String in $elemMatch gh3719', async function() {
const child = new Schema({
_id: { type: String }
}, { _id: false });
@@ -861,24 +471,14 @@ describe('model query casting', function() {
const Parent = db.model('Parent', parent);
- Parent.create({ children: [{ _id: 'foobar' }] }, function(error) {
- assert.ifError(error);
- test();
+ await Parent.create({ children: [{ _id: 'foobar' }] });
+ const docs = await Parent.find({
+ $and: [{ children: { $elemMatch: { _id: 'foobar' } } }]
});
-
- function test() {
- Parent.find({
- $and: [{ children: { $elemMatch: { _id: 'foobar' } } }]
- }, function(error, docs) {
- assert.ifError(error);
-
- assert.equal(docs.length, 1);
- done();
- });
- }
+ assert.equal(docs.length, 1);
});
- it('should cast subdoc _id typed as String to String in $elemMatch inside $not gh3719', function(done) {
+ it('should cast subdoc _id typed as String to String in $elemMatch inside $not gh3719', async function() {
const child = new Schema({
_id: { type: String }
}, { _id: false });
@@ -889,21 +489,12 @@ describe('model query casting', function() {
const Parent = db.model('Parent', parent);
- Parent.create({ children: [{ _id: 'foobar' }] }, function(error) {
- assert.ifError(error);
- test();
- });
+ await Parent.create({ children: [{ _id: 'foobar' }] });
- function test() {
- Parent.find({
- $and: [{ children: { $not: { $elemMatch: { _id: 'foobar' } } } }]
- }, function(error, docs) {
- assert.ifError(error);
-
- assert.equal(docs.length, 0);
- done();
- });
- }
+ const docs = await Parent.find({
+ $and: [{ children: { $not: { $elemMatch: { _id: 'foobar' } } } }]
+ });
+ assert.equal(docs.length, 0);
});
it('casts $nor within $elemMatch (gh-9479)', async function() {
@@ -921,34 +512,25 @@ describe('model query casting', function() {
});
});
- it('works with $all (gh-3394)', function(done) {
+ it('works with $all (gh-3394)', async function() {
const MyModel = db.model('Test', { tags: [ObjectId] });
- const doc = {
+ const savedDoc = await MyModel.create({
tags: ['00000000000000000000000a', '00000000000000000000000b']
- };
-
- MyModel.create(doc, function(error, savedDoc) {
- assert.ifError(error);
- assert.equal(typeof savedDoc.tags[0], 'object');
- MyModel.findOne({ tags: { $all: doc.tags } }, function(error, doc) {
- assert.ifError(error);
- assert.ok(doc);
- done();
- });
});
+ assert.equal(typeof savedDoc.tags[0], 'object');
+
+ const doc = await MyModel.findOne({ tags: { $all: savedDoc.tags } });
+ assert.ok(doc);
});
- it('date with $not + $type (gh-4632)', function(done) {
+ it('date with $not + $type (gh-4632)', async function() {
const MyModel = db.model('Test', { test: Date });
- MyModel.find({ test: { $not: { $type: 9 } } }, function(error) {
- assert.ifError(error);
- done();
- });
+ await MyModel.find({ test: { $not: { $type: 9 } } });
});
- it('setOnInsert with custom type (gh-5126)', function(done) {
+ it('setOnInsert with custom type (gh-5126)', async function() {
function Point(key, options) {
mongoose.SchemaType.call(this, key, options, 'Point');
}
@@ -977,15 +559,11 @@ describe('model query casting', function() {
}
}
};
- Test.findOneAndUpdate({ name: 'a' }, u).
- exec(function(error) {
- assert.ifError(error);
- assert.equal(called, 1);
- done();
- });
+ await Test.findOneAndUpdate({ name: 'a' }, u).exec();
+ assert.equal(called, 1);
});
- it('lowercase in query (gh-4569)', function(done) {
+ it('lowercase in query (gh-4569)', async function() {
const contexts = [];
const testSchema = new Schema({
@@ -1000,7 +578,7 @@ describe('model query casting', function() {
});
const Test = db.model('Test', testSchema);
- Test.create({ name: 'val', num: 2.02 }).
+ return Test.create({ name: 'val', num: 2.02 }).
then(function() {
assert.equal(contexts.length, 1);
assert.equal(contexts[0].constructor.name, 'model');
@@ -1020,12 +598,10 @@ describe('model query casting', function() {
assert.equal(doc.num, 3);
assert.equal(contexts.length, 2);
assert.equal(contexts[1].constructor.name, 'Query');
- }).
- then(function() { done(); }).
- catch(done);
+ });
});
- it('runSettersOnQuery only once on find (gh-5434)', function(done) {
+ it('runSettersOnQuery only once on find (gh-5434)', async function() {
let vs = [];
const UserSchema = new mongoose.Schema({
name: String,
@@ -1043,22 +619,17 @@ describe('model query casting', function() {
const Test = db.model('Test', UserSchema);
- Test.find({ foo: '123' }).exec(function(error) {
- assert.ifError(error);
- assert.equal(vs.length, 1);
- assert.strictEqual(vs[0], '123');
-
- vs = [];
- Test.find({ foo: '123' }, function(error) {
- assert.ifError(error);
- assert.equal(vs.length, 1);
- assert.strictEqual(vs[0], '123');
- done();
- });
- });
+ await Test.find({ foo: '123' }).exec();
+ assert.equal(vs.length, 1);
+ assert.strictEqual(vs[0], '123');
+
+ vs = [];
+ await Test.find({ foo: '123' });
+ assert.equal(vs.length, 1);
+ assert.strictEqual(vs[0], '123');
});
- it('setters run only once on findOne (gh-6157)', function(done) {
+ it('setters run only once on findOne (gh-6157)', async function() {
let vs = [];
const UserSchema = new mongoose.Schema({
name: String,
@@ -1076,22 +647,17 @@ describe('model query casting', function() {
const Test = db.model('Test', UserSchema);
- Test.findOne({ foo: '123' }).exec(function(error) {
- assert.ifError(error);
- assert.equal(vs.length, 1);
- assert.strictEqual(vs[0], '123');
-
- vs = [];
- Test.findOne({ foo: '123' }, function(error) {
- assert.ifError(error);
- assert.equal(vs.length, 1);
- assert.strictEqual(vs[0], '123');
- done();
- });
- });
+ await Test.findOne({ foo: '123' });
+ assert.equal(vs.length, 1);
+ assert.strictEqual(vs[0], '123');
+
+ vs = [];
+ await Test.findOne({ foo: '123' });
+ assert.equal(vs.length, 1);
+ assert.strictEqual(vs[0], '123');
});
- it('runSettersOnQuery as query option (gh-5350)', function(done) {
+ it('runSettersOnQuery as query option (gh-5350)', function() {
const contexts = [];
const testSchema = new Schema({
@@ -1106,7 +672,7 @@ describe('model query casting', function() {
});
const Test = db.model('Test', testSchema);
- Test.create({ name: 'val', num: 2.02 }).
+ return Test.create({ name: 'val', num: 2.02 }).
then(function() {
assert.equal(contexts.length, 1);
assert.equal(contexts[0].constructor.name, 'model');
@@ -1116,23 +682,16 @@ describe('model query casting', function() {
assert.ok(doc);
assert.equal(doc.name, 'val');
assert.equal(doc.num, 2);
- }).
- then(function() { done(); }).
- catch(done);
+ });
});
- it('_id = 0 (gh-4610)', function(done) {
+ it('_id = 0 (gh-4610)', async function() {
const MyModel = db.model('Test', { _id: Number });
- MyModel.create({ _id: 0 }, function(error) {
- assert.ifError(error);
- MyModel.findById({ _id: 0 }, function(error, doc) {
- assert.ifError(error);
- assert.ok(doc);
- assert.equal(doc._id, 0);
- done();
- });
- });
+ await MyModel.create({ _id: 0 });
+ const doc = await MyModel.findById({ _id: 0 });
+ assert.ok(doc);
+ assert.equal(doc._id, 0);
});
it('converts to CastError (gh-6803)', function() {
@@ -1148,7 +707,7 @@ describe('model query casting', function() {
});
});
- it('minDistance (gh-4197)', function(done) {
+ it('minDistance (gh-4197)', async function() {
const schema = new Schema({
name: String,
loc: {
@@ -1160,38 +719,33 @@ describe('model query casting', function() {
schema.index({ loc: '2dsphere' });
const MyModel = db.model('Test', schema);
+ await MyModel.init();
+
+ const docs = [
+ { name: 'San Mateo Caltrain', loc: _geojsonPoint([-122.33, 37.57]) },
+ { name: 'Squaw Valley', loc: _geojsonPoint([-120.24, 39.21]) },
+ { name: 'Mammoth Lakes', loc: _geojsonPoint([-118.9, 37.61]) }
+ ];
+ const RADIUS_OF_EARTH_IN_METERS = 6378100;
+ await MyModel.create(docs);
+
+ const results = await MyModel.
+ find().
+ near('loc', {
+ center: [-122.33, 37.57],
+ minDistance: (1000 / RADIUS_OF_EARTH_IN_METERS).toString(),
+ maxDistance: (280000 / RADIUS_OF_EARTH_IN_METERS).toString(),
+ spherical: true
+ }).
+ exec();
- MyModel.on('index', function(error) {
- assert.ifError(error);
- const docs = [
- { name: 'San Mateo Caltrain', loc: _geojsonPoint([-122.33, 37.57]) },
- { name: 'Squaw Valley', loc: _geojsonPoint([-120.24, 39.21]) },
- { name: 'Mammoth Lakes', loc: _geojsonPoint([-118.9, 37.61]) }
- ];
- const RADIUS_OF_EARTH_IN_METERS = 6378100;
- MyModel.create(docs, function(error) {
- assert.ifError(error);
- MyModel.
- find().
- near('loc', {
- center: [-122.33, 37.57],
- minDistance: (1000 / RADIUS_OF_EARTH_IN_METERS).toString(),
- maxDistance: (280000 / RADIUS_OF_EARTH_IN_METERS).toString(),
- spherical: true
- }).
- exec(function(error, results) {
- assert.ifError(error);
- assert.equal(results.length, 1);
- assert.equal(results[0].name, 'Squaw Valley');
- done();
- });
- });
- });
+ assert.equal(results.length, 1);
+ assert.equal(results[0].name, 'Squaw Valley');
});
- it('array ops don\'t break with strict:false (gh-6952)', function(done) {
+ it('array ops don\'t break with strict:false (gh-6952)', function() {
const schema = new Schema({}, { strict: false });
const Test = db.model('Test', schema);
- Test.create({ outerArray: [] })
+ return Test.create({ outerArray: [] })
.then(function(created) {
const toBePushedObj = { innerArray: ['onetwothree'] };
const update = { $push: { outerArray: toBePushedObj } };
@@ -1201,9 +755,77 @@ describe('model query casting', function() {
.then(function(updated) {
const doc = updated.toObject();
assert.strictEqual(doc.outerArray[0].innerArray[0], 'onetwothree');
- done();
});
});
+ it('should not throw a cast error when dealing with an array of an array of strings in combination with $elemMatch and $not (gh-13880)', async function() {
+ const testSchema = new Schema({
+ arr: [[String]]
+ });
+ const Test = db.model('Test', testSchema);
+ const doc = new Test({ arr: [[1, 2, 3], [2, 3, 4]] });
+ await doc.save();
+ const query = { arr: { $elemMatch: { $not: { $elemMatch: { $eq: '1' } } } } };
+ const res = await Test.find(query);
+ assert(res);
+ assert(res[0].arr);
+ });
+ it('should not throw a cast error when dealing with an array of objects in combination with $elemMatch (gh-13974)', async function() {
+ const testSchema = new Schema({
+ arr: [Object]
+ });
+
+ const Test = db.model('Test', testSchema);
+ const obj1 = new Test({ arr: [{ id: 'one' }, { id: 'two' }] });
+ await obj1.save();
+
+ const obj2 = new Test({ arr: [{ id: 'two' }, { id: 'three' }] });
+ await obj2.save();
+
+ const obj3 = new Test({ arr: [{ id: 'three' }, { id: 'four' }] });
+ await obj3.save();
+
+ const res = await Test.find({
+ arr: {
+ $elemMatch: {
+ $or: [{ id: 'one' }, { id: 'two' }]
+ }
+ }
+ }).sort({ _id: 1 });
+ assert.ok(res);
+ assert.deepStrictEqual(res.map(doc => doc.arr[1].id), ['two', 'three']);
+ });
+
+ it('should not throw a cast error when dealing with an array of objects in combination with $elemMatch and nested $and', async function() {
+ const testSchema = new Schema({
+ arr: [Object]
+ });
+
+ const Test = db.model('Test', testSchema);
+ const obj1 = new Test({ arr: [{ id: 'one', name: 'sample1' }, { id: 'two' }] });
+ await obj1.save();
+
+ const obj2 = new Test({ arr: [{ id: 'two', name: 'sample1' }, { id: 'three' }] });
+ await obj2.save();
+
+ const obj3 = new Test({ arr: [{ id: 'three', name: 'sample1' }, { id: 'four' }] });
+ await obj3.save();
+ const res = await Test.find({
+ arr: {
+ $elemMatch: {
+ $and: [
+ { name: 'sample1' },
+ { $or: [
+ { id: 'one' },
+ { id: 'two' }
+ ] }
+ ]
+ }
+ }
+ }).sort({ _id: 1 });
+ assert.ok(res);
+ assert.equal(res.length, 2);
+ assert.deepStrictEqual(res.map(doc => doc.arr[1].id), ['two', 'three']);
+ });
});
function _geojsonPoint(coordinates) {
diff --git a/test/model.querying.test.js b/test/model.querying.test.js
index 63a49d90411..f149d9a5bc5 100644
--- a/test/model.querying.test.js
+++ b/test/model.querying.test.js
@@ -82,7 +82,7 @@ describe('model: querying:', function() {
await db.close();
});
- it('find returns a Query', function(done) {
+ it('find returns a Query', function() {
// query
assert.ok(BlogPostB.find({}) instanceof Query);
@@ -97,11 +97,9 @@ describe('model: querying:', function() {
// query, fields (null), options
assert.ok(BlogPostB.find({}, null, {}) instanceof Query);
-
- done();
});
- it('findOne returns a Query', function(done) {
+ it('findOne returns a Query', function() {
// query
assert.ok(BlogPostB.findOne({}) instanceof Query);
@@ -116,118 +114,6 @@ describe('model: querying:', function() {
// query, fields (null), options
assert.ok(BlogPostB.findOne({}, null, {}) instanceof Query);
-
- done();
- });
-
- it('an empty find does not hang', function(done) {
- function fn() {
- done();
- }
-
- BlogPostB.find({}, fn);
- });
-
- it('a query is executed when a callback is passed', function(done) {
- let count = 5;
- const q = { _id: new DocumentObjectId() }; // make sure the query is fast
-
- function fn() {
- if (--count) {
- return;
- }
- done();
- }
-
- // query
- assert.ok(BlogPostB.find(q, fn) instanceof Query);
-
- // query, fields (object)
- assert.ok(BlogPostB.find(q, {}, fn) instanceof Query);
-
- // query, fields (null)
- assert.ok(BlogPostB.find(q, null, fn) instanceof Query);
-
- // query, fields, options
- assert.ok(BlogPostB.find(q, {}, {}, fn) instanceof Query);
-
- // query, fields (''), options
- assert.ok(BlogPostB.find(q, '', {}, fn) instanceof Query);
- });
-
- it('query is executed where a callback for findOne', function(done) {
- let count = 5;
- const q = { _id: new DocumentObjectId() }; // make sure the query is fast
-
- function fn() {
- if (--count) {
- return;
- }
- done();
- }
-
- // query
- assert.ok(BlogPostB.findOne(q, fn) instanceof Query);
-
- // query, fields
- assert.ok(BlogPostB.findOne(q, {}, fn) instanceof Query);
-
- // query, fields (empty string)
- assert.ok(BlogPostB.findOne(q, '', fn) instanceof Query);
-
- // query, fields, options
- assert.ok(BlogPostB.findOne(q, {}, {}, fn) instanceof Query);
-
- // query, fields (null), options
- assert.ok(BlogPostB.findOne(q, null, {}, fn) instanceof Query);
- });
-
- describe.skip('count', function() {
- it('returns a Query', function(done) {
- assert.ok(BlogPostB.count({}) instanceof Query);
- done();
- });
-
- it('Query executes when you pass a callback', function(done) {
- let pending = 2;
-
- function fn() {
- if (--pending) {
- return;
- }
- done();
- }
-
- assert.ok(BlogPostB.count({}, fn) instanceof Query);
- assert.ok(BlogPostB.count(fn) instanceof Query);
- });
-
- it('counts documents', function(done) {
- const title = 'Wooooot ' + random();
-
- const post = new BlogPostB();
- post.set('title', title);
-
- post.save(function(err) {
- assert.ifError(err);
-
- const post = new BlogPostB();
- post.set('title', title);
-
- post.save(function(err) {
- assert.ifError(err);
-
- BlogPostB.count({ title: title }, function(err, count) {
- assert.ifError(err);
-
- assert.equal(typeof count, 'number');
- assert.equal(count, 2);
-
- done();
- });
- });
- });
- });
});
describe('distinct', function() {
@@ -236,58 +122,36 @@ describe('model: querying:', function() {
done();
});
- it('executes when you pass a callback', function(done) {
+ it('executes when you exec', async function() {
let Address = new Schema({ zip: String });
Address = db.model('Test', Address);
- Address.create({ zip: '10010' }, { zip: '10010' }, { zip: '99701' }, function(err) {
- assert.strictEqual(null, err);
- const query = Address.distinct('zip', {}, function(err, results) {
- assert.ifError(err);
- assert.equal(results.length, 2);
- assert.ok(results.indexOf('10010') > -1);
- assert.ok(results.indexOf('99701') > -1);
- done();
- });
- assert.ok(query instanceof Query);
- });
+ await Address.create({ zip: '10010' }, { zip: '10010' }, { zip: '99701' });
+ const query = Address.distinct('zip', {});
+ assert.ok(query instanceof Query);
+ const results = await query.exec();
+
+ assert.equal(results.length, 2);
+ assert.ok(results.indexOf('10010') > -1);
+ assert.ok(results.indexOf('99701') > -1);
});
- it('permits excluding conditions gh-1541', function(done) {
+ it('permits excluding conditions gh-1541', async function() {
let Address = new Schema({ zip: String });
Address = db.model('Test', Address);
- Address.create({ zip: '10010' }, { zip: '10010' }, { zip: '99701' }, function(err) {
- assert.ifError(err);
- Address.distinct('zip', function(err, results) {
- assert.ifError(err);
- assert.equal(results.length, 2);
- assert.ok(results.indexOf('10010') > -1);
- assert.ok(results.indexOf('99701') > -1);
- done();
- });
- });
- });
- });
+ await Address.create({ zip: '10010' }, { zip: '10010' }, { zip: '99701' });
+ const results = await Address.distinct('zip');
+ assert.equal(results.length, 2);
+ assert.ok(results.indexOf('10010') > -1);
+ assert.ok(results.indexOf('99701') > -1);
- describe('update', function() {
- it('returns a Query', function(done) {
- assert.ok(BlogPostB.update({}, {}) instanceof Query);
- assert.ok(BlogPostB.update({}, {}, {}) instanceof Query);
- done();
});
+ });
- it('Query executes when you pass a callback', function(done) {
- let count = 2;
-
- function fn() {
- if (--count) {
- return;
- }
- done();
- }
-
- assert.ok(BlogPostB.update({ title: random() }, {}, fn) instanceof Query);
- assert.ok(BlogPostB.update({ title: random() }, {}, {}, fn) instanceof Query);
+ describe('updateOne', function() {
+ it('returns a Query', function() {
+ assert.ok(BlogPostB.updateOne({}, {}) instanceof Query);
+ assert.ok(BlogPostB.updateOne({}, {}, {}) instanceof Query);
});
it('can handle minimize option (gh-3381)', function() {
@@ -301,205 +165,138 @@ describe('model: querying:', function() {
then(() => Model.collection.findOne()).
then(doc => {
assert.ok(doc.mixed == null);
- }).
- catch(err => {
- throw err;
});
});
});
describe('findOne', function() {
- it('works', function(done) {
+ it('works', async function() {
const title = 'Wooooot ' + random();
const post = new BlogPostB();
post.set('title', title);
- post.save(function(err) {
- assert.ifError(err);
-
- BlogPostB.findOne({ title: title }, function(err, doc) {
- assert.ifError(err);
- assert.equal(title, doc.get('title'));
- assert.equal(doc.isNew, false);
+ await post.save();
- done();
- });
- });
+ const doc = await BlogPostB.findOne({ title: title });
+ assert.equal(title, doc.get('title'));
+ assert.equal(doc.isNew, false);
});
- it('casts $modifiers', function(done) {
+ it('casts $modifiers', async function() {
const post = new BlogPostB({
meta: {
visitors: -10
}
});
- post.save(function(err) {
- assert.ifError(err);
-
- const query = { 'meta.visitors': { $gt: '-20', $lt: -1 } };
- BlogPostB.findOne(query, function(err, found) {
- assert.ifError(err);
- assert.ok(found);
- assert.equal(found.get('meta.visitors').valueOf(), post.get('meta.visitors').valueOf());
- found.id; // trigger caching
- assert.equal(found.get('_id').toString(), post.get('_id'));
- done();
- });
- });
+ await post.save();
+ const query = { 'meta.visitors': { $gt: '-20', $lt: -1 } };
+ const found = await BlogPostB.findOne(query);
+ assert.ok(found);
+ assert.equal(found.get('meta.visitors').valueOf(), post.get('meta.visitors').valueOf());
+ found.id; // trigger caching
+ assert.equal(found.get('_id').toString(), post.get('_id'));
});
- it('querying if an array contains one of multiple members $in a set', function(done) {
+ it('querying if an array contains one of multiple members $in a set', async function() {
const post = new BlogPostB();
post.tags.push('football');
- post.save(function(err) {
- assert.ifError(err);
+ await post.save();
- BlogPostB.findOne({ tags: { $in: ['football', 'baseball'] } }, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc._id.toString(), post._id);
+ let doc = await BlogPostB.findOne({ tags: { $in: ['football', 'baseball'] } });
+ assert.equal(doc._id.toString(), post._id);
+
+ doc = await BlogPostB.findOne({ _id: post._id, tags: /otba/i });
+ assert.equal(doc._id.toString(), post._id);
- BlogPostB.findOne({ _id: post._id, tags: /otba/i }, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc._id.toString(), post._id);
- done();
- });
- });
- });
});
- it('querying if an array contains one of multiple members $in a set 2', function(done) {
+ it('querying if an array contains one of multiple members $in a set 2', async function() {
const BlogPostA = BlogPostB;
const post = new BlogPostA({ tags: ['gooberOne'] });
- post.save(function(err) {
- assert.ifError(err);
+ await post.save();
+ const query = { tags: { $in: ['gooberOne'] } };
- const query = { tags: { $in: ['gooberOne'] } };
-
- BlogPostA.findOne(query, function(err, returned) {
- cb();
- assert.ifError(err);
- assert.ok(!!~returned.tags.indexOf('gooberOne'));
- assert.equal(returned._id.toString(), post._id);
- });
- });
+ const returned = await BlogPostA.findOne(query);
+ assert.ok(!!~returned.tags.indexOf('gooberOne'));
+ assert.equal(returned._id.toString(), post._id);
const doc = { meta: { visitors: 9898, a: null } };
- post.collection.insertOne(doc, {}, function(err) {
- assert.ifError(err);
+ await post.collection.insertOne(doc, {});
- BlogPostA.findOne({ _id: doc._id }, function(err, found) {
- cb();
- assert.ifError(err);
- assert.equal(found.get('meta.visitors'), 9898);
- });
- });
-
- let pending = 2;
+ const found = await BlogPostA.findOne({ _id: doc._id });
+ assert.equal(found.get('meta.visitors'), 9898);
- function cb() {
- if (--pending) {
- return;
- }
- done();
- }
});
- it('querying via $where a string', function(done) {
- BlogPostB.create({ title: 'Steve Jobs', author: 'Steve Jobs' }, function(err, created) {
- assert.ifError(err);
-
- BlogPostB.findOne({ $where: 'this.title && this.title === this.author' }, function(err, found) {
- assert.ifError(err);
-
- assert.equal(found._id.toString(), created._id);
- done();
- });
+ it('querying via $where a string', async function() {
+ const created = await BlogPostB.create({
+ title: 'Steve Jobs',
+ author: 'Steve Jobs'
});
- });
+ const found = await BlogPostB.findOne({ $where: 'this.title && this.title === this.author' });
- it('querying via $where a function', function(done) {
- BlogPostB.create({ author: 'Atari', slug: 'Atari' }, function(err, created) {
- assert.ifError(err);
+ assert.equal(found._id.toString(), created._id);
+ });
- BlogPostB.findOne({
- $where: function() {
- return (this.author && this.slug && this.author === this.slug);
- }
- }, function(err, found) {
- assert.ifError(err);
+ it('querying via $where a function', async function() {
+ const created = await BlogPostB.create({ author: 'Atari', slug: 'Atari' });
- assert.equal(found._id.toString(), created._id);
- done();
- });
+ const found = await BlogPostB.findOne({
+ $where: function() {
+ return (this.author && this.slug && this.author === this.slug);
+ }
});
+ assert.equal(found._id.toString(), created._id);
});
- it('based on nested fields', function(done) {
+ it('based on nested fields', async function() {
const post = new BlogPostB({
meta: {
visitors: 5678
}
});
- post.save(function(err) {
- assert.ifError(err);
-
- BlogPostB.findOne({ 'meta.visitors': 5678 }, function(err, found) {
- assert.ifError(err);
- assert.equal(found.get('meta.visitors')
- .valueOf(), post.get('meta.visitors').valueOf());
- assert.equal(found.get('_id').toString(), post.get('_id'));
- done();
- });
- });
+ await post.save();
+ const found = await BlogPostB.findOne({ 'meta.visitors': 5678 });
+ assert.equal(found.get('meta.visitors')
+ .valueOf(), post.get('meta.visitors').valueOf());
+ assert.equal(found.get('_id').toString(), post.get('_id'));
});
- it('based on embedded doc fields (gh-242, gh-463)', function(done) {
- BlogPostB.create({ comments: [{ title: 'i should be queryable' }], numbers: [1, 2, 33333], tags: ['yes', 'no'] }, function(err, created) {
- assert.ifError(err);
- BlogPostB.findOne({ 'comments.title': 'i should be queryable' }, function(err, found) {
- assert.ifError(err);
- assert.equal(found._id.toString(), created._id);
-
- BlogPostB.findOne({ 'comments.0.title': 'i should be queryable' }, function(err, found) {
- assert.ifError(err);
- assert.equal(found._id.toString(), created._id);
-
- // GH-463
- BlogPostB.findOne({ 'numbers.2': 33333 }, function(err, found) {
- assert.ifError(err);
- assert.equal(found._id.toString(), created._id);
-
- BlogPostB.findOne({ 'tags.1': 'no' }, function(err, found) {
- assert.ifError(err);
- assert.equal(found._id.toString(), created._id);
- done();
- });
- });
- });
- });
- });
+ it('based on embedded doc fields (gh-242, gh-463)', async function() {
+ const created = await BlogPostB.create({ comments: [{ title: 'i should be queryable' }], numbers: [1, 2, 33333], tags: ['yes', 'no'] });
+ let found = await BlogPostB.findOne({ 'comments.title': 'i should be queryable' });
+ assert.equal(found._id.toString(), created._id);
+
+ found = await BlogPostB.findOne({ 'comments.0.title': 'i should be queryable' });
+
+ assert.equal(found._id.toString(), created._id);
+
+ // GH-463
+ found = await BlogPostB.findOne({ 'numbers.2': 33333 });
+ assert.equal(found._id.toString(), created._id);
+
+ found = await BlogPostB.findOne({ 'tags.1': 'no' });
+ assert.equal(found._id.toString(), created._id);
+
});
- it('works with nested docs and string ids (gh-389)', function(done) {
- BlogPostB.create({ comments: [{ title: 'i should be queryable by _id' }, { title: 'me too me too!' }] }, function(err, created) {
- assert.ifError(err);
- const id = created.comments[1]._id.toString();
- BlogPostB.findOne({ 'comments._id': id }, function(err, found) {
- assert.ifError(err);
- assert.strictEqual(!!found, true, 'Find by nested doc id hex string fails');
- assert.equal(found._id.toString(), created._id);
- done();
- });
- });
+ it('works with nested docs and string ids (gh-389)', async function() {
+ const created = await BlogPostB.create({ comments: [{ title: 'i should be queryable by _id' }, { title: 'me too me too!' }] });
+
+ const id = created.comments[1]._id.toString();
+ const found = await BlogPostB.findOne({ 'comments._id': id });
+ assert.strictEqual(!!found, true, 'Find by nested doc id hex string fails');
+ assert.equal(found._id.toString(), created._id);
+
});
- it('using #all with nested #elemMatch', function(done) {
+ it('using #all with nested #elemMatch', async function() {
const P = BlogPostB;
const post = new P({ title: 'nested elemMatch' });
post.comments.push({ title: 'comment A' }, { title: 'comment B' }, { title: 'comment C' });
@@ -507,163 +304,110 @@ describe('model: querying:', function() {
const id1 = post.comments[1]._id;
const id2 = post.comments[2]._id;
- post.save(function(err) {
- assert.ifError(err);
-
- const query0 = { $elemMatch: { _id: id1, title: 'comment B' } };
- const query1 = { $elemMatch: { _id: id2.toString(), title: 'comment C' } };
+ await post.save();
+ const query0 = { $elemMatch: { _id: id1, title: 'comment B' } };
+ const query1 = { $elemMatch: { _id: id2.toString(), title: 'comment C' } };
- P.findOne({ comments: { $all: [query0, query1] } }, function(err, p) {
- assert.ifError(err);
- assert.equal(p.id, post.id);
- done();
- });
- });
+ const p = await P.findOne({ comments: { $all: [query0, query1] } });
+ assert.equal(p.id, post.id);
});
- it('using #or with nested #elemMatch', function(done) {
+ it('using #or with nested #elemMatch', async function() {
const P = BlogPostB;
const post = new P({ title: 'nested elemMatch' });
post.comments.push({ title: 'comment D' }, { title: 'comment E' }, { title: 'comment F' });
const id1 = post.comments[1]._id;
- post.save(function(err) {
- assert.ifError(err);
+ await post.save();
- const query0 = { comments: { $elemMatch: { title: 'comment Z' } } };
- const query1 = { comments: { $elemMatch: { _id: id1.toString(), title: 'comment E' } } };
+ const query0 = { comments: { $elemMatch: { title: 'comment Z' } } };
+ const query1 = { comments: { $elemMatch: { _id: id1.toString(), title: 'comment E' } } };
+
+ const p = await P.findOne({ $or: [query0, query1] });
+ assert.equal(p.id, post.id);
- P.findOne({ $or: [query0, query1] }, function(err, p) {
- assert.ifError(err);
- assert.equal(p.id, post.id);
- done();
- });
- });
});
- it('buffer $in array', function(done) {
- BlogPostB.create({
+ it('buffer $in array', async function() {
+ const created = await BlogPostB.create({
sigs: [Buffer.from([1, 2, 3]),
Buffer.from([4, 5, 6]),
Buffer.from([7, 8, 9])]
- }, function(err, created) {
- assert.ifError(err);
- BlogPostB.findOne({ sigs: Buffer.from([1, 2, 3]) }, function(err, found) {
- assert.ifError(err);
- found.id;
- assert.equal(found._id.toString(), created._id);
- const query = { sigs: { $in: [Buffer.from([3, 3, 3]), Buffer.from([4, 5, 6])] } };
- BlogPostB.findOne(query, function(err) {
- assert.ifError(err);
- done();
- });
- });
});
+ const found = await BlogPostB.findOne({ sigs: Buffer.from([1, 2, 3]) });
+ found.id;
+ assert.equal(found._id.toString(), created._id);
+ const query = { sigs: { $in: [Buffer.from([3, 3, 3]), Buffer.from([4, 5, 6])] } };
+ await BlogPostB.findOne(query);
+
});
- it('regex with Array (gh-599)', function(done) {
+ it('regex with Array (gh-599)', async function() {
const B = BlogPostB;
- B.create({ tags: 'wooof baaaark meeeeow'.split(' ') }, function(err) {
- assert.ifError(err);
- B.findOne({ tags: /ooof$/ }, function(err, doc) {
- assert.ifError(err);
- assert.strictEqual(true, !!doc);
- assert.ok(!!~doc.tags.indexOf('meeeeow'));
-
- B.findOne({ tags: { $regex: 'eow$' } }, function(err, doc) {
- assert.ifError(err);
- assert.strictEqual(true, !!doc);
- assert.strictEqual(true, !!~doc.tags.indexOf('meeeeow'));
- done();
- });
- });
- });
+ await B.create({ tags: 'wooof baaaark meeeeow'.split(' ') });
+ let doc = await B.findOne({ tags: /ooof$/ });
+ assert.strictEqual(true, !!doc);
+ assert.ok(!!~doc.tags.indexOf('meeeeow'));
+
+ doc = await B.findOne({ tags: { $regex: 'eow$' } });
+ assert.strictEqual(true, !!doc);
+ assert.strictEqual(true, !!~doc.tags.indexOf('meeeeow'));
+
});
- it('regex with options', function(done) {
+ it('regex with options', async function() {
const B = BlogPostB;
const post = new B({ title: '$option queries' });
- post.save(function(err) {
- assert.ifError(err);
- B.findOne({ title: { $regex: ' QUERIES$', $options: 'i' } }, function(err, doc) {
- assert.strictEqual(null, err, err && err.stack);
- assert.equal(doc.id, post.id);
- done();
- });
- });
+ await post.save();
+ const doc = await B.findOne({ title: { $regex: ' QUERIES$', $options: 'i' } });
+ assert.equal(doc.id, post.id);
+
});
- it('works with $elemMatch and $in combo (gh-1100)', function(done) {
+ it('works with $elemMatch and $in combo (gh-1100)', async function() {
const id1 = new DocumentObjectId();
const id2 = new DocumentObjectId();
- BlogPostB.create({ owners: [id1, id2] }, function(err, created) {
- assert.ifError(err);
- BlogPostB.findOne({ owners: { $elemMatch: { $in: [id2.toString()] } } }, function(err, found) {
- assert.ifError(err);
- assert.ok(found);
- assert.equal(created.id, found.id);
- done();
- });
- });
+ const created = await BlogPostB.create({ owners: [id1, id2] });
+ const found = await BlogPostB.findOne({ owners: { $elemMatch: { $in: [id2.toString()] } } });
+ assert.ok(found);
+ assert.equal(created.id, found.id);
+
});
});
describe('findById', function() {
- it('handles undefined', function(done) {
+ it('handles undefined', async function() {
const title = 'Edwald ' + random();
const post = new BlogPostB();
post.set('title', title);
- post.save(function(err) {
- assert.ifError(err);
+ await post.save();
- BlogPostB.findById(undefined, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc, null);
- done();
- });
- });
+ const doc = await BlogPostB.findById(undefined);
+ assert.equal(doc, null);
});
- it('works', function(done) {
+ it('works', async function() {
const title = 'Edwald ' + random();
const post = new BlogPostB();
post.set('title', title);
- post.save(function(err) {
- assert.ifError(err);
-
- let pending = 2;
+ await post.save();
- BlogPostB.findById(post.get('_id'), function(err, doc) {
- assert.ifError(err);
- assert.ok(doc instanceof BlogPostB);
- assert.equal(doc.get('title'), title);
- if (--pending) {
- return;
- }
- done();
- });
+ let doc = await BlogPostB.findById(post.get('_id'));
+ assert.ok(doc instanceof BlogPostB);
+ assert.equal(doc.get('title'), title);
- BlogPostB.findById(post.get('_id').toHexString(), function(err, doc) {
- assert.ifError(err);
- assert.ok(doc instanceof BlogPostB);
- assert.equal(doc.get('title'), title);
- if (--pending) {
- return;
- }
- done();
- });
- });
+ doc = await BlogPostB.findById(post.get('_id').toHexString());
+ assert.ok(doc instanceof BlogPostB);
+ assert.equal(doc.get('title'), title);
});
- it('works with partial initialization', function(done) {
- let queries = 5;
-
+ it('works with partial initialization', async function() {
const post = new BlogPostB();
post.title = 'hahaha';
@@ -671,173 +415,119 @@ describe('model: querying:', function() {
post.meta.visitors = 53;
post.tags = ['humidity', 'soggy'];
- post.save(function(err) {
- assert.ifError(err);
-
- BlogPostB.findById(post.get('_id'), function(err, doc) {
- assert.ifError(err);
-
- assert.equal(doc.isInit('title'), true);
- assert.equal(doc.isInit('slug'), true);
- assert.equal(doc.isInit('date'), false);
- assert.equal(doc.isInit('meta.visitors'), true);
- assert.equal(doc.meta.visitors.valueOf(), 53);
- assert.equal(doc.tags.length, 2);
- if (--queries) {
- return;
- }
- done();
- });
-
- BlogPostB.findById(post.get('_id'), 'title', function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.isInit('title'), true);
- assert.equal(doc.isInit('slug'), false);
- assert.equal(doc.isInit('date'), false);
- assert.equal(doc.isInit('meta.visitors'), false);
- assert.equal(doc.meta.visitors, undefined);
- assert.equal(doc.tags, undefined);
- if (--queries) {
- return;
- }
- done();
- });
-
- BlogPostB.findById(post.get('_id'), '-slug', function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.isInit('title'), true);
- assert.equal(doc.isInit('slug'), false);
- assert.equal(doc.isInit('date'), false);
- assert.equal(doc.isInit('meta.visitors'), true);
- assert.equal(doc.meta.visitors, 53);
- assert.equal(doc.tags.length, 2);
- if (--queries) {
- return;
- }
- done();
- });
-
- BlogPostB.findById(post.get('_id'), { title: 1 }, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.isInit('title'), true);
- assert.equal(doc.isInit('slug'), false);
- assert.equal(doc.isInit('date'), false);
- assert.equal(doc.isInit('meta.visitors'), false);
- assert.equal(doc.meta.visitors, undefined);
- assert.equal(doc.tags, undefined);
- if (--queries) {
- return;
- }
- done();
- });
-
- BlogPostB.findById(post.get('_id'), 'slug', function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.isInit('title'), false);
- assert.equal(doc.isInit('slug'), true);
- assert.equal(doc.isInit('date'), false);
- assert.equal(doc.isInit('meta.visitors'), false);
- assert.equal(doc.meta.visitors, undefined);
- assert.equal(doc.tags, undefined);
- if (--queries) {
- return;
- }
- done();
- });
- });
- });
-
- it('querying if an array contains at least a certain single member (gh-220)', function(done) {
+ await post.save();
+ let doc = await BlogPostB.findById(post.get('_id'));
+
+ assert.equal(doc.isInit('title'), true);
+ assert.equal(doc.isInit('slug'), true);
+ assert.equal(doc.isInit('date'), false);
+ assert.equal(doc.isInit('meta.visitors'), true);
+ assert.equal(doc.meta.visitors.valueOf(), 53);
+ assert.equal(doc.tags.length, 2);
+
+
+ doc = await BlogPostB.findById(post.get('_id'), 'title');
+ assert.equal(doc.isInit('title'), true);
+ assert.equal(doc.isInit('slug'), false);
+ assert.equal(doc.isInit('date'), false);
+ assert.equal(doc.isInit('meta.visitors'), false);
+ assert.equal(doc.meta.visitors, undefined);
+ assert.equal(doc.tags, undefined);
+
+
+ doc = await BlogPostB.findById(post.get('_id'), '-slug');
+ assert.equal(doc.isInit('title'), true);
+ assert.equal(doc.isInit('slug'), false);
+ assert.equal(doc.isInit('date'), false);
+ assert.equal(doc.isInit('meta.visitors'), true);
+ assert.equal(doc.meta.visitors, 53);
+ assert.equal(doc.tags.length, 2);
+
+ doc = await BlogPostB.findById(post.get('_id'), { title: 1 });
+ assert.equal(doc.isInit('title'), true);
+ assert.equal(doc.isInit('slug'), false);
+ assert.equal(doc.isInit('date'), false);
+ assert.equal(doc.isInit('meta.visitors'), false);
+ assert.equal(doc.meta.visitors, undefined);
+ assert.equal(doc.tags, undefined);
+
+ doc = await BlogPostB.findById(post.get('_id'), 'slug');
+ assert.equal(doc.isInit('title'), false);
+ assert.equal(doc.isInit('slug'), true);
+ assert.equal(doc.isInit('date'), false);
+ assert.equal(doc.isInit('meta.visitors'), false);
+ assert.equal(doc.meta.visitors, undefined);
+ assert.equal(doc.tags, undefined);
+
+ });
+
+ it('querying if an array contains at least a certain single member (gh-220)', async function() {
const post = new BlogPostB();
post.tags.push('cat');
- post.save(function(err) {
- assert.ifError(err);
-
- BlogPostB.findOne({ tags: 'cat' }, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc._id.toString(), post._id);
- done();
- });
- });
+ await post.save();
+ const doc = await BlogPostB.findOne({ tags: 'cat' });
+ assert.equal(doc._id.toString(), post._id);
});
- it('where an array where the $slice operator', function(done) {
- BlogPostB.create({ numbers: [500, 600, 700, 800] }, function(err, created) {
- assert.ifError(err);
- BlogPostB.findById(created._id, { numbers: { $slice: 2 } }, function(err, found) {
- assert.ifError(err);
- assert.equal(found._id.toString(), created._id);
- assert.equal(found.numbers.length, 2);
- assert.equal(found.numbers[0], 500);
- assert.equal(found.numbers[1], 600);
- BlogPostB.findById(created._id, { numbers: { $slice: -2 } }, function(err, found) {
- assert.ifError(err);
- assert.equal(found._id.toString(), created._id);
- assert.equal(found.numbers.length, 2);
- assert.equal(found.numbers[0], 700);
- assert.equal(found.numbers[1], 800);
- BlogPostB.findById(created._id, { numbers: { $slice: [1, 2] } }, function(err, found) {
- assert.ifError(err);
- assert.equal(found._id.toString(), created._id);
- assert.equal(found.numbers.length, 2);
- assert.equal(found.numbers[0], 600);
- assert.equal(found.numbers[1], 700);
- done();
- });
- });
- });
- });
+ it('where an array where the $slice operator', async function() {
+ const created = await BlogPostB.create({ numbers: [500, 600, 700, 800] });
+ let found = await BlogPostB.findById(created._id, { numbers: { $slice: 2 } });
+ assert.equal(found._id.toString(), created._id);
+ assert.equal(found.numbers.length, 2);
+ assert.equal(found.numbers[0], 500);
+ assert.equal(found.numbers[1], 600);
+ found = await BlogPostB.findById(created._id, { numbers: { $slice: -2 } });
+ assert.equal(found._id.toString(), created._id);
+ assert.equal(found.numbers.length, 2);
+ assert.equal(found.numbers[0], 700);
+ assert.equal(found.numbers[1], 800);
+ found = await BlogPostB.findById(created._id, { numbers: { $slice: [1, 2] } });
+
+ assert.equal(found._id.toString(), created._id);
+ assert.equal(found.numbers.length, 2);
+ assert.equal(found.numbers[0], 600);
+ assert.equal(found.numbers[1], 700);
+
});
});
describe('find', function() {
- it('works', function(done) {
+ it('works', async function() {
const title = 'Wooooot ' + random();
- const post = new BlogPostB();
+ let post = new BlogPostB();
post.set('title', title);
- post.save(function(err) {
- assert.ifError(err);
+ await post.save();
+ post = new BlogPostB();
+ post.set('title', title);
- const post = new BlogPostB();
- post.set('title', title);
+ await post.save();
- post.save(function(err) {
- assert.ifError(err);
+ const docs = await BlogPostB.find({ title: title });
+ assert.equal(docs.length, 2);
- BlogPostB.find({ title: title }, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 2);
+ assert.equal(title, docs[0].get('title'));
+ assert.equal(docs[0].isNew, false);
- assert.equal(title, docs[0].get('title'));
- assert.equal(docs[0].isNew, false);
+ assert.equal(title, docs[1].get('title'));
+ assert.equal(docs[1].isNew, false);
- assert.equal(title, docs[1].get('title'));
- assert.equal(docs[1].isNew, false);
- done();
- });
- });
- });
});
- it('returns docs where an array that contains one specific member', function(done) {
- BlogPostB.create({ numbers: [100, 101, 102] }, function(err, created) {
- assert.ifError(err);
- BlogPostB.find({ numbers: 100 }, function(err, found) {
- assert.ifError(err);
- assert.equal(found.length, 1);
- assert.equal(found[0]._id.toString(), created._id);
- done();
- });
- });
+ it('returns docs where an array that contains one specific member', async function() {
+ const created = await BlogPostB.create({ numbers: [100, 101, 102] });
+ const found = await BlogPostB.find({ numbers: 100 });
+ assert.equal(found.length, 1);
+ assert.equal(found[0]._id.toString(), created._id);
+
});
- it('works when comparing $ne with single value against an array', function(done) {
+ it('works when comparing $ne with single value against an array', async function() {
const schema = new Schema({
ids: [Schema.ObjectId],
b: Schema.ObjectId
@@ -850,93 +540,54 @@ describe('model: querying:', function() {
const id3 = new DocumentObjectId();
const id4 = new DocumentObjectId();
- NE.create({ ids: [id1, id4], b: id3 }, function(err) {
- assert.ifError(err);
- NE.create({ ids: [id2, id4], b: id3 }, function(err) {
- assert.ifError(err);
-
- const query = NE.find({ b: id3.toString(), ids: { $ne: id1 } });
- query.exec(function(err, nes1) {
- assert.ifError(err);
- assert.equal(nes1.length, 1);
-
- NE.find({ b: { $ne: [1] } }, function(err) {
- assert.equal(err.message, 'Cast to ObjectId failed for value "[ 1 ]" (type Array) at path "b" for model "Test"');
-
- NE.find({ b: { $ne: 4 } }, function(err) {
- assert.equal(err.message, 'Cast to ObjectId failed for value "4" (type number) at path "b" for model "Test"');
-
- NE.find({ b: id3, ids: { $ne: id4 } }, function(err, nes4) {
- assert.ifError(err);
- assert.equal(nes4.length, 0);
- done();
- });
- });
- });
- });
- });
- });
- });
+ await NE.create({ ids: [id1, id4], b: id3 });
+ await NE.create({ ids: [id2, id4], b: id3 });
- it('with partial initialization', function(done) {
- let queries = 4;
+ const query = NE.find({ b: id3.toString(), ids: { $ne: id1 } });
+ const nes1 = await query.exec();
+ assert.equal(nes1.length, 1);
+ let err = await NE.find({ b: { $ne: [1] } }).then(() => null, err => err);
+ assert.equal(err.message, 'Cast to ObjectId failed for value "[ 1 ]" (type Array) at path "b" for model "Test"');
+
+ err = await NE.find({ b: { $ne: 4 } }).then(() => null, err => err);
+ assert.equal(err.message, 'Cast to ObjectId failed for value "4" (type number) at path "b" for model "Test"');
+
+ const nes4 = await NE.find({ b: id3, ids: { $ne: id4 } });
+ assert.equal(nes4.length, 0);
+
+ });
+
+ it('with partial initialization', async function() {
const post = new BlogPostB();
post.title = 'hahaha';
post.slug = 'woot';
- post.save(function(err) {
- assert.ifError(err);
-
- BlogPostB.find({ _id: post.get('_id') }, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs[0].isInit('title'), true);
- assert.equal(docs[0].isInit('slug'), true);
- assert.equal(docs[0].isInit('date'), false);
- assert.strictEqual('kandinsky', docs[0].def);
- if (--queries) {
- return;
- }
- done();
- });
+ await post.save();
+ let docs = await BlogPostB.find({ _id: post.get('_id') });
+ assert.equal(docs[0].isInit('title'), true);
+ assert.equal(docs[0].isInit('slug'), true);
+ assert.equal(docs[0].isInit('date'), false);
+ assert.strictEqual('kandinsky', docs[0].def);
- BlogPostB.find({ _id: post.get('_id') }, 'title', function(err, docs) {
- assert.ifError(err);
- assert.equal(docs[0].isInit('title'), true);
- assert.equal(docs[0].isInit('slug'), false);
- assert.equal(docs[0].isInit('date'), false);
- assert.strictEqual(undefined, docs[0].def);
- if (--queries) {
- return;
- }
- done();
- });
+ docs = await BlogPostB.find({ _id: post.get('_id') }, 'title');
+ assert.equal(docs[0].isInit('title'), true);
+ assert.equal(docs[0].isInit('slug'), false);
+ assert.equal(docs[0].isInit('date'), false);
+ assert.strictEqual(undefined, docs[0].def);
- BlogPostB.find({ _id: post.get('_id') }, { slug: 0, def: 0 }, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs[0].isInit('title'), true);
- assert.equal(docs[0].isInit('slug'), false);
- assert.equal(docs[0].isInit('date'), false);
- assert.strictEqual(undefined, docs[0].def);
- if (--queries) {
- return;
- }
- done();
- });
+ docs = await BlogPostB.find({ _id: post.get('_id') }, { slug: 0, def: 0 });
+ assert.equal(docs[0].isInit('title'), true);
+ assert.equal(docs[0].isInit('slug'), false);
+ assert.equal(docs[0].isInit('date'), false);
+ assert.strictEqual(undefined, docs[0].def);
- BlogPostB.find({ _id: post.get('_id') }, 'slug', function(err, docs) {
- assert.ifError(err);
- assert.equal(docs[0].isInit('title'), false);
- assert.equal(docs[0].isInit('slug'), true);
- assert.equal(docs[0].isInit('date'), false);
- assert.strictEqual(undefined, docs[0].def);
- if (--queries) {
- return;
- }
- done();
- });
- });
+ docs = await BlogPostB.find({ _id: post.get('_id') }, 'slug');
+ assert.equal(docs[0].isInit('title'), false);
+ assert.equal(docs[0].isInit('slug'), true);
+ assert.equal(docs[0].isInit('date'), false);
+ assert.strictEqual(undefined, docs[0].def);
});
it('where $exists', async function() {
@@ -964,280 +615,167 @@ describe('model: querying:', function() {
assert.ok(threw);
});
- it('works with $elemMatch (gh-1100)', function(done) {
+ it('works with $elemMatch (gh-1100)', async function() {
const id1 = new DocumentObjectId();
const id2 = new DocumentObjectId();
- BlogPostB.create({ owners: [id1, id2] }, function(err) {
- assert.ifError(err);
- BlogPostB.find({ owners: { $elemMatch: { $in: [id2.toString()] } } }, function(err, found) {
- assert.ifError(err);
- assert.equal(found.length, 1);
- done();
- });
- });
+ await BlogPostB.create({ owners: [id1, id2] });
+ const found = await BlogPostB.find({ owners: { $elemMatch: { $in: [id2.toString()] } } });
+ assert.equal(found.length, 1);
});
- it('where $mod', function(done) {
+ it('where $mod', async function() {
const Mod = db.model('Test', ModSchema);
- Mod.create({ num: 1 }, function(err, one) {
- assert.ifError(err);
- Mod.create({ num: 2 }, function(err) {
- assert.ifError(err);
- Mod.find({ num: { $mod: [2, 1] } }, function(err, found) {
- assert.ifError(err);
- assert.equal(found.length, 1);
- assert.equal(found[0]._id.toString(), one._id);
- done();
- });
- });
- });
+ const one = await Mod.create({ num: 1 });
+ await Mod.create({ num: 2 });
+ const found = await Mod.find({ num: { $mod: [2, 1] } });
+ assert.equal(found.length, 1);
+ assert.equal(found[0]._id.toString(), one._id);
+
});
- it('where $not', function(done) {
+ it('where $not', async function() {
const Mod = db.model('Test', ModSchema);
- Mod.create({ num: 1 }, function(err) {
- assert.ifError(err);
- Mod.create({ num: 2 }, function(err, two) {
- assert.ifError(err);
- Mod.find({ num: { $not: { $mod: [2, 1] } } }, function(err, found) {
- assert.ifError(err);
- assert.equal(found.length, 1);
- assert.equal(found[0]._id.toString(), two._id);
- done();
- });
- });
- });
+ await Mod.create({ num: 1 });
+ const two = await Mod.create({ num: 2 });
+ const found = await Mod.find({ num: { $not: { $mod: [2, 1] } } });
+ assert.equal(found.length, 1);
+ assert.equal(found[0]._id.toString(), two._id);
+
});
- it('where or()', function(done) {
+ it('where or()', async function() {
const Mod = db.model('Test', ModSchema);
- Mod.create({ num: 1 }, { num: 2, str: 'two' }, function(err, one, two) {
- assert.ifError(err);
-
- let pending = 3;
- test1();
- test2();
- test3();
-
- function test1() {
- Mod.find({ $or: [{ num: 1 }, { num: 2 }] }, function(err, found) {
- cb();
- assert.ifError(err);
- assert.equal(found.length, 2);
-
- let found1 = false;
- let found2 = false;
-
- found.forEach(function(doc) {
- if (doc.id === one.id) {
- found1 = true;
- } else if (doc.id === two.id) {
- found2 = true;
- }
- });
-
- assert.ok(found1);
- assert.ok(found2);
- });
- }
+ const [one, two] = await Mod.create({ num: 1 }, { num: 2, str: 'two' });
+ let found = await Mod.find({ $or: [{ num: 1 }, { num: 2 }] });
+ assert.equal(found.length, 2);
- function test2() {
- Mod.find({ $or: [{ str: 'two' }, { str: 'three' }] }, function(err, found) {
- cb();
- assert.ifError(err);
- assert.equal(found.length, 1);
- assert.equal(found[0]._id.toString(), two._id);
- });
- }
+ let found1 = false;
+ let found2 = false;
- function test3() {
- Mod.find({ $or: [{ num: 1 }] }).or([{ str: 'two' }]).exec(function(err, found) {
- cb();
- assert.ifError(err);
- assert.equal(found.length, 2);
-
- let found1 = false;
- let found2 = false;
-
- found.forEach(function(doc) {
- if (doc.id === one.id) {
- found1 = true;
- } else if (doc.id === two.id) {
- found2 = true;
- }
- });
-
- assert.ok(found1);
- assert.ok(found2);
- });
+ found.forEach(function(doc) {
+ if (doc.id === one.id) {
+ found1 = true;
+ } else if (doc.id === two.id) {
+ found2 = true;
}
+ });
- function cb() {
- if (--pending) {
- return;
- }
- done();
+ assert.ok(found1);
+ assert.ok(found2);
+
+ found = await Mod.find({ $or: [{ str: 'two' }, { str: 'three' }] });
+ assert.equal(found.length, 1);
+ assert.equal(found[0]._id.toString(), two._id);
+
+ found = await Mod.find({ $or: [{ num: 1 }] }).or([{ str: 'two' }]).exec();
+ assert.equal(found.length, 2);
+
+ found1 = false;
+ found2 = false;
+
+ found.forEach(function(doc) {
+ if (doc.id === one.id) {
+ found1 = true;
+ } else if (doc.id === two.id) {
+ found2 = true;
}
});
+
+ assert.ok(found1);
+ assert.ok(found2);
+
});
- it('using $or with array of Document', function(done) {
+ it('using $or with array of Document', async function() {
const Mod = db.model('Test', ModSchema);
- Mod.create({ num: 1 }, function(err, one) {
- assert.ifError(err);
- Mod.find({ num: 1 }, function(err, found) {
- assert.ifError(err);
- Mod.find({ $or: found }, function(err, found) {
- assert.ifError(err);
- assert.equal(found.length, 1);
- assert.equal(found[0]._id.toString(), one._id);
- done();
- });
- });
- });
+ const one = await Mod.create({ num: 1 });
+ let found = await Mod.find({ num: 1 });
+ found = await Mod.find({ $or: found });
+ assert.equal(found.length, 1);
+ assert.equal(found[0]._id.toString(), one._id);
+
});
- it('where $ne', function(done) {
+ it('where $ne', async function() {
const Mod = db.model('Test', ModSchema);
- Mod.create({ num: 1 }, function(err) {
- assert.ifError(err);
- Mod.create({ num: 2 }, function(err, two) {
- assert.ifError(err);
- Mod.create({ num: 3 }, function(err, three) {
- assert.ifError(err);
- Mod.find({ num: { $ne: 1 } }, function(err, found) {
- assert.ifError(err);
-
- assert.equal(found.length, 2);
- assert.equal(found[0]._id.toString(), two._id);
- assert.equal(found[1]._id.toString(), three._id);
- done();
- });
- });
- });
- });
+ await Mod.create({ num: 1 });
+ const two = await Mod.create({ num: 2 });
+ const three = await Mod.create({ num: 3 });
+ const found = await Mod.find({ num: { $ne: 1 } });
+
+ assert.equal(found.length, 2);
+ assert.equal(found[0]._id.toString(), two._id);
+ assert.equal(found[1]._id.toString(), three._id);
+
});
- it('where $nor', function(done) {
+ it('where $nor', async function() {
const Mod = db.model('Test', ModSchema);
- Mod.create({ num: 1 }, { num: 2, str: 'two' }, function(err, one, two) {
- assert.ifError(err);
-
- let pending = 3;
- test1();
- test2();
- test3();
-
- function test1() {
- Mod.find({ $nor: [{ num: 1 }, { num: 3 }] }, function(err, found) {
- cb();
- assert.ifError(err);
- assert.equal(found.length, 1);
- assert.equal(found[0]._id.toString(), two._id);
- });
- }
+ const [one, two] = await Mod.create([{ num: 1 }, { num: 2, str: 'two' }]);
- function test2() {
- Mod.find({ $nor: [{ str: 'two' }, { str: 'three' }] }, function(err, found) {
- cb();
- assert.ifError(err);
- assert.equal(found.length, 1);
- assert.equal(found[0]._id.toString(), one._id);
- });
- }
+ let found = await Mod.find({ $nor: [{ num: 1 }, { num: 3 }] });
+ assert.equal(found.length, 1);
+ assert.equal(found[0]._id.toString(), two._id);
- function test3() {
- Mod.find({ $nor: [{ num: 2 }] }).nor([{ str: 'two' }]).exec(function(err, found) {
- cb();
- assert.ifError(err);
- assert.equal(found.length, 1);
- assert.equal(found[0]._id.toString(), one._id);
- });
- }
+ found = await Mod.find({ $nor: [{ str: 'two' }, { str: 'three' }] });
+ assert.equal(found.length, 1);
+ assert.equal(found[0]._id.toString(), one._id);
- function cb() {
- if (--pending) {
- return;
- }
- done();
- }
- });
+ found = await Mod.find({ $nor: [{ num: 2 }] }).nor([{ str: 'two' }]);
});
- it('STRICT null matches', function(done) {
+ it('STRICT null matches', async function() {
const a = { title: 'A', author: null };
const b = { title: 'B' };
- BlogPostB.create(a, b, function(err, createdA) {
- assert.ifError(err);
- BlogPostB.find({ author: { $in: [null], $exists: true } }, function(err, found) {
- assert.ifError(err);
- assert.equal(found.length, 1);
- assert.equal(found[0]._id.toString(), createdA._id);
- done();
- });
- });
+ const [createdA] = await BlogPostB.create([a, b]);
+ const found = await BlogPostB.find({ author: { $in: [null], $exists: true } });
+ assert.equal(found.length, 1);
+ assert.equal(found[0]._id.toString(), createdA._id);
+
});
- it('null matches null and undefined', function(done) {
- BlogPostB.create(
+ it('null matches null and undefined', async function() {
+ await BlogPostB.create(
{ title: 'A', author: null },
- { title: 'B' }, function(err) {
- assert.ifError(err);
- BlogPostB.find({ author: null }, function(err, found) {
- assert.ifError(err);
- assert.equal(found.length, 2);
- done();
- });
- });
+ { title: 'B' }
+ );
+ const found = await BlogPostB.find({ author: null });
+ assert.equal(found.length, 2);
+
});
- it('a document whose arrays contain at least $all string values', function(done) {
- const post = new BlogPostB({ title: 'Aristocats' });
+ it('a document whose arrays contain at least $all string values', async function() {
+ let post = new BlogPostB({ title: 'Aristocats' });
post.tags.push('onex');
post.tags.push('twox');
post.tags.push('threex');
- post.save(function(err) {
- assert.ifError(err);
-
- BlogPostB.findById(post._id, function(err, post) {
- assert.ifError(err);
-
- BlogPostB.find({ title: { $all: ['Aristocats'] } }, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 1);
-
- BlogPostB.find({ title: { $all: [/^Aristocats/] } }, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 1);
-
- BlogPostB.find({ tags: { $all: ['onex', 'twox', 'threex'] } }, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 1);
-
- BlogPostB.find({ tags: { $all: [/^onex/i] } }, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 1);
-
- BlogPostB.findOne({ tags: { $all: /^two/ } }, function(err, doc) {
- assert.ifError(err);
- assert.equal(post.id, doc.id);
- done();
- });
- });
- });
- });
- });
- });
- });
+ await post.save();
+ post = await BlogPostB.findById(post._id);
+
+ let docs = await BlogPostB.find({ title: { $all: ['Aristocats'] } });
+ assert.equal(docs.length, 1);
+
+ docs = await BlogPostB.find({ title: { $all: [/^Aristocats/] } });
+ assert.equal(docs.length, 1);
+
+ docs = await BlogPostB.find({ tags: { $all: ['onex', 'twox', 'threex'] } });
+ assert.equal(docs.length, 1);
+
+ docs = await BlogPostB.find({ tags: { $all: [/^onex/i] } });
+ assert.equal(docs.length, 1);
+
+ docs = await BlogPostB.findOne({ tags: { $all: /^two/ } });
+ assert.equal(post._id.toString(), docs._id.toString());
+
});
- it('using #nor with nested #elemMatch', function(done) {
+ it('using #nor with nested #elemMatch', async function() {
const p0 = { title: 'nested $nor elemMatch1', comments: [] };
const p1 = { title: 'nested $nor elemMatch0', comments: [] };
@@ -1245,194 +783,115 @@ describe('model: querying:', function() {
const P = BlogPostB;
- P.create(p0, p1, function(err, post0, post1) {
- assert.ifError(err);
+ const [post0, post1] = await P.create([p0, p1]);
- const id = post1.comments[1]._id;
+ const id = post1.comments[1]._id;
- const query0 = { comments: { $elemMatch: { title: 'comment Z' } } };
- const query1 = { comments: { $elemMatch: { _id: id.toString(), title: 'comment Y' } } };
+ const query0 = { comments: { $elemMatch: { title: 'comment Z' } } };
+ const query1 = { comments: { $elemMatch: { _id: id.toString(), title: 'comment Y' } } };
- P.find({ $nor: [query0, query1] }, function(err, posts) {
- assert.ifError(err);
- assert.equal(posts.length, 1);
- assert.equal(posts[0].id, post0.id);
- done();
- });
- });
- });
+ const posts = await P.find({ $nor: [query0, query1] });
+ assert.equal(posts.length, 1);
+ assert.equal(posts[0].id, post0.id);
- it('strings via regexp', function(done) {
- BlogPostB.create({ title: 'Next to Normal' }, function(err, created) {
- assert.ifError(err);
- BlogPostB.findOne({ title: /^Next/ }, function(err, found) {
- assert.ifError(err);
- assert.equal(found._id.toString(), created._id);
-
- const reg = '^Next to Normal$';
-
- BlogPostB.find({ title: { $regex: reg } }, function(err, found) {
- assert.ifError(err);
- assert.equal(found.length, 1);
- assert.equal(found[0]._id.toString(), created._id);
-
- BlogPostB.findOne({ title: { $regex: reg } }, function(err, found) {
- assert.ifError(err);
- assert.equal(found._id.toString(), created._id);
-
- BlogPostB.where('title').regex(reg).findOne(function(err, found) {
- assert.ifError(err);
- assert.equal(found._id.toString(), created._id);
-
- BlogPostB.where('title').regex(/^Next/).findOne(function(err, found) {
- assert.ifError(err);
- assert.equal(found._id.toString(), created._id);
- done();
- });
- });
- });
- });
- });
- });
});
- it('a document whose arrays contain at least $all values', function(done) {
- const a1 = { numbers: [-1, -2, -3, -4], meta: { visitors: 4 } };
- const a2 = { numbers: [0, -1, -2, -3, -4] };
- BlogPostB.create(a1, a2, function(err, whereoutZero, whereZero) {
- assert.ifError(err);
-
- BlogPostB.find({ numbers: { $all: [-1, -2, -3, -4] } }, function(err, found) {
- assert.ifError(err);
- assert.equal(found.length, 2);
- BlogPostB.find({ 'meta.visitors': { $all: [4] } }, function(err, found) {
- assert.ifError(err);
- assert.equal(found.length, 1);
- assert.equal(found[0]._id.toString(), whereoutZero._id);
- BlogPostB.find({ numbers: { $all: [0, -1] } }, function(err, found) {
- assert.ifError(err);
- assert.equal(found.length, 1);
- assert.equal(found[0]._id.toString(), whereZero._id);
- done();
- });
- });
- });
- });
- });
+ it('strings via regexp', async function() {
+ const created = await BlogPostB.create({ title: 'Next to Normal' });
+ let found = await BlogPostB.findOne({ title: /^Next/ });
+ assert.equal(found._id.toString(), created._id);
- it('where $size', function(done) {
- BlogPostB.create({ numbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] }, function(err) {
- assert.ifError(err);
- BlogPostB.create({ numbers: [11, 12, 13, 14, 15, 16, 17, 18, 19, 20] }, function(err) {
- assert.ifError(err);
- BlogPostB.create({ numbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] }, function(err) {
- assert.ifError(err);
- BlogPostB.find({ numbers: { $size: 10 } }, function(err, found) {
- assert.ifError(err);
- assert.equal(found.length, 2);
- BlogPostB.find({ numbers: { $size: 11 } }, function(err, found) {
- assert.ifError(err);
- assert.equal(found.length, 1);
- done();
- });
- });
- });
- });
- });
- });
+ const reg = '^Next to Normal$';
- it('$gt, $lt, $lte, $gte work on strings', function(done) {
- const D = db.model('Test', new Schema({ dt: String }));
+ found = await BlogPostB.find({ title: { $regex: reg } });
+ assert.equal(found.length, 1);
+ assert.equal(found[0]._id.toString(), created._id);
- D.create({ dt: '2011-03-30' }, cb);
- D.create({ dt: '2011-03-31' }, cb);
- D.create({ dt: '2011-04-01' }, cb);
- D.create({ dt: '2011-04-02' }, cb);
+ found = await BlogPostB.findOne({ title: { $regex: reg } });
+ assert.equal(found._id.toString(), created._id);
- let pending = 4;
+ found = await BlogPostB.where('title').regex(reg).findOne();
+ assert.equal(found._id.toString(), created._id);
- function cb(err) {
- assert.ifError(err);
+ found = await BlogPostB.where('title').regex(/^Next/).findOne();
+ assert.equal(found._id.toString(), created._id);
- if (--pending) {
- return;
- }
+ });
- pending = 2;
+ it('a document whose arrays contain at least $all values', async function() {
+ const a1 = { numbers: [-1, -2, -3, -4], meta: { visitors: 4 } };
+ const a2 = { numbers: [0, -1, -2, -3, -4] };
+ const [whereoutZero, whereZero] = await BlogPostB.create([a1, a2]);
- D.find({ dt: { $gte: '2011-03-30', $lte: '2011-04-01' } }).sort('dt').exec(function(err, docs) {
- if (!--pending) {
- done();
- }
- assert.ifError(err);
- assert.equal(docs.length, 3);
- assert.equal(docs[0].dt, '2011-03-30');
- assert.equal(docs[1].dt, '2011-03-31');
- assert.equal(docs[2].dt, '2011-04-01');
- assert.ok(!docs.some(function(d) {
- return d.dt === '2011-04-02';
- }));
- });
+ let found = await BlogPostB.find({ numbers: { $all: [-1, -2, -3, -4] } });
+ assert.equal(found.length, 2);
+ found = await BlogPostB.find({ 'meta.visitors': { $all: [4] } });
+ assert.equal(found.length, 1);
+ assert.equal(found[0]._id.toString(), whereoutZero._id);
+ found = await BlogPostB.find({ numbers: { $all: [0, -1] } });
+ assert.equal(found.length, 1);
+ assert.equal(found[0]._id.toString(), whereZero._id);
+ });
+
+ it('where $size', async function() {
+ await BlogPostB.create({ numbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] });
+ await BlogPostB.create({ numbers: [11, 12, 13, 14, 15, 16, 17, 18, 19, 20] });
+ await BlogPostB.create({ numbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] });
+ let found = await BlogPostB.find({ numbers: { $size: 10 } });
+ assert.equal(found.length, 2);
+ found = await BlogPostB.find({ numbers: { $size: 11 } });
+ assert.equal(found.length, 1);
- D.find({ dt: { $gt: '2011-03-30', $lt: '2011-04-02' } }).sort('dt').exec(function(err, docs) {
- if (!--pending) {
- done();
- }
- assert.ifError(err);
- assert.equal(docs.length, 2);
- assert.equal(docs[0].dt, '2011-03-31');
- assert.equal(docs[1].dt, '2011-04-01');
- assert.ok(!docs.some(function(d) {
- return d.dt === '2011-03-30';
- }));
- assert.ok(!docs.some(function(d) {
- return d.dt === '2011-04-02';
- }));
- });
- }
});
- describe('text search indexes', function() {
- it('works with text search ensure indexes ', function(done) {
- if (!mongo26_or_greater) {
- return done();
- }
+ it('$gt, $lt, $lte, $gte work on strings', async function() {
+ const D = db.model('Test', new Schema({ dt: String }));
+
+ await D.create({ dt: '2011-03-30' });
+ await D.create({ dt: '2011-03-31' });
+ await D.create({ dt: '2011-04-01' });
+ await D.create({ dt: '2011-04-02' });
+
+ let docs = await D.find({ dt: { $gte: '2011-03-30', $lte: '2011-04-01' } }).sort('dt');
+ assert.equal(docs.length, 3);
+ assert.equal(docs[0].dt, '2011-03-30');
+ assert.equal(docs[1].dt, '2011-03-31');
+ assert.equal(docs[2].dt, '2011-04-01');
+ assert.ok(!docs.some(function(d) {
+ return d.dt === '2011-04-02';
+ }));
+
+ docs = await D.find({ dt: { $gt: '2011-03-30', $lt: '2011-04-02' } }).sort('dt');
+ assert.equal(docs.length, 2);
+ assert.equal(docs[0].dt, '2011-03-31');
+ assert.equal(docs[1].dt, '2011-04-01');
+ assert.ok(!docs.some(function(d) {
+ return d.dt === '2011-03-30';
+ }));
+ assert.ok(!docs.some(function(d) {
+ return d.dt === '2011-04-02';
+ }));
+ });
+ describe('text search indexes', function() {
+ it('works with text search ensure indexes ', async function() {
const blogPost = BlogPostB;
- blogPost.collection.createIndex({ title: 'text' }, function(error) {
- assert.ifError(error);
- const a = new blogPost({ title: 'querying in mongoose' });
- const b = new blogPost({ title: 'text search in mongoose' });
- a.save(function(error) {
- assert.ifError(error);
- b.save(function(error) {
- assert.ifError(error);
- blogPost.
- find({ $text: { $search: 'text search' } }, { score: { $meta: 'textScore' } }).
- limit(2).
- exec(function(error, documents) {
- assert.ifError(error);
- assert.equal(documents.length, 1);
- assert.equal(documents[0].title, 'text search in mongoose');
- a.remove({}, function(error) {
- assert.ifError(error);
- b.remove({}, function(error) {
- assert.ifError(error);
- done();
- });
- });
- });
- });
- });
- });
+ await blogPost.collection.createIndex({ title: 'text' });
+ const a = new blogPost({ title: 'querying in mongoose' });
+ const b = new blogPost({ title: 'text search in mongoose' });
+ await a.save();
+ await b.save();
+ const documents = await blogPost.
+ find({ $text: { $search: 'text search' } }, { score: { $meta: 'textScore' } }).
+ limit(2).
+ exec();
+ assert.equal(documents.length, 1);
+ assert.equal(documents[0].title, 'text search in mongoose');
+
});
it('works when text search is called by a schema (gh-3824) (gh-6851)', async function() {
- if (!mongo26_or_greater) {
- return this.skip();
- }
-
const exampleSchema = new Schema({
title: String,
name: { type: String, text: true },
@@ -1467,126 +926,83 @@ describe('model: querying:', function() {
});
describe('limit', function() {
- it('works', function(done) {
- BlogPostB.create({ title: 'first limit' }, function(err, first) {
- assert.ifError(err);
- BlogPostB.create({ title: 'second limit' }, function(err, second) {
- assert.ifError(err);
- BlogPostB.create({ title: 'third limit' }, function(err) {
- assert.ifError(err);
- BlogPostB.find({ title: /limit$/ }).limit(2).find(function(err, found) {
- assert.ifError(err);
- assert.equal(found.length, 2);
- assert.equal(found[0].id, first.id);
- assert.equal(found[1].id, second.id);
- done();
- });
- });
- });
- });
+ it('works', async function() {
+ const first = await BlogPostB.create({ title: 'first limit' });
+ const second = await BlogPostB.create({ title: 'second limit' });
+ await BlogPostB.create({ title: 'third limit' });
+ const found = await BlogPostB.find({ title: /limit$/ }).limit(2);
+ assert.equal(found.length, 2);
+ assert.equal(found[0].id, first.id);
+ assert.equal(found[1].id, second.id);
+
});
});
describe('skip', function() {
- it('works', function(done) {
- BlogPostB.create({ title: '1 skip' }, function(err) {
- assert.ifError(err);
- BlogPostB.create({ title: '2 skip' }, function(err, second) {
- assert.ifError(err);
- BlogPostB.create({ title: '3 skip' }, function(err, third) {
- assert.ifError(err);
- BlogPostB.find({ title: /skip$/ }).sort({ title: 1 }).skip(1).limit(2).find(function(err, found) {
- assert.ifError(err);
- assert.equal(found.length, 2);
- assert.equal(found[0].id, second._id);
- assert.equal(found[1].id, third._id);
- done();
- });
- });
- });
- });
+ it('works', async function() {
+ await BlogPostB.create({ title: '1 skip' });
+ const second = await BlogPostB.create({ title: '2 skip' });
+ const third = await BlogPostB.create({ title: '3 skip' });
+ const found = await BlogPostB.find({ title: /skip$/ }).sort({ title: 1 }).skip(1).limit(2);
+ assert.equal(found.length, 2);
+ assert.equal(found[0]._id.toString(), second._id.toString());
+ assert.equal(found[1]._id.toString(), third._id.toString());
+
});
});
describe('sort', function() {
- it('works', function(done) {
- BlogPostB.create({ meta: { visitors: 100 } }, function(err, least) {
- assert.ifError(err);
- BlogPostB.create({ meta: { visitors: 300 } }, function(err, largest) {
- assert.ifError(err);
- BlogPostB.create({ meta: { visitors: 200 } }, function(err, middle) {
- assert.ifError(err);
- BlogPostB
- .where('meta.visitors').gt(99).lt(301)
- .sort('-meta.visitors')
- .find(function(err, found) {
- assert.ifError(err);
- assert.equal(found.length, 3);
- assert.equal(found[0].id, largest._id);
- assert.equal(found[1].id, middle._id);
- assert.equal(found[2].id, least._id);
- done();
- });
- });
- });
- });
- });
- it('handles sorting by text score', function(done) {
- if (!mongo26_or_greater) {
- return done();
- }
-
+ it('works', async function() {
+ const least = await BlogPostB.create({ meta: { visitors: 100 } });
+ const largest = await BlogPostB.create({ meta: { visitors: 300 } });
+ const middle = await BlogPostB.create({ meta: { visitors: 200 } });
+ const found = await BlogPostB
+ .where('meta.visitors').gt(99).lt(301)
+ .sort('-meta.visitors')
+ .find();
+ assert.equal(found.length, 3);
+ assert.equal(found[0].id, largest._id);
+ assert.equal(found[1].id, middle._id);
+ assert.equal(found[2].id, least._id);
+
+ });
+ it('handles sorting by text score', async function() {
const blogPost = BlogPostB;
- blogPost.collection.createIndex({ title: 'text' }, function(error) {
- assert.ifError(error);
- const a = new blogPost({ title: 'searching in mongoose' });
- const b = new blogPost({ title: 'text search in mongoose' });
- a.save(function(error) {
- assert.ifError(error);
- b.save(function(error) {
- assert.ifError(error);
- blogPost.
- find({ $text: { $search: 'text search' } }, { score: { $meta: 'textScore' } }).
- sort({ score: { $meta: 'textScore' } }).
- limit(2).
- exec(function(error, documents) {
- assert.ifError(error);
- assert.equal(documents.length, 2);
- assert.equal(documents[0].title, 'text search in mongoose');
- assert.equal(documents[1].title, 'searching in mongoose');
- done();
- });
- });
- });
- });
+ await blogPost.collection.createIndex({ title: 'text' });
+ const a = new blogPost({ title: 'searching in mongoose' });
+ const b = new blogPost({ title: 'text search in mongoose' });
+ await a.save();
+ await b.save();
+ const documents = await blogPost.
+ find({ $text: { $search: 'text search' } }, { score: { $meta: 'textScore' } }).
+ sort({ score: { $meta: 'textScore' } }).
+ limit(2).
+ exec();
+ assert.equal(documents.length, 2);
+ assert.equal(documents[0].title, 'text search in mongoose');
+ assert.equal(documents[1].title, 'searching in mongoose');
+
});
});
describe('nested mixed "x.y.z"', function() {
- it('works', function(done) {
- BlogPostB.find({ 'mixed.nested.stuff': 'skynet' }, function(err) {
- assert.ifError(err);
- done();
- });
+ it('works', async function() {
+ await BlogPostB.find({ 'mixed.nested.stuff': 'skynet' });
});
});
- it('by Date (gh-336)', function(done) {
+ it('by Date (gh-336)', async function() {
const Test = db.model('Test', new Schema({ date: Date }));
const now = new Date();
- Test.create({ date: now }, { date: new Date(now - 10000) }, function(err) {
- assert.ifError(err);
- Test.find({ date: now }, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 1);
- done();
- });
- });
+ await Test.create({ date: now }, { date: new Date(now - 10000) });
+ const docs = await Test.find({ date: now });
+ assert.equal(docs.length, 1);
+
});
- it('mixed types with $elemMatch (gh-591)', function(done) {
+ it('mixed types with $elemMatch (gh-591)', async function() {
const S = new Schema({ a: [{}], b: Number });
const M = db.model('Test', S);
@@ -1594,63 +1010,47 @@ describe('model: querying:', function() {
m.a = [1, 2, { name: 'Frodo' }, 'IDK', { name: 100 }];
m.b = 10;
- m.save(function(err) {
- assert.ifError(err);
+ await m.save();
- M.find({ a: { name: 'Frodo' }, b: '10' }, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs[0].a.length, 5);
- assert.equal(docs[0].b.valueOf(), 10);
+ let docs = await M.find({ a: { name: 'Frodo' }, b: '10' });
+ assert.equal(docs[0].a.length, 5);
+ assert.equal(docs[0].b.valueOf(), 10);
- const query = {
- a: {
- $elemMatch: { name: 100 }
- }
- };
-
- M.find(query, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs[0].a.length, 5);
- done();
- });
- });
- });
+ const query = {
+ a: {
+ $elemMatch: { name: 100 }
+ }
+ };
+ docs = await M.find(query);
+ assert.equal(docs[0].a.length, 5);
});
describe('$all', function() {
- it('with ObjectIds (gh-690)', function(done) {
+ it('with ObjectIds (gh-690)', async function() {
const SSchema = new Schema({ name: String });
const PSchema = new Schema({ sub: [SSchema] });
const P = db.model('Test', PSchema);
const sub = [{ name: 'one' }, { name: 'two' }, { name: 'three' }];
- P.create({ sub: sub }, function(err, p) {
- assert.ifError(err);
+ const p = await P.create({ sub: sub });
- const o0 = p.sub[0]._id;
- const o1 = p.sub[1]._id;
- const o2 = p.sub[2]._id;
+ const o0 = p.sub[0]._id;
+ const o1 = p.sub[1]._id;
+ const o2 = p.sub[2]._id;
- P.findOne({ 'sub._id': { $all: [o1, o2.toString()] } }, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.id, p.id);
+ let doc = await P.findOne({ 'sub._id': { $all: [o1, o2.toString()] } });
+ assert.equal(doc.id, p.id);
- P.findOne({ 'sub._id': { $all: [o0, new DocumentObjectId()] } }, function(err, doc) {
- assert.ifError(err);
- assert.equal(!!doc, false);
+ doc = await P.findOne({ 'sub._id': { $all: [o0, new DocumentObjectId()] } });
+ assert.equal(!!doc, false);
+
+ doc = await P.findOne({ 'sub._id': { $all: [o2] } });
+ assert.equal(doc.id, p.id);
- P.findOne({ 'sub._id': { $all: [o2] } }, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.id, p.id);
- done();
- });
- });
- });
- });
});
- it('with Dates', function(done) {
+ it('with Dates', async function() {
this.timeout(4500);
const SSchema = new Schema({ d: Date });
const PSchema = new Schema({ sub: [SSchema] });
@@ -1662,29 +1062,21 @@ describe('model: querying:', function() {
{ d: new Date(Date.now() - 30000) }
];
- P.create({ sub: sub }, function(err, p) {
- assert.ifError(err);
+ const p = await P.create({ sub: sub });
- const o0 = p.sub[0].d;
- const o1 = p.sub[1].d;
- const o2 = p.sub[2].d;
+ const o0 = p.sub[0].d;
+ const o1 = p.sub[1].d;
+ const o2 = p.sub[2].d;
- P.findOne({ 'sub.d': { $all: [o1, o2] } }, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.id, p.id);
+ let doc = await P.findOne({ 'sub.d': { $all: [o1, o2] } });
+ assert.equal(doc.id, p.id);
- P.findOne({ 'sub.d': { $all: [o0, new Date()] } }, function(err, doc) {
- assert.ifError(err);
- assert.equal(!!doc, false);
+ doc = await P.findOne({ 'sub.d': { $all: [o0, new Date()] } });
+ assert.equal(!!doc, false);
+
+ doc = await P.findOne({ 'sub.d': { $all: [o2] } });
+ assert.equal(doc.id, p.id);
- P.findOne({ 'sub.d': { $all: [o2] } }, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.id, p.id);
- done();
- });
- });
- });
- });
});
it('with $elemMatch (gh-3163)', async function() {
@@ -1714,60 +1106,45 @@ describe('model: querying:', function() {
});
describe('and', function() {
- it('works with queries gh-1188', function(done) {
+ it('works with queries gh-1188', async function() {
const B = BlogPostB;
- B.create({ title: 'and operator', published: false, author: 'Me' }, function(err) {
- assert.ifError(err);
-
- B.find({ $and: [{ title: 'and operator' }] }, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 1);
-
- B.find({ $and: [{ title: 'and operator' }, { published: true }] }, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 0);
-
- B.find({ $and: [{ title: 'and operator' }, { published: false }] }, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 1);
-
- const query = B.find();
- query.and([
- { title: 'and operator', published: false },
- { author: 'Me' }
- ]);
- query.exec(function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 1);
-
- const query = B.find();
- query.and([
- { title: 'and operator', published: false },
- { author: 'You' }
- ]);
- query.exec(function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 0);
- done();
- });
- });
- });
- });
- });
- });
+ await B.create({ title: 'and operator', published: false, author: 'Me' });
+
+ let docs = await B.find({ $and: [{ title: 'and operator' }] });
+ assert.equal(docs.length, 1);
+
+ docs = await B.find({ $and: [{ title: 'and operator' }, { published: true }] });
+ assert.equal(docs.length, 0);
+
+ docs = await B.find({ $and: [{ title: 'and operator' }, { published: false }] });
+ assert.equal(docs.length, 1);
+
+ let query = B.find();
+ query.and([
+ { title: 'and operator', published: false },
+ { author: 'Me' }
+ ]);
+ docs = await query.exec();
+ assert.equal(docs.length, 1);
+
+ query = B.find();
+ query.and([
+ { title: 'and operator', published: false },
+ { author: 'You' }
+ ]);
+ docs = await query.exec();
+ assert.equal(docs.length, 0);
+
});
- it('works with nested query selectors gh-1884', function(done) {
+ it('works with nested query selectors gh-1884', async function() {
const B = db.model('Test', { a: String, b: String });
- B.deleteOne({ $and: [{ a: 'coffee' }, { b: { $in: ['bacon', 'eggs'] } }] }, function(error) {
- assert.ifError(error);
- done();
- });
+ await B.deleteOne({ $and: [{ a: 'coffee' }, { b: { $in: ['bacon', 'eggs'] } }] });
});
});
- it('works with different methods and query types', function(done) {
+ it('works with different methods and query types', async function() {
const BufSchema = new Schema({ name: String, block: Buffer });
const Test = db.model('Test', BufSchema);
@@ -1776,53 +1153,35 @@ describe('model: querying:', function() {
const docC = { name: 'C', block: 'hello world' };
const docD = { name: 'D', block: { type: 'Buffer', data: [103, 104, 45, 54, 56, 54, 51] } };
- Test.create(docA, docB, docC, docD, function(err, a, b, c, d) {
- assert.ifError(err);
+ const [a, b, c, d] = await Test.create([docA, docB, docC, docD]);
- assert.equal(a.block.toString('utf8'), 'über');
- assert.equal(b.block.toString('utf8'), 'buffer shtuffs are neat');
- assert.equal(c.block.toString('utf8'), 'hello world');
- assert.equal(d.block.toString('utf8'), 'gh-6863');
+ assert.equal(a.block.toString('utf8'), 'über');
+ assert.equal(b.block.toString('utf8'), 'buffer shtuffs are neat');
+ assert.equal(c.block.toString('utf8'), 'hello world');
+ assert.equal(d.block.toString('utf8'), 'gh-6863');
- Test.findById(a._id, function(err, a) {
- assert.ifError(err);
- assert.equal(a.block.toString('utf8'), 'über');
+ const ra = await Test.findById(a._id);
+ assert.equal(ra.block.toString('utf8'), 'über');
- Test.findOne({ block: 'buffer shtuffs are neat' }, function(err, rb) {
- assert.ifError(err);
- assert.equal(rb.block.toString('utf8'), 'buffer shtuffs are neat');
+ let rb = await Test.findOne({ block: 'buffer shtuffs are neat' });
+ assert.equal(rb.block.toString('utf8'), 'buffer shtuffs are neat');
- Test.findOne({ block: /buffer/i }, function(err) {
- assert.equal(err.message, 'Cast to Buffer failed for value ' +
+ const err = await Test.findOne({ block: /buffer/i }).then(() => null, err => err);
+ assert.equal(err.message, 'Cast to Buffer failed for value ' +
'"/buffer/i" (type RegExp) at path "block" for model "Test"');
- Test.findOne({ block: [195, 188, 98, 101, 114] }, function(err, rb) {
- assert.ifError(err);
- assert.equal(rb.block.toString('utf8'), 'über');
-
- Test.findOne({ block: 'aGVsbG8gd29ybGQ=' }, function(err, rb) {
- assert.ifError(err);
- assert.strictEqual(rb, null);
-
- Test.findOne({ block: Buffer.from('aGVsbG8gd29ybGQ=', 'base64') }, function(err, rb) {
- assert.ifError(err);
- assert.equal(rb.block.toString('utf8'), 'hello world');
-
- Test.findOne({ block: { type: 'Buffer', data: [195, 188, 98, 101, 114] } }, function(err, rb) {
- assert.ifError(err);
- assert.equal(rb.block.toString('utf8'), 'über');
-
- Test.deleteOne({}, function(err) {
- assert.ifError(err);
- done();
- });
- });
- });
- });
- });
- });
- });
- });
- });
+ rb = await Test.findOne({ block: [195, 188, 98, 101, 114] });
+ assert.equal(rb.block.toString('utf8'), 'über');
+
+ rb = await Test.findOne({ block: 'aGVsbG8gd29ybGQ=' });
+ assert.strictEqual(rb, null);
+
+ rb = await Test.findOne({ block: Buffer.from('aGVsbG8gd29ybGQ=', 'base64') });
+ assert.equal(rb.block.toString('utf8'), 'hello world');
+
+ rb = await Test.findOne({ block: { type: 'Buffer', data: [195, 188, 98, 101, 114] } });
+ assert.equal(rb.block.toString('utf8'), 'über');
+
+
});
it('with conditionals', async function() {
@@ -1902,122 +1261,64 @@ describe('model: querying:', function() {
await Test.deleteOne({});
});
- it('with previously existing null values in the db', function(done) {
+ it('with previously existing null values in the db', async function() {
const post = new BlogPostB();
const doc = { meta: { visitors: 9898, a: null } };
- post.collection.insertOne(doc, {}, function(err) {
- assert.ifError(err);
+ await post.collection.insertOne(doc, {});
+
+ const found = await BlogPostB.findOne({ _id: doc._id });
+ assert.equal(found.get('meta.visitors').valueOf(), 9898);
- BlogPostB.findOne({ _id: doc._id }, function(err, found) {
- assert.ifError(err);
- assert.equal(found.get('meta.visitors').valueOf(), 9898);
- done();
- });
- });
});
- it('with unused values in the db', function(done) {
+ it('with unused values in the db', async function() {
const post = new BlogPostB();
const doc = { meta: { visitors: 9898, color: 'blue' } };
- post.collection.insertOne(doc, {}, function(err) {
- assert.ifError(err);
-
- BlogPostB.findOne({ _id: doc._id }, function(err, found) {
- assert.ifError(err);
- assert.equal(found.get('meta.visitors').valueOf(), 9898);
- found.save(function(err) {
- assert.ifError(err);
- done();
- });
- });
- });
+ await post.collection.insertOne(doc, {});
+
+ const found = await BlogPostB.findOne({ _id: doc._id });
+ assert.equal(found.get('meta.visitors').valueOf(), 9898);
+ await found.save();
});
describe('2d', function() {
- it('$near (gh-309)', function(done) {
+ it('$near (gh-309)', async function() {
const Test = db.model('Test', geoSchema);
- let pending = 2;
-
- function complete(err) {
- if (complete.ran) {
- return;
- }
- if (err) {
- return done(complete.ran = err);
- }
- --pending || test();
- }
+ await Test.init();
+ await Test.create({ loc: [10, 20] }, { loc: [40, 90] });
- Test.on('index', complete);
- Test.create({ loc: [10, 20] }, { loc: [40, 90] }, complete);
+ const docs = await Test.find({ loc: { $near: [30, 40] } });
+ assert.equal(docs.length, 2);
- function test() {
- Test.find({ loc: { $near: [30, 40] } }, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 2);
- done();
- });
- }
});
- it('$within arrays (gh-586)', function(done) {
+ it('$within arrays (gh-586)', async function() {
const Test = db.model('Test', geoSchema);
- let pending = 2;
+ await Test.init();
+ await Test.create({ loc: [35, 50] }, { loc: [-40, -90] });
- function complete(err) {
- if (complete.ran) {
- return;
- }
- if (err) {
- return done(complete.ran = err);
- }
- --pending || test();
- }
+ const docs = await Test.find({ loc: { $within: { $box: [[30, 40], [40, 60]] } } });
+ assert.equal(docs.length, 1);
- Test.on('index', complete);
- Test.create({ loc: [35, 50] }, { loc: [-40, -90] }, complete);
- function test() {
- Test.find({ loc: { $within: { $box: [[30, 40], [40, 60]] } } }, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 1);
- done();
- });
- }
});
- it('$nearSphere with arrays (gh-610)', function(done) {
+ it('$nearSphere with arrays (gh-610)', async function() {
const Test = db.model('Test', geoSchema);
- let pending = 2;
+ await Test.init();
+ await Test.create({ loc: [10, 20] }, { loc: [40, 90] });
- function complete(err) {
- if (complete.ran) {
- return;
- }
- if (err) {
- return done(complete.ran = err);
- }
- --pending || test();
- }
+ const docs = await Test.find({ loc: { $nearSphere: [30, 40] } });
+ assert.equal(docs.length, 2);
- Test.on('index', complete);
- Test.create({ loc: [10, 20] }, { loc: [40, 90] }, complete);
-
- function test() {
- Test.find({ loc: { $nearSphere: [30, 40] } }, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 2);
- done();
- });
- }
});
- it('$nearSphere with invalid coordinate does not crash (gh-1874)', function(done) {
+ it('$nearSphere with invalid coordinate does not crash (gh-1874)', async function() {
const geoSchema = new Schema({
loc: {
type: { type: String },
@@ -2026,73 +1327,37 @@ describe('model: querying:', function() {
});
const Test = db.model('Test', geoSchema);
- let pending = 2;
- const complete = function(err) {
- if (complete.ran) {
- return;
- }
- if (err) {
- done(complete.ran = err);
- return;
- }
- --pending || test();
- };
- Test.on('index', complete);
- Test.create(
+ await Test.init();
+ await Test.create(
{ loc: { coordinates: [30, 41] } },
- { loc: { coordinates: [31, 40] } },
- complete);
+ { loc: { coordinates: [31, 40] } }
+ );
- const test = function() {
- const q = new Query({}, {}, null, Test.collection);
- q.find({
- loc: {
- $nearSphere: {
- $geometry: { type: 'Point', coordinates: [30, 40] },
- $maxDistance: 10000000
- }
+ const q = new Query({}, {}, null, Test.collection);
+ q.find({
+ loc: {
+ $nearSphere: {
+ $geometry: { type: 'Point', coordinates: [30, 40] },
+ $maxDistance: 10000000
}
- });
-
- assert.doesNotThrow(function() {
- q.cast(Test);
- });
+ }
+ });
- done();
- };
+ q.cast(Test);
});
- it('$maxDistance with arrays', function(done) {
+ it('$maxDistance with arrays', async function() {
const Test = db.model('Test', geoSchema);
- let pending = 2;
+ await Test.init();
+ await Test.create({ loc: [20, 80] }, { loc: [25, 30] });
- function complete(err) {
- if (complete.ran) {
- return;
- }
- if (err) {
- done(complete.ran = err);
- return;
- }
- --pending || test();
- }
+ let docs = await Test.find({ loc: { $near: [25, 31], $maxDistance: 1 } });
+ assert.equal(docs.length, 1);
+ docs = await Test.find({ loc: { $near: [25, 32], $maxDistance: 1 } });
+ assert.equal(docs.length, 0);
- Test.on('index', complete);
- Test.create({ loc: [20, 80] }, { loc: [25, 30] }, complete);
-
- function test() {
- Test.find({ loc: { $near: [25, 31], $maxDistance: 1 } }, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 1);
- Test.find({ loc: { $near: [25, 32], $maxDistance: 1 } }, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 0);
- done();
- });
- });
- }
});
});
@@ -2163,10 +1428,6 @@ describe('model: querying:', function() {
describe('$geoIntersects', function() {
it('LineString', async function() {
- if (!mongo24_or_greater) {
- return;
- }
-
const Test = db.model('Test', geoSchema);
await Test.init();
@@ -2185,138 +1446,85 @@ describe('model: querying:', function() {
assert.equal(created.id, doc.id);
});
- it('MultiLineString', function(done) {
- if (!mongo24_or_greater) {
- return done();
- }
-
+ it('MultiLineString', async function() {
const Test = db.model('Test', geoMultiSchema);
- Test.create({
+ const created = await Test.create({
geom: [{ type: 'LineString', coordinates: [[-178.0, 10.0], [178.0, 10.0]] },
{ type: 'LineString', coordinates: [[-178.0, 5.0], [178.0, 5.0]] }]
- }, function(err, created) {
- assert.ifError(err);
-
- const geojsonLine = { type: 'LineString', coordinates: [[180.0, 11.0], [180.0, '9.00']] };
-
- Test.find({ geom: { $geoIntersects: { $geometry: geojsonLine } } }, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 1);
- assert.equal(created.id, docs[0].id);
-
- Test.where('geom').intersects().geometry(geojsonLine).findOne(function(err, doc) {
- assert.ifError(err);
- assert.equal(created.id, doc.id);
- done();
- });
- });
});
- });
+ const geojsonLine = { type: 'LineString', coordinates: [[180.0, 11.0], [180.0, '9.00']] };
- it('MultiPolygon', function(done) {
- if (!mongo24_or_greater) {
- return done();
- }
+ const docs = await Test.find({ geom: { $geoIntersects: { $geometry: geojsonLine } } });
+ assert.equal(docs.length, 1);
+ assert.equal(created.id, docs[0].id);
+
+ const doc = await Test.where('geom').intersects().geometry(geojsonLine).findOne();
+ assert.equal(created.id, doc.id);
+
+ });
+ it('MultiPolygon', async function() {
const Test = db.model('Test', geoMultiSchema);
- Test.create({
+ const created = await Test.create({
geom: [{ type: 'Polygon', coordinates: [[[28.7, 41], [29.2, 40.9], [29.1, 41.3], [28.7, 41]]] },
{ type: 'Polygon', coordinates: [[[-1, -1], [1, -1], [1, 1], [-1, 1], [-1, -1]]] }]
- }, function(err, created) {
- assert.ifError(err);
-
- const geojsonPolygon = { type: 'Polygon', coordinates: [[[26, 36], [45, 36], [45, 42], [26, 42], [26, 36]]] };
-
- Test.find({ geom: { $geoIntersects: { $geometry: geojsonPolygon } } }, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 1);
- assert.equal(created.id, docs[0].id);
-
- Test.where('geom').intersects().geometry(geojsonPolygon).findOne(function(err, doc) {
- assert.ifError(err);
- assert.equal(created.id, doc.id);
- done();
- });
- });
});
+
+ const geojsonPolygon = { type: 'Polygon', coordinates: [[[26, 36], [45, 36], [45, 42], [26, 42], [26, 36]]] };
+
+ const docs = await Test.find({ geom: { $geoIntersects: { $geometry: geojsonPolygon } } });
+ assert.equal(docs.length, 1);
+ assert.equal(created.id, docs[0].id);
+
+ const doc = await Test.where('geom').intersects().geometry(geojsonPolygon).findOne();
+ assert.equal(created.id, doc.id);
+
});
});
describe('$near', function() {
- it('Point', function(done) {
- if (!mongo24_or_greater) {
- return done();
- }
-
+ it('Point', async function() {
const Test = db.model('Test', geoSchema);
- Test.on('index', function(err) {
- assert.ifError(err);
+ await Test.init();
- Test.create({ line: { type: 'Point', coordinates: [-179.0, 0.0] } }, function(err, created) {
- assert.ifError(err);
+ const created = await Test.create({ line: { type: 'Point', coordinates: [-179.0, 0.0] } });
- const geojsonPoint = { type: 'Point', coordinates: [-179.0, 0.0] };
+ const geojsonPoint = { type: 'Point', coordinates: [-179.0, 0.0] };
- Test.find({ line: { $near: geojsonPoint } }, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 1);
- assert.equal(created.id, docs[0].id);
+ let docs = await Test.find({ line: { $near: geojsonPoint } });
+ assert.equal(docs.length, 1);
+ assert.equal(created.id, docs[0].id);
- Test.find({ line: { $near: { $geometry: geojsonPoint, $maxDistance: 50 } } }, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 1);
- assert.equal(created.id, docs[0].id);
- done();
- });
- });
- });
- });
- });
+ docs = await Test.find({ line: { $near: { $geometry: geojsonPoint, $maxDistance: 50 } } });
+ assert.equal(docs.length, 1);
+ assert.equal(created.id, docs[0].id);
- it('works with GeoJSON (gh-1482)', function(done) {
- if (!mongo24_or_greater) {
- return done();
- }
+ });
+ it('works with GeoJSON (gh-1482)', async function() {
const geoJSONSchema = new Schema({ loc: { type: { type: String }, coordinates: [Number] } });
geoJSONSchema.index({ loc: '2dsphere' });
const Test = db.model('Test', geoJSONSchema);
+ await Test.init();
- let pending = 2;
-
- function complete(err) {
- if (complete.ran) {
- return;
- }
- if (err) {
- return done(complete.ran = err);
- }
- --pending || test();
- }
-
- Test.on('index', complete);
- Test.create({ loc: { type: 'Point', coordinates: [10, 20] } }, {
+ await Test.create({ loc: { type: 'Point', coordinates: [10, 20] } }, {
loc: {
type: 'Point', coordinates: [40, 90]
}
- }, complete);
-
- function test() {
- // $maxDistance is in meters... so even though they aren't that far off
- // in lat/long, need an incredibly high number here
- Test.where('loc').near({
- center: {
- type: 'Point', coordinates: [11, 20]
- }, maxDistance: 1000000
- }).exec(function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 1);
- done();
- });
- }
+ });
+
+ // $maxDistance is in meters... so even though they aren't that far off
+ // in lat/long, need an incredibly high number here
+ const docs = await Test.where('loc').near({
+ center: {
+ type: 'Point', coordinates: [11, 20]
+ }, maxDistance: 1000000
+ }).exec();
+ assert.equal(docs.length, 1);
+
});
it('works with legacy 2dsphere pair in schema (gh-6937)', async function() {
if (!mongo24_or_greater) {
@@ -2346,56 +1554,39 @@ describe('model: querying:', function() {
}
});
- it('work', function(done) {
- if (!mongo24_or_greater) {
- return done();
- }
-
+ it('work', async function() {
const schema = new Schema({ t: { type: String, index: 'hashed' } });
const H = db.model('Test', schema);
- H.on('index', function(err) {
- assert.ifError(err);
- H.collection.getIndexes({ full: true }, function(err, indexes) {
- assert.ifError(err);
-
- const found = indexes.some(function(index) {
- return index.key.t === 'hashed';
- });
- assert.ok(found);
-
- H.create({ t: 'hashing' }, {}, function(err, doc1, doc2) {
- assert.ifError(err);
- assert.ok(doc1);
- assert.ok(doc2);
- done();
- });
- });
+ await H.init();
+ const indexes = await H.collection.getIndexes({ full: true });
+
+ const found = indexes.some(function(index) {
+ return index.key.t === 'hashed';
});
+ assert.ok(found);
+
+ const [doc1, doc2] = await H.create([{ t: 'hashing' }, {}]);
+ assert.ok(doc1);
+ assert.ok(doc2);
});
});
describe('lean', function() {
- it('find', function(done) {
+ it('find', async function() {
const title = 'Wooooot ' + random();
const post = new BlogPostB();
post.set('title', title);
- post.save(function(err) {
- assert.ifError(err);
- BlogPostB.find({ title: title }).lean().exec(function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 1);
- assert.strictEqual(docs[0] instanceof mongoose.Document, false);
- BlogPostB.find({ title: title }, null, { lean: true }, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 1);
- assert.strictEqual(docs[0] instanceof mongoose.Document, false);
- done();
- });
- });
- });
+ await post.save();
+ let docs = await BlogPostB.find({ title: title }).lean().exec();
+ assert.equal(docs.length, 1);
+ assert.strictEqual(docs[0] instanceof mongoose.Document, false);
+ docs = await BlogPostB.find({ title: title }, null, { lean: true });
+ assert.equal(docs.length, 1);
+ assert.strictEqual(docs[0] instanceof mongoose.Document, false);
+
});
it('removes the __v property if versionKey: false is set (gh-8934)', async function() {
@@ -2409,21 +1600,17 @@ describe('model: querying:', function() {
assert.ok(!('__v' in updateFoundPost));
});
- it('findOne', function(done) {
+ it('findOne', async function() {
const title = 'Wooooot ' + random();
const post = new BlogPostB();
post.set('title', title);
- post.save(function(err) {
- assert.ifError(err);
- BlogPostB.findOne({ title: title }, null, { lean: true }, function(err, doc) {
- assert.ifError(err);
- assert.ok(doc);
- assert.strictEqual(false, doc instanceof mongoose.Document);
- done();
- });
- });
+ await post.save();
+ const doc = await BlogPostB.findOne({ title: title }, null, { lean: true });
+ assert.ok(doc);
+ assert.strictEqual(false, doc instanceof mongoose.Document);
+
});
it('properly casts nested and/or queries (gh-676)', function(done) {
const sch = new Schema({
@@ -2447,7 +1634,7 @@ describe('model: querying:', function() {
assert.equal(typeof q._conditions.$and[1].$and[1].num, 'number');
done();
});
- it('properly casts deeply nested and/or queries (gh-676)', function(done) {
+ it('properly casts deeply nested and/or queries (gh-676)', function() {
const sch = new Schema({
num: Number,
subdoc: { title: String, num: Number }
@@ -2462,25 +1649,20 @@ describe('model: querying:', function() {
q._castConditions();
assert.equal(typeof q._conditions.$and[0].$or[0].$and[0].$or[0].num, 'number');
assert.equal(typeof q._conditions.$and[0].$or[0].$and[0].$or[1]['subdoc.num'], 'number');
- done();
});
- it('casts $elemMatch (gh-2199)', function(done) {
+ it('casts $elemMatch (gh-2199)', async function() {
const schema = new Schema({ dates: [Date] });
const Dates = db.model('Test', schema);
const array = ['2014-07-01T02:00:00.000Z', '2014-07-01T04:00:00.000Z'];
- Dates.create({ dates: array }, function(err) {
- assert.ifError(err);
- const elemMatch = { $gte: '2014-07-01T03:00:00.000Z' };
- Dates.findOne({}, { dates: { $elemMatch: elemMatch } }, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.dates.length, 1);
- assert.equal(doc.dates[0].getTime(),
- new Date('2014-07-01T04:00:00.000Z').getTime());
- done();
- });
- });
+ await Dates.create({ dates: array });
+ const elemMatch = { $gte: '2014-07-01T03:00:00.000Z' };
+ const doc = await Dates.findOne({}, { dates: { $elemMatch: elemMatch } });
+ assert.equal(doc.dates.length, 1);
+ assert.equal(doc.dates[0].getTime(),
+ new Date('2014-07-01T04:00:00.000Z').getTime());
+
});
it('does not run resetId setter on query (gh-6093)', function() {
@@ -2492,27 +1674,11 @@ describe('model: querying:', function() {
});
describe('$eq', function() {
- let mongo26 = false;
-
- before(async function() {
- const version = await start.mongodVersion();
-
- mongo26 = version[0] > 2 || (version[0] === 2 && version[1] >= 6);
- });
-
- it('casts $eq (gh-2752)', function(done) {
- BlogPostB.findOne(
- { _id: { $eq: '000000000000000000000001' }, numbers: { $eq: [1, 2] } },
- function(err, doc) {
- if (mongo26) {
- assert.ifError(err);
- } else {
- assert.ok(err.toString().indexOf('MongoServerError') !== -1);
- }
-
- assert.ok(!doc);
- done();
- });
+ it('casts $eq (gh-2752)', async function() {
+ const doc = await BlogPostB.findOne(
+ { _id: { $eq: '000000000000000000000001' }, numbers: { $eq: [1, 2] } }
+ );
+ assert.ok(!doc);
});
});
});
diff --git a/test/model.test.js b/test/model.test.js
index 848a1d6455b..af518fe7340 100644
--- a/test/model.test.js
+++ b/test/model.test.js
@@ -7,8 +7,10 @@ const sinon = require('sinon');
const start = require('./common');
const assert = require('assert');
+const { once } = require('events');
const random = require('./util').random;
const util = require('./util');
+const model = require('../lib/model');
const mongoose = start.mongoose;
const Schema = mongoose.Schema;
@@ -123,32 +125,29 @@ describe('Model', function() {
});
describe('constructor', function() {
- it('works without "new" keyword', function(done) {
+ it('works without "new" keyword', function() {
const B = BlogPost;
let b = B();
assert.ok(b instanceof B);
b = B();
assert.ok(b instanceof B);
- done();
});
- it('works "new" keyword', function(done) {
+ it('works "new" keyword', function() {
const B = BlogPost;
let b = new B();
assert.ok(b instanceof B);
b = new B();
assert.ok(b instanceof B);
- done();
});
});
describe('isNew', function() {
- it('is true on instantiation', function(done) {
+ it('is true on instantiation', function() {
const post = new BlogPost();
assert.equal(post.isNew, true);
- done();
});
});
- it('gh-2140', function(done) {
+ it('gh-2140', function() {
db.deleteModel(/Test/);
const S = new Schema({
field: [{ text: String }]
@@ -160,16 +159,14 @@ describe('Model', function() {
s.field = [{ text: 'text' }];
assert.ok(s.field[0]);
- done();
});
describe('schema', function() {
- it('should exist', function(done) {
+ it('should exist', function() {
assert.ok(BlogPost.schema instanceof Schema);
assert.ok(BlogPost.prototype.schema instanceof Schema);
- done();
});
- it('emits init event', function(done) {
+ it('emits init event', function() {
const schema = new Schema({ name: String });
let model;
@@ -180,12 +177,11 @@ describe('Model', function() {
db.deleteModel(/Test/);
const Named = db.model('Test', schema);
assert.equal(model, Named);
- done();
});
});
describe('structure', function() {
- it('default when instantiated', function(done) {
+ it('default when instantiated', function() {
const post = new BlogPost();
assert.equal(post.db.model('BlogPost').modelName, 'BlogPost');
assert.equal(post.constructor.modelName, 'BlogPost');
@@ -208,15 +204,14 @@ describe('Model', function() {
assert.ok(post.get('owners').isMongooseArray);
assert.ok(post.get('comments').isMongooseDocumentArray);
assert.ok(post.get('nested.array').isMongooseArray);
- done();
});
describe('array', function() {
describe('defaults', function() {
- it('to a non-empty array', function(done) {
+ it('to a non-empty array', function() {
const DefaultArraySchema = new Schema({
- arr: { type: Array, cast: String, default: ['a', 'b', 'c'] },
- single: { type: Array, cast: String, default: ['a'] }
+ arr: { type: Array, default: ['a', 'b', 'c'] },
+ single: { type: Array, default: ['a'] }
});
const DefaultArray = db.model('Test', DefaultArraySchema);
const arr = new DefaultArray();
@@ -226,12 +221,11 @@ describe('Model', function() {
assert.equal(arr.get('arr')[2], 'c');
assert.equal(arr.get('single').length, 1);
assert.equal(arr.get('single')[0], 'a');
- done();
});
- it('empty', function(done) {
+ it('empty', function() {
const DefaultZeroCardArraySchema = new Schema({
- arr: { type: Array, cast: String, default: [] },
+ arr: { type: Array, default: [] },
auto: [Number]
});
const DefaultZeroCardArray = db.model('Test', DefaultZeroCardArraySchema);
@@ -239,70 +233,39 @@ describe('Model', function() {
assert.equal(arr.get('arr').length, 0);
assert.equal(arr.arr.length, 0);
assert.equal(arr.auto.length, 0);
- done();
});
});
});
- it('a hash with one null value', function(done) {
+ it('a hash with one null value', function() {
const post = new BlogPost({
title: null
});
assert.strictEqual(null, post.title);
- done();
});
- it('when saved', function(done) {
- let pending = 2;
-
- function cb() {
- if (--pending) {
- return;
- }
- done();
- }
-
+ it('when saved', async function() {
const post = new BlogPost();
- post.on('save', function(post) {
- assert.ok(post.get('_id') instanceof DocumentObjectId);
-
- assert.equal(post.get('title'), undefined);
- assert.equal(post.get('slug'), undefined);
- assert.equal(post.get('date'), undefined);
- assert.equal(post.get('published'), undefined);
-
- assert.equal(typeof post.get('meta'), 'object');
- assert.deepEqual(post.get('meta'), {});
- assert.equal(post.get('meta.date'), undefined);
- assert.equal(post.get('meta.visitors'), undefined);
-
- assert.ok(post.get('owners').isMongooseArray);
- assert.ok(post.get('comments').isMongooseDocumentArray);
- cb();
- });
- post.save(function(err, post) {
- assert.ifError(err);
- assert.ok(post.get('_id') instanceof DocumentObjectId);
+ await post.save();
+ assert.ok(post.get('_id') instanceof DocumentObjectId);
- assert.equal(post.get('title'), undefined);
- assert.equal(post.get('slug'), undefined);
- assert.equal(post.get('date'), undefined);
- assert.equal(post.get('published'), undefined);
+ assert.equal(post.get('title'), undefined);
+ assert.equal(post.get('slug'), undefined);
+ assert.equal(post.get('date'), undefined);
+ assert.equal(post.get('published'), undefined);
- assert.equal(typeof post.get('meta'), 'object');
- assert.deepEqual(post.get('meta'), {});
- assert.equal(post.get('meta.date'), undefined);
- assert.equal(post.get('meta.visitors'), undefined);
+ assert.equal(typeof post.get('meta'), 'object');
+ assert.deepEqual(post.get('meta'), {});
+ assert.equal(post.get('meta.date'), undefined);
+ assert.equal(post.get('meta.visitors'), undefined);
- assert.ok(post.get('owners').isMongooseArray);
- assert.ok(post.get('comments').isMongooseDocumentArray);
- cb();
- });
+ assert.ok(post.get('owners').isMongooseArray);
+ assert.ok(post.get('comments').isMongooseDocumentArray);
});
describe('init', function() {
- it('works', function(done) {
+ it('works', async function() {
const post = new BlogPost();
post.init({
@@ -352,10 +315,9 @@ describe('Model', function() {
assert.ok(post.comments.isMongooseDocumentArray);
assert.ok(post.comments[0] instanceof EmbeddedDocument);
assert.ok(post.comments[1] instanceof EmbeddedDocument);
- done();
});
- it('partially', function(done) {
+ it('partially', function() {
const post = new BlogPost();
post.init({
title: 'Test',
@@ -375,10 +337,9 @@ describe('Model', function() {
assert.ok(post.get('owners').isMongooseArray);
assert.ok(post.get('comments').isMongooseDocumentArray);
- done();
});
- it('with partial hash', function(done) {
+ it('with partial hash', function() {
const post = new BlogPost({
meta: {
date: new Date(),
@@ -387,10 +348,9 @@ describe('Model', function() {
});
assert.equal(post.get('meta.visitors').valueOf(), 5);
- done();
});
- it('isNew on embedded documents', function(done) {
+ it('isNew on embedded documents', function() {
const post = new BlogPost();
post.init({
title: 'Test',
@@ -399,32 +359,28 @@ describe('Model', function() {
});
assert.equal(post.get('comments')[0].isNew, false);
- done();
});
- it('isNew on embedded documents after saving', function(done) {
+ it('isNew on embedded documents after saving', async function() {
const post = new BlogPost({ title: 'hocus pocus' });
post.comments.push({ title: 'Humpty Dumpty', comments: [{ title: 'nested' }] });
assert.equal(post.get('comments')[0].isNew, true);
assert.equal(post.get('comments')[0].comments[0].isNew, true);
post.invalidate('title'); // force error
- post.save(function() {
- assert.equal(post.isNew, true);
- assert.equal(post.get('comments')[0].isNew, true);
- assert.equal(post.get('comments')[0].comments[0].isNew, true);
- post.save(function(err) {
- assert.strictEqual(null, err);
- assert.equal(post.isNew, false);
- assert.equal(post.get('comments')[0].isNew, false);
- assert.equal(post.get('comments')[0].comments[0].isNew, false);
- done();
- });
- });
+
+ await post.save().catch(() => {});
+ assert.equal(post.isNew, true);
+ assert.equal(post.get('comments')[0].isNew, true);
+ assert.equal(post.get('comments')[0].comments[0].isNew, true);
+ await post.save();
+ assert.equal(post.isNew, false);
+ assert.equal(post.get('comments')[0].isNew, false);
+ assert.equal(post.get('comments')[0].comments[0].isNew, false);
});
});
});
- it('collection name can be specified through schema', function(done) {
+ it('collection name can be specified through schema', function() {
const schema = new Schema({ name: String }, { collection: 'tests' });
const Named = mongoose.model('CollectionNamedInSchema1', schema);
assert.equal(Named.prototype.collection.name, 'tests');
@@ -432,25 +388,19 @@ describe('Model', function() {
const users2schema = new Schema({ name: String }, { collection: 'tests' });
const Named2 = db.model('FooBar', users2schema);
assert.equal(Named2.prototype.collection.name, 'tests');
- done();
});
- it('saving a model with a null value should perpetuate that null value to the db', function(done) {
+ it('saving a model with a null value should perpetuate that null value to the db', async function() {
const post = new BlogPost({
title: null
});
assert.strictEqual(null, post.title);
- post.save(function(err) {
- assert.strictEqual(err, null);
- BlogPost.findById(post.id, function(err, found) {
- assert.strictEqual(err, null);
- assert.strictEqual(found.title, null);
- done();
- });
- });
+ await post.save();
+ const check = await BlogPost.findById(post.id);
+ assert.strictEqual(check.title, null);
});
- it('saves subdocuments middleware correctly', function(done) {
+ it('saves subdocuments middleware correctly', async function() {
let child_hook;
let parent_hook;
const childSchema = new Schema({
@@ -481,35 +431,25 @@ describe('Model', function() {
}]
});
- parent.save(function(err, parent) {
- assert.equal(parent_hook, 'Bob');
- assert.equal(child_hook, 'Mary');
- assert.ifError(err);
- parent.children[0].name = 'Jane';
- parent.save(function(err) {
- assert.equal(child_hook, 'Jane');
- assert.ifError(err);
- done();
- });
- });
+ const doc = await parent.save();
+ assert.equal(parent_hook, 'Bob');
+ assert.equal(child_hook, 'Mary');
+ doc.children[0].name = 'Jane';
+ await doc.save();
+ assert.equal(child_hook, 'Jane');
});
- it('instantiating a model with a hash that maps to at least 1 undefined value', function(done) {
+ it('instantiating a model with a hash that maps to at least 1 undefined value', async function() {
const post = new BlogPost({
title: undefined
});
assert.strictEqual(undefined, post.title);
- post.save(function(err) {
- assert.strictEqual(null, err);
- BlogPost.findById(post.id, function(err, found) {
- assert.strictEqual(err, null);
- assert.strictEqual(found.title, undefined);
- done();
- });
- });
+ await post.save();
+ const check = await BlogPost.findById(post.id);
+ assert.strictEqual(check.title, undefined);
});
- it('modified nested objects which contain MongoseNumbers should not cause a RangeError on save (gh-714)', function(done) {
+ it('modified nested objects which contain MongoseNumbers should not cause a RangeError on save (gh-714)', async function() {
const schema = new Schema({
nested: {
num: Number
@@ -519,21 +459,14 @@ describe('Model', function() {
const M = db.model('Test', schema);
const m = new M();
m.nested = null;
- m.save(function(err) {
- assert.ifError(err);
-
- M.findById(m, function(err, m) {
- assert.ifError(err);
- m.nested.num = 5;
- m.save(function(err) {
- assert.ifError(err);
- done();
- });
- });
- });
+ await m.save();
+ const check = await M.findById(m);
+ check.nested.num = 5;
+ const res = await check.save();
+ assert.ok(res);
});
- it('no RangeError on remove() of a doc with Number _id (gh-714)', function(done) {
+ it('no RangeError on deleteOne() of a doc with Number _id (gh-714)', async function() {
const MySchema = new Schema({
_id: { type: Number },
name: String
@@ -545,50 +478,36 @@ describe('Model', function() {
name: 'test',
_id: 35
});
- instance.save(function(err) {
- assert.ifError(err);
-
- MyModel.findById(35, function(err, doc) {
- assert.ifError(err);
- doc.remove({}, function(err) {
- assert.ifError(err);
- done();
- });
- });
- });
+ await instance.save();
+ const doc = await MyModel.findById(35);
+ assert.ok(doc);
+ await doc.deleteOne({});
+ assert.ok(doc);
});
- it('over-writing a number should persist to the db (gh-342)', function(done) {
+ it('over-writing a number should persist to the db (gh-342)', async function() {
const post = new BlogPost({
meta: {
date: new Date(),
visitors: 10
}
});
-
- post.save(function(err) {
- assert.ifError(err);
- post.set('meta.visitors', 20);
- post.save(function(err) {
- assert.ifError(err);
- BlogPost.findById(post.id, function(err, found) {
- assert.ifError(err);
- assert.equal(found.get('meta.visitors').valueOf(), 20);
- done();
- });
- });
- });
+ const doc = await post.save();
+ doc.set('meta.visitors', 20);
+ await doc.save();
+ const check = await BlogPost.findById(doc.id);
+ assert.equal(check.get('meta.visitors').valueOf(), 20);
});
describe('methods', function() {
- it('can be defined', function(done) {
+ it('can be defined', function() {
const post = new BlogPost();
assert.equal(post.cool(), post);
- done();
+
});
- it('can be defined on embedded documents', function(done) {
+ it('can be defined on embedded documents', function() {
const ChildSchema = new Schema({ name: String });
ChildSchema.method('talk', function() {
return 'gaga';
@@ -607,10 +526,10 @@ describe('Model', function() {
const p = new ParentA();
p.children.push({});
assert.equal(typeof p.children[0].talk, 'function');
- done();
+
});
- it('can be defined with nested key', function(done) {
+ it('can be defined with nested key', function() {
const NestedKeySchema = new Schema({});
NestedKeySchema.method('foo', {
bar: function() {
@@ -620,19 +539,19 @@ describe('Model', function() {
const NestedKey = db.model('Test', NestedKeySchema);
const n = new NestedKey();
assert.equal(n.foo.bar(), n);
- done();
+
});
});
describe('statics', function() {
- it('can be defined', function(done) {
+ it('can be defined', function() {
assert.equal(BlogPost.woot(), BlogPost);
- done();
+
});
});
describe('casting as validation errors', function() {
- it('error', function(done) {
+ it('error', async function() {
let threw = false;
let post;
@@ -652,19 +571,15 @@ describe('Model', function() {
assert.equal(threw, false);
- post.save(function(err) {
- assert.ok(err instanceof MongooseError);
- assert.ok(err instanceof ValidationError);
- assert.equal(Object.keys(err.errors).length, 2);
- post.date = new Date();
- post.meta.date = new Date();
- post.save(function(err) {
- assert.ifError(err);
- done();
- });
- });
+ const err = await post.save().then(() => null, err => err);
+ assert.ok(err instanceof MongooseError);
+ assert.ok(err instanceof ValidationError);
+ assert.equal(Object.keys(err.errors).length, 2);
+ post.date = new Date();
+ post.meta.date = new Date();
+ await post.save();
});
- it('nested error', function(done) {
+ it('nested error', async function() {
let threw = false;
const post = new BlogPost();
@@ -689,15 +604,13 @@ describe('Model', function() {
assert.equal(threw, false);
- post.save(function(err) {
- assert.ok(err instanceof MongooseError);
- assert.ok(err instanceof ValidationError);
- done();
- });
+ const err = await post.save().then(() => null, err => err);
+ assert.ok(err instanceof MongooseError);
+ assert.ok(err instanceof ValidationError);
});
- it('subdocument cast error', function(done) {
+ it('subdocument cast error', async function() {
const post = new BlogPost({
title: 'Test',
slug: 'test',
@@ -705,16 +618,13 @@ describe('Model', function() {
});
post.get('comments')[0].set('date', 'invalid');
-
- post.save(function(err) {
- assert.ok(err instanceof MongooseError);
- assert.ok(err instanceof ValidationError);
- done();
- });
+ const err = await post.save().then(() => null, err => err);
+ assert.ok(err instanceof MongooseError);
+ assert.ok(err instanceof ValidationError);
});
- it('subdocument validation error', function(done) {
+ it('subdocument validation error', async function() {
function failingvalidator() {
return false;
}
@@ -732,14 +642,12 @@ describe('Model', function() {
subs: [{ str: 'gaga' }]
});
- post.save(function(err) {
- assert.ok(err instanceof ValidationError);
- done();
- });
+ const err = await post.save().then(() => null, err => err);
+ assert.ok(err instanceof ValidationError);
});
- it('subdocument error when adding a subdoc', function(done) {
+ it('subdocument error when adding a subdoc', async function() {
let threw = false;
const post = new BlogPost();
@@ -754,82 +662,57 @@ describe('Model', function() {
assert.equal(threw, false);
- post.save(function(err) {
- assert.ok(err instanceof MongooseError);
- assert.ok(err instanceof ValidationError);
- done();
- });
+ const err = await post.save().then(() => null, err => err);
+ assert.ok(err instanceof MongooseError);
+ assert.ok(err instanceof ValidationError);
});
- it('updates', function(done) {
+ it('updates', async function() {
const post = new BlogPost();
post.set('title', '1');
const id = post.get('_id');
- post.save(function(err) {
- assert.ifError(err);
-
- BlogPost.updateOne({ title: 1, _id: id }, { title: 2 }, function(err) {
- assert.ifError(err);
-
- BlogPost.findOne({ _id: post.get('_id') }, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.get('title'), '2');
- done();
- });
- });
- });
+ const doc = await post.save();
+ await BlogPost.updateOne({ title: 1, _id: id }, { title: 2 });
+ const check = await BlogPost.findOne({ _id: doc.get('_id') });
+ assert.equal(check.get('title'), '2');
});
- it('$pull', function(done) {
+ it('$pull', function() {
const post = new BlogPost();
post.get('numbers').push('3');
assert.equal(post.get('numbers')[0], 3);
- done();
+
});
- it('$push', function(done) {
+ it('$push', async function() {
const post = new BlogPost();
post.get('numbers').push(1, 2, 3, 4);
- post.save(function() {
- BlogPost.findById(post.get('_id'), function(err, found) {
- assert.equal(found.get('numbers').length, 4);
- found.get('numbers').pull('3');
- found.save(function() {
- BlogPost.findById(found.get('_id'), function(err, found2) {
- assert.ifError(err);
- assert.equal(found2.get('numbers').length, 3);
- done();
- });
- });
- });
- });
+ const doc = await post.save();
+ let check = await BlogPost.findById(doc.get('_id'));
+ assert.equal(check.get('numbers').length, 4);
+ check.get('numbers').pull('3');
+ await check.save();
+ check = await BlogPost.findById(check.get('_id'));
+ assert.equal(check.get('numbers').length, 3);
});
- it('Number arrays', function(done) {
+ it('Number arrays', async function() {
const post = new BlogPost();
post.numbers.push(1, '2', 3);
- post.save(function(err) {
- assert.strictEqual(err, null);
-
- BlogPost.findById(post._id, function(err, doc) {
- assert.ifError(err);
-
- assert.ok(~doc.numbers.indexOf(1));
- assert.ok(~doc.numbers.indexOf(2));
- assert.ok(~doc.numbers.indexOf(3));
-
- done();
- });
- });
+ const doc = await post.save();
+ const check = await BlogPost.findById(doc._id);
+ assert.ok(~check.numbers.indexOf(1));
+ assert.ok(~check.numbers.indexOf(2));
+ assert.ok(~check.numbers.indexOf(3));
});
- it('date casting compat with datejs (gh-502)', function(done) {
+ it('date casting compat with datejs (gh-502)', async function() {
Date.prototype.toObject = function() {
return {
millisecond: 86,
@@ -860,25 +743,18 @@ describe('Model', function() {
const M = db.model('Test', S);
const m = new M();
- m.save(function(err) {
- assert.ifError(err);
- M.findById(m._id, function(err, m) {
- assert.ifError(err);
- m.save(function(err) {
- assert.ifError(err);
- M.deleteOne({}, function(err) {
- delete Date.prototype.toObject;
- assert.ifError(err);
- done();
- });
- });
- });
- });
+ const doc = await m.save();
+ assert.ok(doc);
+ const check = await M.findById(m._id);
+ await check.save();
+ assert.ok(check);
+ await M.deleteOne();
+ delete Date.prototype.toObject;
});
});
describe('validation', function() {
- it('works', function(done) {
+ it('works', async function() {
function dovalidate() {
assert.equal(this.asyncScope, 'correct');
return true;
@@ -886,7 +762,7 @@ describe('Model', function() {
function dovalidateAsync() {
assert.equal(this.scope, 'correct');
- return global.Promise.resolve(true);
+ return Promise.resolve(true);
}
const TestValidation = db.model('Test', new Schema({
@@ -900,19 +776,15 @@ describe('Model', function() {
post.set('scope', 'correct');
post.set('asyncScope', 'correct');
- post.save(function(err) {
- assert.ok(err instanceof MongooseError);
- assert.ok(err instanceof ValidationError);
-
- post.set('simple', 'here');
- post.save(function(err) {
- assert.ifError(err);
- done();
- });
- });
+ const err = await post.save().then(() => null, err => err);
+ assert.ok(err instanceof MongooseError);
+ assert.ok(err instanceof ValidationError);
+ post.set('simple', 'here');
+ const doc = await post.save();
+ assert.ok(doc);
});
- it('custom messaging', function(done) {
+ it('custom messaging', async function() {
function validate(val) {
return val === 'abc';
}
@@ -924,22 +796,18 @@ describe('Model', function() {
const post = new TestValidationMessage();
post.set('simple', '');
- post.save(function(err) {
- assert.ok(err instanceof MongooseError);
- assert.ok(err instanceof ValidationError);
- assert.ok(err.errors.simple instanceof ValidatorError);
- assert.equal(err.errors.simple.message, 'must be abc');
- assert.equal(post.errors.simple.message, 'must be abc');
-
- post.set('simple', 'abc');
- post.save(function(err) {
- assert.ifError(err);
- done();
- });
- });
+ const err = await post.save().then(() => null, err => err);
+ assert.ok(err instanceof MongooseError);
+ assert.ok(err instanceof ValidationError);
+ assert.ok(err.errors.simple instanceof ValidatorError);
+ assert.equal(err.errors.simple.message, 'must be abc');
+ assert.equal(post.errors.simple.message, 'must be abc');
+ post.set('simple', 'abc');
+ const doc = await post.save();
+ assert.ok(doc);
});
- it('with Model.schema.path introspection (gh-272)', function(done) {
+ it('with Model.schema.path introspection (gh-272)', async function() {
const IntrospectionValidationSchema = new Schema({
name: String
});
@@ -948,34 +816,27 @@ describe('Model', function() {
return value.length < 2;
}, 'Name cannot be greater than 1 character for path "{PATH}" with value `{VALUE}`');
const doc = new IntrospectionValidation({ name: 'hi' });
- doc.save(function(err) {
- assert.equal(err.errors.name.message, 'Name cannot be greater than 1 character for path "name" with value `hi`');
- assert.equal(err.name, 'ValidationError');
- assert.ok(err.message.indexOf('Test validation failed') !== -1, err.message);
- done();
- });
+ const err = await doc.save().then(() => null, err => err);
+ assert.equal(err.errors.name.message, 'Name cannot be greater than 1 character for path "name" with value `hi`');
+ assert.equal(err.name, 'ValidationError');
+ assert.ok(err.message.indexOf('Test validation failed') !== -1, err.message);
});
- it('of required undefined values', function(done) {
+ it('of required undefined values', async function() {
const TestUndefinedValidation = db.model('Test', new Schema({
simple: { type: String, required: true }
}));
const post = new TestUndefinedValidation();
-
- post.save(function(err) {
- assert.ok(err instanceof MongooseError);
- assert.ok(err instanceof ValidationError);
-
- post.set('simple', 'here');
- post.save(function(err) {
- assert.ifError(err);
- done();
- });
- });
+ const err = await post.save().then(() => null, err => err);
+ assert.ok(err instanceof MongooseError);
+ assert.ok(err instanceof ValidationError);
+ post.set('simple', 'here');
+ const doc = await post.save();
+ assert.ok(doc);
});
- it('save callback should only execute once (gh-319)', function(done) {
+ it('save callback should only execute once (gh-319)', async function() {
const D = db.model('Test', new Schema({
username: { type: String, validate: /^[a-z]{6}$/i },
email: { type: String, validate: /^[a-z]{6}$/i },
@@ -989,87 +850,67 @@ describe('Model', function() {
});
let timesCalled = 0;
+ const err = await post.save().then(() => null, err => err);
+ assert.ok(err instanceof MongooseError);
+ assert.ok(err instanceof ValidationError);
- post.save(function(err) {
- assert.ok(err instanceof MongooseError);
- assert.ok(err instanceof ValidationError);
-
- assert.equal(++timesCalled, 1);
+ assert.equal(++timesCalled, 1);
- assert.equal(Object.keys(err.errors).length, 3);
- assert.ok(err.errors.password instanceof ValidatorError);
- assert.ok(err.errors.email instanceof ValidatorError);
- assert.ok(err.errors.username instanceof ValidatorError);
- assert.equal(err.errors.password.message, 'Validator failed for path `password` with value `short`');
- assert.equal(err.errors.email.message, 'Validator failed for path `email` with value `too`');
- assert.equal(err.errors.username.message, 'Validator failed for path `username` with value `nope`');
+ assert.equal(Object.keys(err.errors).length, 3);
+ assert.ok(err.errors.password instanceof ValidatorError);
+ assert.ok(err.errors.email instanceof ValidatorError);
+ assert.ok(err.errors.username instanceof ValidatorError);
+ assert.equal(err.errors.password.message, 'Validator failed for path `password` with value `short`');
+ assert.equal(err.errors.email.message, 'Validator failed for path `email` with value `too`');
+ assert.equal(err.errors.username.message, 'Validator failed for path `username` with value `nope`');
- assert.equal(Object.keys(post.errors).length, 3);
- assert.ok(post.errors.password instanceof ValidatorError);
- assert.ok(post.errors.email instanceof ValidatorError);
- assert.ok(post.errors.username instanceof ValidatorError);
- assert.equal(post.errors.password.message, 'Validator failed for path `password` with value `short`');
- assert.equal(post.errors.email.message, 'Validator failed for path `email` with value `too`');
- assert.equal(post.errors.username.message, 'Validator failed for path `username` with value `nope`');
- done();
- });
+ assert.equal(Object.keys(post.errors).length, 3);
+ assert.ok(post.errors.password instanceof ValidatorError);
+ assert.ok(post.errors.email instanceof ValidatorError);
+ assert.ok(post.errors.username instanceof ValidatorError);
+ assert.equal(post.errors.password.message, 'Validator failed for path `password` with value `short`');
+ assert.equal(post.errors.email.message, 'Validator failed for path `email` with value `too`');
+ assert.equal(post.errors.username.message, 'Validator failed for path `username` with value `nope`');
});
- it('query result', function(done) {
+ it('query result', async function() {
const TestV = db.model('Test', new Schema({
resultv: { type: String, required: true }
}));
const post = new TestV();
- post.validate(function(err) {
- assert.ok(err instanceof MongooseError);
- assert.ok(err instanceof ValidationError);
-
- post.resultv = 'yeah';
- post.save(function(err) {
- assert.ifError(err);
- TestV.findOne({ _id: post.id }, function(err, found) {
- assert.ifError(err);
- assert.equal(found.resultv, 'yeah');
- found.save(function(err) {
- assert.ifError(err);
- done();
- });
- });
- });
- });
+ const err = await post.validate().then(() => null, err => err);
+ assert.ok(err instanceof MongooseError);
+ assert.ok(err instanceof ValidationError);
+ post.resultv = 'yeah';
+ const doc = await post.save();
+ const check = await TestV.findOne({ _id: doc._id });
+ assert.equal(check.resultv, 'yeah');
+ await check.save();
+ assert.ok(check);
});
- it('of required previously existing null values', function(done) {
+ it('of required previously existing null values', async function() {
const TestP = db.model('Test', new Schema({
previous: { type: String, required: true },
a: String
}));
const doc = { a: null, previous: null };
- TestP.collection.insertOne(doc, {}, function(err) {
- assert.ifError(err);
- TestP.findOne({ _id: doc._id }, function(err, found) {
- assert.ifError(err);
- assert.equal(found.isNew, false);
- assert.strictEqual(found.get('previous'), null);
-
- found.validate(function(err) {
- assert.ok(err instanceof MongooseError);
- assert.ok(err instanceof ValidationError);
-
- found.set('previous', 'yoyo');
- found.save(function(err) {
- assert.strictEqual(err, null);
- done();
- });
- });
- });
- });
+ await TestP.collection.insertOne(doc);
+ const check = await TestP.findOne({ _id: doc._id });
+ assert.equal(check.isNew, false);
+ assert.strictEqual(check.get('previous'), null);
+ const err = await check.validate().then(() => null, err => err);
+ assert.ok(err instanceof MongooseError);
+ assert.ok(err instanceof ValidationError);
+ check.set('previous', 'yoyo');
+ await check.save();
+ assert.ok(check);
});
- it('nested', function(done) {
+ it('nested', async function() {
const TestNestedValidation = db.model('Test', new Schema({
nested: {
required: { type: String, required: true }
@@ -1078,20 +919,15 @@ describe('Model', function() {
const post = new TestNestedValidation();
post.set('nested.required', null);
-
- post.save(function(err) {
- assert.ok(err instanceof MongooseError);
- assert.ok(err instanceof ValidationError);
-
- post.set('nested.required', 'here');
- post.save(function(err) {
- assert.ifError(err);
- done();
- });
- });
+ const err = await post.save().then(() => null, err => err);
+ assert.ok(err instanceof MongooseError);
+ assert.ok(err instanceof ValidationError);
+ post.set('nested.required', 'here');
+ const check = await post.save();
+ assert.ok(check);
});
- it('of nested subdocuments', function(done) {
+ it('of nested subdocuments', async function() {
const Subsubdocs = new Schema({ required: { type: String, required: true } });
const Subdocs = new Schema({
@@ -1107,42 +943,36 @@ describe('Model', function() {
post.get('items').push({ required: '', subs: [{ required: '' }] });
- post.save(function(err) {
- assert.ok(err instanceof MongooseError);
- assert.ok(err instanceof ValidationError);
- assert.ok(err.errors['items.0.subs.0.required'] instanceof ValidatorError);
- assert.equal(err.errors['items.0.subs.0.required'].message, 'Path `required` is required.');
- assert.ok(post.errors['items.0.subs.0.required'] instanceof ValidatorError);
- assert.equal(post.errors['items.0.subs.0.required'].message, 'Path `required` is required.');
-
- assert.ok(err.errors['items.0.required']);
- assert.ok(post.errors['items.0.required']);
-
- post.items[0].subs[0].set('required', true);
- assert.equal(post.$__.validationError, undefined);
-
- post.save(function(err) {
- assert.ok(err);
- assert.ok(err.errors);
- assert.ok(err.errors['items.0.required'] instanceof ValidatorError);
- assert.equal(err.errors['items.0.required'].message, 'Path `required` is required.');
-
- assert.ok(!err.errors['items.0.subs.0.required']);
- assert.ok(!err.errors['items.0.subs.0.required']);
- assert.ok(!post.errors['items.0.subs.0.required']);
- assert.ok(!post.errors['items.0.subs.0.required']);
-
- post.get('items')[0].set('required', true);
- post.save(function(err) {
- assert.ok(!post.errors);
- assert.ifError(err);
- done();
- });
- });
- });
+ let err = await post.save().then(() => null, err => err);
+ assert.ok(err instanceof MongooseError);
+ assert.ok(err instanceof ValidationError);
+ assert.ok(err.errors['items.0.subs.0.required'] instanceof ValidatorError);
+ assert.equal(err.errors['items.0.subs.0.required'].message, 'Path `required` is required.');
+ assert.ok(post.errors['items.0.subs.0.required'] instanceof ValidatorError);
+ assert.equal(post.errors['items.0.subs.0.required'].message, 'Path `required` is required.');
+
+ assert.ok(err.errors['items.0.required']);
+ assert.ok(post.errors['items.0.required']);
+
+ post.items[0].subs[0].set('required', true);
+ assert.equal(post.$__.validationError, undefined);
+
+ err = await post.save().then(() => null, err => err);
+ assert.ok(err);
+ assert.ok(err.errors);
+ assert.ok(err.errors['items.0.required'] instanceof ValidatorError);
+ assert.equal(err.errors['items.0.required'].message, 'Path `required` is required.');
+
+ assert.ok(!err.errors['items.0.subs.0.required']);
+ assert.ok(!err.errors['items.0.subs.0.required']);
+ assert.ok(!post.errors['items.0.subs.0.required']);
+ assert.ok(!post.errors['items.0.subs.0.required']);
+ post.get('items')[0].set('required', true);
+ await post.save();
+ assert.ok(!post.errors);
});
- it('without saving', function(done) {
+ it('without saving', async function() {
const TestCallingValidation = db.model('Test', new Schema({
item: { type: String, required: true }
}));
@@ -1151,22 +981,16 @@ describe('Model', function() {
assert.equal(post.schema.path('item').isRequired, true);
assert.strictEqual(post.isNew, true);
-
- post.validate(function(err) {
- assert.ok(err instanceof MongooseError);
- assert.ok(err instanceof ValidationError);
- assert.strictEqual(post.isNew, true);
-
- post.item = 'yo';
- post.validate(function(err) {
- assert.equal(err, null);
- assert.strictEqual(post.isNew, true);
- done();
- });
- });
+ const err = await post.validate().then(() => null, err => err);
+ assert.ok(err instanceof MongooseError);
+ assert.ok(err instanceof ValidationError);
+ assert.strictEqual(post.isNew, true);
+ post.item = 'yo';
+ await post.validate();
+ assert.strictEqual(post.isNew, true);
});
- it('when required is set to false', function(done) {
+ it('when required is set to false', function() {
function validator() {
return true;
}
@@ -1178,11 +1002,11 @@ describe('Model', function() {
const post = new TestV();
assert.equal(post.schema.path('result').isRequired, false);
- done();
+
});
describe('middleware', function() {
- it('works', function(done) {
+ it('works', async function() {
let ValidationMiddlewareSchema = null,
Post = null,
post = null;
@@ -1201,128 +1025,20 @@ describe('Model', function() {
Post = db.model('Test', ValidationMiddlewareSchema);
post = new Post();
post.set({ baz: 'bad' });
-
- post.save(function(err) {
- assert.ok(err instanceof MongooseError);
- assert.ok(err instanceof ValidationError);
- assert.equal(err.errors.baz.kind, 'user defined');
- assert.equal(err.errors.baz.path, 'baz');
-
- post.set('baz', 'good');
- post.save(function(err) {
- assert.ifError(err);
- done();
- });
- });
- });
-
- it('async', function(done) {
- let AsyncValidationMiddlewareSchema = null;
- let Post = null;
- let post = null;
-
- AsyncValidationMiddlewareSchema = new Schema({
- prop: { type: String }
- });
-
- AsyncValidationMiddlewareSchema.pre('validate', true, function(next, done) {
- const _this = this;
- setTimeout(function() {
- if (_this.get('prop') === 'bad') {
- _this.invalidate('prop', 'bad');
- }
- done();
- }, 5);
- next();
- });
-
- Post = db.model('Test', AsyncValidationMiddlewareSchema);
- post = new Post();
- post.set({ prop: 'bad' });
-
- post.save(function(err) {
- assert.ok(err instanceof MongooseError);
- assert.ok(err instanceof ValidationError);
- assert.equal(err.errors.prop.kind, 'user defined');
- assert.equal(err.errors.prop.path, 'prop');
-
- post.set('prop', 'good');
- post.save(function(err) {
- assert.ifError(err);
- done();
- });
- });
- });
-
- it('complex', function(done) {
- let ComplexValidationMiddlewareSchema = null;
- let Post = null;
- let post = null;
- const abc = v => v === 'abc';
-
- ComplexValidationMiddlewareSchema = new Schema({
- baz: { type: String },
- abc: { type: String, validate: [abc, 'must be abc'] },
- test: { type: String, validate: [/test/, 'must also be abc'] },
- required: { type: String, required: true }
- });
-
- ComplexValidationMiddlewareSchema.pre('validate', true, function(next, done) {
- const _this = this;
- setTimeout(function() {
- if (_this.get('baz') === 'bad') {
- _this.invalidate('baz', 'bad');
- }
- done();
- }, 5);
- next();
- });
-
- Post = db.model('Test', ComplexValidationMiddlewareSchema);
- post = new Post();
- post.set({
- baz: 'bad',
- abc: 'not abc',
- test: 'fail'
- });
-
- post.save(function(err) {
- assert.ok(err instanceof MongooseError);
- assert.ok(err instanceof ValidationError);
- assert.equal(Object.keys(err.errors).length, 4);
- assert.ok(err.errors.baz instanceof ValidatorError);
- assert.equal(err.errors.baz.kind, 'user defined');
- assert.equal(err.errors.baz.path, 'baz');
- assert.ok(err.errors.abc instanceof ValidatorError);
- assert.equal(err.errors.abc.kind, 'user defined');
- assert.equal(err.errors.abc.message, 'must be abc');
- assert.equal(err.errors.abc.path, 'abc');
- assert.ok(err.errors.test instanceof ValidatorError);
- assert.equal(err.errors.test.message, 'must also be abc');
- assert.equal(err.errors.test.kind, 'user defined');
- assert.equal(err.errors.test.path, 'test');
- assert.ok(err.errors.required instanceof ValidatorError);
- assert.equal(err.errors.required.kind, 'required');
- assert.equal(err.errors.required.path, 'required');
-
- post.set({
- baz: 'good',
- abc: 'abc',
- test: 'test',
- required: 'here'
- });
-
- post.save(function(err) {
- assert.ifError(err);
- done();
- });
- });
+ const err = await post.save().then(() => null, err => err);
+ assert.ok(err instanceof MongooseError);
+ assert.ok(err instanceof ValidationError);
+ assert.equal(err.errors.baz.kind, 'user defined');
+ assert.equal(err.errors.baz.path, 'baz');
+ post.set('baz', 'good');
+ const doc = await post.save();
+ assert.ok(doc);
});
});
});
describe('defaults application', function() {
- it('works', function(done) {
+ it('works', function() {
const now = Date.now();
const TestDefaults = db.model('Test', new Schema({
@@ -1332,10 +1048,10 @@ describe('Model', function() {
const post = new TestDefaults();
assert.ok(post.get('date') instanceof Date);
assert.equal(+post.get('date'), now);
- done();
+
});
- it('nested', function(done) {
+ it('nested', function() {
const now = Date.now();
const TestDefaults = db.model('Test', new Schema({
@@ -1347,10 +1063,10 @@ describe('Model', function() {
const post = new TestDefaults();
assert.ok(post.get('nested.date') instanceof Date);
assert.equal(+post.get('nested.date'), now);
- done();
+
});
- it('subdocument', function(done) {
+ it('subdocument', function() {
const now = Date.now();
const Items = new Schema({
@@ -1365,29 +1081,22 @@ describe('Model', function() {
post.get('items').push({});
assert.ok(post.get('items')[0].get('date') instanceof Date);
assert.equal(+post.get('items')[0].get('date'), now);
- done();
+
});
- it('allows nulls', function(done) {
+ it('allows nulls', async function() {
const T = db.model('Test', new Schema({ name: { type: String, default: null } }));
const t = new T();
assert.strictEqual(null, t.name);
-
- t.save(function(err) {
- assert.ifError(err);
-
- T.findById(t._id, function(err, t) {
- assert.ifError(err);
- assert.strictEqual(null, t.name);
- done();
- });
- });
+ await t.save();
+ const check = await T.findById(t._id);
+ assert.equal(null, check.name);
});
});
describe('virtuals', function() {
- it('getters', function(done) {
+ it('getters', function() {
const post = new BlogPost({
title: 'Letters from Earth',
author: 'Mark Twain'
@@ -1395,38 +1104,30 @@ describe('Model', function() {
assert.equal(post.get('titleWithAuthor'), 'Letters from Earth by Mark Twain');
assert.equal(post.titleWithAuthor, 'Letters from Earth by Mark Twain');
- done();
+
});
- it('set()', function(done) {
+ it('set()', function() {
const post = new BlogPost();
post.set('titleWithAuthor', 'Huckleberry Finn by Mark Twain');
assert.equal(post.get('title'), 'Huckleberry Finn');
assert.equal(post.get('author'), 'Mark Twain');
- done();
+
});
- it('should not be saved to the db', function(done) {
+ it('should not be saved to the db', async function() {
const post = new BlogPost();
post.set('titleWithAuthor', 'Huckleberry Finn by Mark Twain');
-
- post.save(function(err) {
- assert.ifError(err);
-
- BlogPost.findById(post.get('_id'), function(err, found) {
- assert.ifError(err);
-
- assert.equal(found.get('title'), 'Huckleberry Finn');
- assert.equal(found.get('author'), 'Mark Twain');
- assert.ok(!('titleWithAuthor' in found.toObject()));
- done();
- });
- });
+ await post.save();
+ const check = await BlogPost.findById(post.get('_id'));
+ assert.equal(check.get('title'), 'Huckleberry Finn');
+ assert.equal(check.get('author'), 'Mark Twain');
+ assert.ok(!('titleWithAuthor' in check.toObject()));
});
- it('nested', function(done) {
+ it('nested', function() {
const PersonSchema = new Schema({
name: {
first: String,
@@ -1462,198 +1163,49 @@ describe('Model', function() {
person.name.full = 'Michael Sorrentino';
assert.equal(person.name.first, 'Michael');
assert.equal(person.name.last, 'Sorrentino');
- done();
+
});
});
- describe('.remove()', function() {
- it('works', function(done) {
- BlogPost.create({ title: 1 }, { title: 2 }, function(err) {
- assert.ifError(err);
-
- BlogPost.remove({ title: 1 }, function(err) {
- assert.ifError(err);
-
- BlogPost.find({}, function(err, found) {
- assert.ifError(err);
- assert.equal(found.length, 1);
- assert.equal(found[0].title, '2');
- done();
- });
- });
- });
+ describe('.deleteOne()', function() {
+ it('works', async function() {
+ await BlogPost.create({ title: 1 }, { title: 2 });
+ await BlogPost.deleteOne({ title: 1 });
+ const found = await BlogPost.find({});
+ assert.equal(found.length, 1);
+ assert.equal(found[0].title, '2');
});
- it('errors when id deselected (gh-3118)', function(done) {
- BlogPost.create({ title: 1 }, { title: 2 }, function(err) {
- assert.ifError(err);
- BlogPost.findOne({ title: 1 }, { _id: 0 }, function(error, doc) {
- assert.ifError(error);
- doc.remove(function(err) {
- assert.ok(err);
- assert.equal(err.message, 'No _id found on document!');
- done();
- });
- });
- });
+ it('errors when id deselected (gh-3118)', async function() {
+ await BlogPost.create({ title: 1 }, { title: 2 });
+ const doc = await BlogPost.findOne({ title: 1 }, { _id: 0 });
+ try {
+ await doc.deleteOne();
+ assert.ok(false);
+ } catch (err) {
+ assert.equal(err.message, 'No _id found on document!');
+ }
});
- it('should not remove any records when deleting by id undefined', function(done) {
- BlogPost.create({ title: 1 }, { title: 2 }, function(err) {
- assert.ifError(err);
-
- BlogPost.remove({ _id: undefined }, function(err) {
- assert.ifError(err);
- BlogPost.find({}, function(err, found) {
- assert.equal(found.length, 2, 'Should not remove any records');
- done();
- });
- });
- });
+ it('should not remove any records when deleting by id undefined', async function() {
+ await BlogPost.create({ title: 1 }, { title: 2 });
+ await BlogPost.deleteOne({ _id: undefined });
+ const found = await BlogPost.find({});
+ assert.equal(found.length, 2, 'Should not remove any records');
});
- it('should not remove all documents in the collection (gh-3326)', function(done) {
- BlogPost.create({ title: 1 }, { title: 2 }, function(err) {
- assert.ifError(err);
- BlogPost.findOne({ title: 1 }, function(error, doc) {
- assert.ifError(error);
- doc.remove(function(err) {
- assert.ifError(err);
- BlogPost.find(function(err, found) {
- assert.ifError(err);
- assert.equal(found.length, 1);
- assert.equal(found[0].title, '2');
- done();
- });
- });
- });
- });
- });
- });
-
- describe('#remove()', function() {
- it('passes the removed document (gh-1419)', function(done) {
- BlogPost.create({}, function(err, post) {
- assert.ifError(err);
- BlogPost.findById(post, function(err, found) {
- assert.ifError(err);
-
- found.remove(function(err, doc) {
- assert.ifError(err);
- assert.ok(doc);
- assert.ok(doc.equals(found));
- done();
- });
- });
- });
- });
-
- it('works as a promise', function(done) {
- BlogPost.create({}, function(err, post) {
- assert.ifError(err);
- BlogPost.findById(post, function(err, found) {
- assert.ifError(err);
-
- found.remove().then(function(doc) {
- assert.ok(doc);
- assert.ok(doc.equals(found));
- done();
- }).catch(done);
- });
- });
- });
-
- it('works as a promise with a hook', function(done) {
- let called = 0;
- const RHS = new Schema({
- name: String
- });
- RHS.pre('remove', function(next) {
- called++;
- return next();
- });
-
- const RH = db.model('Test', RHS);
-
- RH.create({ name: 'to be removed' }, function(err, post) {
- assert.ifError(err);
- assert.ok(post);
- RH.findById(post, function(err, found) {
- assert.ifError(err);
- assert.ok(found);
-
- found.remove().then(function(doc) {
- assert.ifError(err);
- assert.equal(called, 1);
- assert.ok(doc);
- assert.ok(doc.equals(found));
- done();
- }).catch(done);
- });
- });
- });
-
- it('handles query vs document middleware (gh-3054)', async function() {
- const schema = new Schema({ name: String });
-
- let docMiddleware = 0;
- let queryMiddleware = 0;
-
- schema.pre('remove', { query: true, document: false }, function() {
- ++queryMiddleware;
- assert.ok(this instanceof Model.Query);
- });
-
- schema.pre('remove', { query: false, document: true }, function() {
- ++docMiddleware;
- assert.ok(this instanceof Model);
- });
-
- const Model = db.model('Test', schema);
-
-
- const doc = await Model.create({ name: String });
-
- assert.equal(docMiddleware, 0);
- assert.equal(queryMiddleware, 0);
- await doc.remove();
-
- assert.equal(docMiddleware, 1);
- assert.equal(queryMiddleware, 0);
-
- await Model.remove({});
- assert.equal(docMiddleware, 1);
- assert.equal(queryMiddleware, 1);
- });
-
- describe('when called multiple times', function() {
- it('always executes the passed callback gh-1210', function(done) {
- const post = new BlogPost();
-
- post.save(function(err) {
- assert.ifError(err);
-
- let pending = 2;
-
- post.remove(function() {
- if (--pending) {
- return;
- }
- done();
- });
- post.remove(function() {
- if (--pending) {
- return;
- }
- done();
- });
- });
- });
+ it('should not remove all documents in the collection (gh-3326)', async function() {
+ await BlogPost.create({ title: 1 }, { title: 2 });
+ const doc = await BlogPost.findOne({ title: 1 });
+ await doc.deleteOne();
+ const found = await BlogPost.find();
+ assert.equal(found.length, 1);
+ assert.equal(found[0].title, '2');
});
});
describe('getters', function() {
- it('with same name on embedded docs do not class', function(done) {
+ it('with same name on embedded docs do not class', function() {
const Post = new Schema({
title: String,
author: { name: String },
@@ -1672,10 +1224,10 @@ describe('Model', function() {
assert.equal(post.author.name, 'A');
assert.equal(post.subject.name, 'B');
assert.equal(post.author.name, 'A');
- done();
+
});
- it('should not be triggered at construction (gh-685)', function(done) {
+ it('should not be triggered at construction (gh-685)', function() {
let called = false;
const schema = new mongoose.Schema({
@@ -1708,10 +1260,10 @@ describe('Model', function() {
assert.equal(called, true);
assert.equal(num.valueOf(), 100);
assert.equal(b.$__getValue('number').valueOf(), 50);
- done();
+
});
- it('with type defined with { type: Native } (gh-190)', function(done) {
+ it('with type defined with { type: Native } (gh-190)', function() {
const schema = new Schema({ date: { type: Date } });
const ShortcutGetter = db.model('Test', schema);
@@ -1719,11 +1271,11 @@ describe('Model', function() {
post.set('date', Date.now());
assert.ok(post.date instanceof Date);
- done();
+
});
describe('nested', function() {
- it('works', function(done) {
+ it('works', function() {
const schema = new Schema({
first: {
second: [Number]
@@ -1734,10 +1286,10 @@ describe('Model', function() {
assert.equal(typeof doc.first, 'object');
assert.ok(doc.first.second.isMongooseArray);
- done();
+
});
- it('works with object literals', function(done) {
+ it('works with object literals', function() {
const date = new Date();
const meta = {
@@ -1809,10 +1361,10 @@ describe('Model', function() {
assert.equal((+post.get('meta').date), date - 3000);
assert.equal((+post.meta.visitors), 4815162342);
assert.equal((+post.get('meta').visitors), 4815162342);
- done();
+
});
- it('object property access works when root initd with null', function(done) {
+ it('object property access works when root initd with null', async function() {
const schema = new Schema({
nest: {
st: String
@@ -1827,14 +1379,11 @@ describe('Model', function() {
t.nest = { st: 'jsconf rules' };
assert.deepEqual(t.nest.toObject(), { st: 'jsconf rules' });
assert.equal(t.nest.st, 'jsconf rules');
-
- t.save(function(err) {
- assert.ifError(err);
- done();
- });
+ const doc = await t.save();
+ assert.ok(doc);
});
- it('object property access works when root initd with undefined', function(done) {
+ it('object property access works when root initd with undefined', async function() {
const schema = new Schema({
nest: {
st: String
@@ -1849,14 +1398,11 @@ describe('Model', function() {
t.nest = { st: 'jsconf rules' };
assert.deepEqual(t.nest.toObject(), { st: 'jsconf rules' });
assert.equal(t.nest.st, 'jsconf rules');
-
- t.save(function(err) {
- assert.ifError(err);
- done();
- });
+ const doc = await t.save();
+ assert.ok(doc);
});
- it('pre-existing null object re-save', function(done) {
+ it('pre-existing null object re-save', async function() {
const schema = new Schema({
nest: {
st: String,
@@ -1868,39 +1414,26 @@ describe('Model', function() {
const t = new T({ nest: null });
- t.save(function(err) {
- assert.ifError(err);
-
- t.nest = { st: 'jsconf rules', yep: 'it does' };
-
- // check that entire `nest` object is being $set
- const u = t.$__delta()[1];
- assert.ok(u.$set);
- assert.ok(u.$set.nest);
- assert.equal(Object.keys(u.$set.nest).length, 2);
- assert.ok(u.$set.nest.yep);
- assert.ok(u.$set.nest.st);
-
- t.save(function(err) {
- assert.ifError(err);
-
- T.findById(t.id, function(err, t) {
- assert.ifError(err);
- assert.equal(t.nest.st, 'jsconf rules');
- assert.equal(t.nest.yep, 'it does');
-
- t.nest = null;
- t.save(function(err) {
- assert.ifError(err);
- assert.strictEqual(t._doc.nest, null);
- done();
- });
- });
- });
- });
- });
-
- it('array of Mixed on existing doc can be pushed to', function(done) {
+ await t.save();
+ t.nest = { st: 'jsconf rules', yep: 'it does' };
+ // check that entire `nest` object is being $set
+ const u = t.$__delta()[1];
+ assert.ok(u.$set);
+ assert.ok(u.$set.nest);
+ assert.equal(Object.keys(u.$set.nest).length, 2);
+ assert.ok(u.$set.nest.yep);
+ assert.ok(u.$set.nest.st);
+ await t.save();
+ const res = await T.findById(t.id);
+ assert.equal(res.nest.st, 'jsconf rules');
+ assert.equal(res.nest.yep, 'it does');
+
+ res.nest = null;
+ const check = await res.save();
+ assert.strictEqual(check._doc.nest, null);
+ });
+
+ it('array of Mixed on existing doc can be pushed to', async function() {
const DooDad = db.model('Test', new Schema({
nested: {
arrays: []
@@ -1910,31 +1443,16 @@ describe('Model', function() {
const date = 1234567890;
doodad.nested.arrays.push(['+10', 'yup', date]);
-
- doodad.save(function(err) {
- assert.ifError(err);
-
- DooDad.findById(doodad._id, function(err, doodad) {
- assert.ifError(err);
-
- assert.deepEqual(doodad.nested.arrays.toObject(), [['+10', 'yup', date]]);
-
- doodad.nested.arrays.push(['another', 1]);
-
- doodad.save(function(err) {
- assert.ifError(err);
-
- DooDad.findById(doodad._id, function(err, doodad) {
- assert.ifError(err);
- assert.deepEqual(doodad.nested.arrays.toObject(), [['+10', 'yup', date], ['another', 1]]);
- done();
- });
- });
- });
- });
+ await doodad.save();
+ const res = await DooDad.findById(doodad._id);
+ assert.deepEqual(res.nested.arrays.toObject(), [['+10', 'yup', date]]);
+ res.nested.arrays.push(['another', 1]);
+ await res.save();
+ const check = await DooDad.findById(res._id);
+ assert.deepEqual(check.nested.arrays.toObject(), [['+10', 'yup', date], ['another', 1]]);
});
- it('props can be set directly when property was named "type"', function(done) {
+ it('props can be set directly when property was named "type"', async function() {
function def() {
return [{ x: 1 }, { x: 2 }, { x: 3 }];
}
@@ -1948,37 +1466,22 @@ describe('Model', function() {
}
}));
const doodad = new DooDad();
-
- doodad.save(function(err) {
- assert.ifError(err);
-
- DooDad.findById(doodad._id, function(err, doodad) {
- assert.ifError(err);
-
- assert.equal(doodad.nested.type, 'yep');
- assert.deepEqual(doodad.nested.array.toObject(), [{ x: 1 }, { x: 2 }, { x: 3 }]);
-
- doodad.nested.type = 'nope';
- doodad.nested.array = ['some', 'new', 'stuff'];
-
- doodad.save(function(err) {
- assert.ifError(err);
-
- DooDad.findById(doodad._id, function(err, doodad) {
- assert.ifError(err);
- assert.equal(doodad.nested.type, 'nope');
- assert.deepEqual(doodad.nested.array.toObject(), ['some', 'new', 'stuff']);
- done();
- });
- });
- });
- });
+ const doc = await doodad.save();
+ const res = await DooDad.findById(doc._id);
+ assert.equal(res.nested.type, 'yep');
+ assert.deepEqual(res.nested.array.toObject(), [{ x: 1 }, { x: 2 }, { x: 3 }]);
+ res.nested.type = 'nope';
+ res.nested.array = ['some', 'new', 'stuff'];
+ const newDoc = await res.save();
+ const check = await DooDad.findById(newDoc._id);
+ assert.equal(check.nested.type, 'nope');
+ assert.deepEqual(check.nested.array.toObject(), ['some', 'new', 'stuff']);
});
});
});
describe('setters', function() {
- it('are used on embedded docs (gh-365 gh-390 gh-422)', function(done) {
+ it('are used on embedded docs (gh-365 gh-390 gh-422)', async function() {
function setLat(val) {
return parseInt(val, 10);
}
@@ -2009,164 +1512,90 @@ describe('Model', function() {
const deal = new Deal({ title: 'My deal', locations: [{ lat: 1.2, long: 33 }] });
assert.equal(deal.locations[0].lat.valueOf(), 1);
assert.equal(deal.locations[0].long.valueOf(), 2);
-
- deal.save(function(err) {
- assert.ifError(err);
- Deal.findById(deal._id, function(err, deal) {
- assert.ifError(err);
- assert.equal(deal.locations[0].lat.valueOf(), 1);
- // GH-422
- assert.equal(deal.locations[0].long.valueOf(), 2);
- done();
- });
- });
+ const doc = await deal.save();
+ const check = await Deal.findById(doc._id);
+ assert.equal(check.locations[0].lat.valueOf(), 1);
+ // GH-422
+ assert.equal(check.locations[0].long.valueOf(), 2);
});
});
- it('changing a number non-atomically (gh-203)', function(done) {
+ it('changing a number non-atomically (gh-203)', async function() {
const post = new BlogPost();
post.meta.visitors = 5;
- post.save(function(err) {
- assert.ifError(err);
-
- BlogPost.findById(post._id, function(err, doc) {
- assert.ifError(err);
-
- doc.meta.visitors -= 2;
-
- doc.save(function(err) {
- assert.ifError(err);
-
- BlogPost.findById(post._id, function(err, doc) {
- assert.ifError(err);
- assert.equal(+doc.meta.visitors, 3);
- done();
- });
- });
- });
- });
+ const doc = await post.save();
+ const res = await BlogPost.findById(doc._id);
+ res.meta.visitors -= 2;
+ const newDoc = await res.save();
+ const check = await BlogPost.findById(newDoc._id);
+ assert.equal(+check.meta.visitors, 3);
});
describe('atomic subdocument', function() {
- it('saving', function(done) {
- let totalDocs = 4;
- const saveQueue = [];
-
+ it('saving', async function() {
const post = new BlogPost();
- function complete() {
- BlogPost.findOne({ _id: post.get('_id') }, function(err, doc) {
-
- assert.ifError(err);
- assert.equal(doc.get('comments').length, 5);
-
- let v = doc.get('comments').some(function(comment) {
- return comment.get('title') === '1';
- });
-
- assert.ok(v);
-
- v = doc.get('comments').some(function(comment) {
- return comment.get('title') === '2';
- });
-
- assert.ok(v);
-
- v = doc.get('comments').some(function(comment) {
- return comment.get('title') === '3';
- });
-
- assert.ok(v);
+ await post.save();
+ await Promise.all(
+ Array(5).fill(null).map(async(_, i) => {
+ const doc = await BlogPost.findOne({ _id: post.get('_id') });
+ doc.get('comments').push({ title: '' + (i + 1) });
+ await doc.save();
+ })
+ );
- v = doc.get('comments').some(function(comment) {
- return comment.get('title') === '4';
- });
+ const doc = await BlogPost.findOne({ _id: post.get('_id') });
+ assert.equal(doc.get('comments').length, 5);
- assert.ok(v);
+ let v = doc.get('comments').some(function(comment) {
+ return comment.get('title') === '1';
+ });
- v = doc.get('comments').some(function(comment) {
- return comment.get('title') === '5';
- });
+ assert.ok(v);
- assert.ok(v);
- done();
- });
- }
+ v = doc.get('comments').some(function(comment) {
+ return comment.get('title') === '2';
+ });
- function save(doc) {
- saveQueue.push(doc);
- if (saveQueue.length === 4) {
- saveQueue.forEach(function(doc) {
- doc.save(function(err) {
- assert.ifError(err);
- --totalDocs || complete();
- });
- });
- }
- }
+ assert.ok(v);
- post.save(function(err) {
- assert.ifError(err);
+ v = doc.get('comments').some(function(comment) {
+ return comment.get('title') === '3';
+ });
- BlogPost.findOne({ _id: post.get('_id') }, function(err, doc) {
- assert.ifError(err);
- doc.get('comments').push({ title: '1' });
- save(doc);
- });
+ assert.ok(v);
- BlogPost.findOne({ _id: post.get('_id') }, function(err, doc) {
- assert.ifError(err);
- doc.get('comments').push({ title: '2' });
- save(doc);
- });
+ v = doc.get('comments').some(function(comment) {
+ return comment.get('title') === '4';
+ });
- BlogPost.findOne({ _id: post.get('_id') }, function(err, doc) {
- assert.ifError(err);
- doc.get('comments').push({ title: '3' });
- save(doc);
- });
+ assert.ok(v);
- BlogPost.findOne({ _id: post.get('_id') }, function(err, doc) {
- assert.ifError(err);
- doc.get('comments').push({ title: '4' }, { title: '5' });
- save(doc);
- });
+ v = doc.get('comments').some(function(comment) {
+ return comment.get('title') === '5';
});
+
+ assert.ok(v);
});
- it('setting (gh-310)', function(done) {
- BlogPost.create({
- comments: [{ title: 'first-title', body: 'first-body' }]
- }, function(err, blog) {
- assert.ifError(err);
- BlogPost.findById(blog.id, function(err, agent1blog) {
- assert.ifError(err);
- BlogPost.findById(blog.id, function(err, agent2blog) {
- assert.ifError(err);
- agent1blog.get('comments')[0].title = 'second-title';
- agent1blog.save(function(err) {
- assert.ifError(err);
- agent2blog.get('comments')[0].body = 'second-body';
- agent2blog.save(function(err) {
- assert.ifError(err);
- BlogPost.findById(blog.id, function(err, foundBlog) {
- assert.ifError(err);
- const comment = foundBlog.get('comments')[0];
- assert.equal(comment.title, 'second-title');
- assert.equal(comment.body, 'second-body');
- done();
- });
- });
- });
- });
- });
- });
+ it('setting (gh-310)', async function() {
+ const blog = await BlogPost.create({ comments: [{ title: 'first-title', body: 'first-body' }] });
+ const agent1blog = await BlogPost.findById(blog.id);
+ const agent2blog = await BlogPost.findById(blog.id);
+ agent1blog.get('comments')[0].title = 'second-title';
+ await agent1blog.save();
+ agent2blog.get('comments')[0].body = 'second-body';
+ await agent2blog.save();
+ const res = await BlogPost.findById(blog.id);
+ const comment = res.get('comments')[0];
+ assert.equal(comment.title, 'second-title');
+ assert.equal(comment.body, 'second-body');
});
});
- it('doubly nested array saving and loading', function(done) {
+ it('doubly nested array saving and loading', async function() {
const Inner = new Schema({
arr: [Number]
});
@@ -2178,30 +1607,20 @@ describe('Model', function() {
const outer = new Outer();
outer.inner.push({});
- outer.save(function(err) {
- assert.ifError(err);
- assert.ok(outer.get('_id') instanceof DocumentObjectId);
-
- Outer.findById(outer.get('_id'), function(err, found) {
- assert.ifError(err);
- assert.equal(found.inner.length, 1);
- found.inner[0].arr.push(5);
- found.save(function(err) {
- assert.ifError(err);
- assert.ok(found.get('_id') instanceof DocumentObjectId);
- Outer.findById(found.get('_id'), function(err, found2) {
- assert.ifError(err);
- assert.equal(found2.inner.length, 1);
- assert.equal(found2.inner[0].arr.length, 1);
- assert.equal(found2.inner[0].arr[0], 5);
- done();
- });
- });
- });
- });
+ await outer.save();
+ assert.ok(outer.get('_id') instanceof DocumentObjectId);
+ const doc = await Outer.findById(outer.get('_id'));
+ assert.equal(doc.inner.length, 1);
+ doc.inner[0].arr.push(5);
+ await doc.save();
+ assert.ok(doc.get('_id') instanceof DocumentObjectId);
+ const res = await Outer.findById(doc.get('_id'));
+ assert.equal(res.inner.length, 1);
+ assert.equal(res.inner[0].arr.length, 1);
+ assert.equal(res.inner[0].arr[0], 5);
});
- it('multiple number push() calls', function(done) {
+ it('multiple number push() calls', async function() {
const schema = new Schema({
nested: {
nums: [Number]
@@ -2210,26 +1629,22 @@ describe('Model', function() {
const Temp = db.model('Test', schema);
- Temp.create({}, function(err, t) {
- assert.ifError(err);
- t.nested.nums.push(1);
- t.nested.nums.push(2);
+ const t = await Temp.create({});
- assert.equal(t.nested.nums.length, 2);
+ t.nested.nums.push(1);
+ t.nested.nums.push(2);
- t.save(function(err) {
- assert.ifError(err);
- assert.equal(t.nested.nums.length, 2);
- Temp.findById(t._id, function(err) {
- assert.ifError(err);
- assert.equal(t.nested.nums.length, 2);
- done();
- });
- });
- });
+ assert.equal(t.nested.nums.length, 2);
+
+ await t.save();
+
+ assert.equal(t.nested.nums.length, 2);
+
+ const check = await Temp.findById(t._id);
+ assert.equal(check.nested.nums.length, 2);
});
- it('multiple push() calls', function(done) {
+ it('multiple push() calls', async function() {
const schema = new Schema({
nested: {
nums: [Number]
@@ -2237,26 +1652,18 @@ describe('Model', function() {
});
const Temp = db.model('Test', schema);
+ const t = await Temp.create({});
- Temp.create({}, function(err, t) {
- assert.ifError(err);
- t.nested.nums.push(1);
- t.nested.nums.push(2, 3);
- assert.equal(t.nested.nums.length, 3);
-
- t.save(function(err) {
- assert.ifError(err);
- assert.equal(t.nested.nums.length, 3);
- Temp.findById(t._id, function(err, found) {
- assert.ifError(err);
- assert.equal(found.nested.nums.length, 3);
- done();
- });
- });
- });
+ t.nested.nums.push(1);
+ t.nested.nums.push(2, 3);
+ assert.equal(t.nested.nums.length, 3);
+ await t.save();
+ assert.equal(t.nested.nums.length, 3);
+ const check = await Temp.findById(t._id);
+ assert.equal(check.nested.nums.length, 3);
});
- it('activePaths should be updated for nested modifieds', function(done) {
+ it('activePaths should be updated for nested modifieds', async function() {
const schema = new Schema({
nested: {
nums: [Number]
@@ -2264,18 +1671,14 @@ describe('Model', function() {
});
const Temp = db.model('Test', schema);
-
- Temp.create({ nested: { nums: [1, 2, 3, 4, 5] } }, function(err, t) {
- assert.ifError(err);
- t.nested.nums.pull(1);
- t.nested.nums.pull(2);
- assert.equal(t.$__.activePaths.paths['nested.nums'], 'modify');
- done();
- });
+ const t = await Temp.create({ nested: { nums: [1, 2, 3, 4, 5] } });
+ t.nested.nums.pull(1);
+ t.nested.nums.pull(2);
+ assert.equal(t.$__.activePaths.paths['nested.nums'], 'modify');
});
- it('activePaths should be updated for nested modifieds as promise', function(done) {
+ it('activePaths should be updated for nested modifieds as promise', async function() {
const schema = new Schema({
nested: {
nums: [Number]
@@ -2284,16 +1687,13 @@ describe('Model', function() {
const Temp = db.model('Test', schema);
- const p1 = Temp.create({ nested: { nums: [1, 2, 3, 4, 5] } });
- p1.then(function(t) {
- t.nested.nums.pull(1);
- t.nested.nums.pull(2);
- assert.equal(t.$__.activePaths.paths['nested.nums'], 'modify');
- done();
- }).catch(done);
+ const t = await Temp.create({ nested: { nums: [1, 2, 3, 4, 5] } });
+ t.nested.nums.pull(1);
+ t.nested.nums.pull(2);
+ assert.equal(t.$__.activePaths.paths['nested.nums'], 'modify');
});
- it('$pull should affect what you see in an array before a save', function(done) {
+ it('$pull should affect what you see in an array before a save', async function() {
const schema = new Schema({
nested: {
nums: [Number]
@@ -2301,16 +1701,12 @@ describe('Model', function() {
});
const Temp = db.model('Test', schema);
-
- Temp.create({ nested: { nums: [1, 2, 3, 4, 5] } }, function(err, t) {
- assert.ifError(err);
- t.nested.nums.pull(1);
- assert.equal(t.nested.nums.length, 4);
- done();
- });
+ const t = await Temp.create({ nested: { nums: [1, 2, 3, 4, 5] } });
+ t.nested.nums.pull(1);
+ assert.equal(t.nested.nums.length, 4);
});
- it('$shift', function(done) {
+ it('$shift', async function() {
const schema = new Schema({
nested: {
nums: [Number]
@@ -2319,318 +1715,185 @@ describe('Model', function() {
const Temp = db.model('Test', schema);
- Temp.create({ nested: { nums: [1, 2, 3] } }, function(err, t) {
- assert.ifError(err);
-
- Temp.findById(t._id, function(err, found) {
- assert.ifError(err);
- assert.equal(found.nested.nums.length, 3);
- found.nested.nums.$pop();
- assert.equal(found.nested.nums.length, 2);
- assert.equal(found.nested.nums[0], 1);
- assert.equal(found.nested.nums[1], 2);
-
- found.save(function(err) {
- assert.ifError(err);
- Temp.findById(t._id, function(err, found) {
- assert.ifError(err);
- assert.equal(found.nested.nums.length, 2);
- assert.equal(found.nested.nums[0], 1, 1);
- assert.equal(found.nested.nums[1], 2, 2);
- found.nested.nums.$shift();
- assert.equal(found.nested.nums.length, 1);
- assert.equal(found.nested.nums[0], 2);
-
- found.save(function(err) {
- assert.ifError(err);
- Temp.findById(t._id, function(err, found) {
- assert.ifError(err);
- assert.equal(found.nested.nums.length, 1);
- assert.equal(found.nested.nums[0], 2);
- done();
- });
- });
- });
- });
- });
- });
+ const t = await Temp.create({ nested: { nums: [1, 2, 3] } });
+ const doc = await Temp.findById(t._id);
+ assert.equal(doc.nested.nums.length, 3);
+ doc.nested.nums.$pop();
+ assert.equal(doc.nested.nums.length, 2);
+ assert.equal(doc.nested.nums[0], 1);
+ assert.equal(doc.nested.nums[1], 2);
+ await doc.save();
+ const check = await Temp.findById(t._id);
+ assert.equal(check.nested.nums.length, 2);
+ assert.equal(check.nested.nums[0], 1, 1);
+ assert.equal(check.nested.nums[1], 2, 2);
+ check.nested.nums.$shift();
+ assert.equal(check.nested.nums.length, 1);
+ assert.equal(check.nested.nums[0], 2);
+ await check.save();
+ const final = await Temp.findById(t._id);
+ assert.equal(final.nested.nums.length, 1);
+ assert.equal(final.nested.nums[0], 2);
});
describe('saving embedded arrays', function() {
- it('of Numbers atomically', function(done) {
+ it('of Numbers atomically', async function() {
const TempSchema = new Schema({
nums: [Number]
});
- let totalDocs = 2;
- const saveQueue = [];
const Temp = db.model('Test', TempSchema);
const t = new Temp();
- function complete() {
- Temp.findOne({ _id: t.get('_id') }, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.get('nums').length, 3);
-
- let v = doc.get('nums').some(function(num) {
- return num.valueOf() === 1;
- });
- assert.ok(v);
-
- v = doc.get('nums').some(function(num) {
- return num.valueOf() === 2;
- });
- assert.ok(v);
-
- v = doc.get('nums').some(function(num) {
- return num.valueOf() === 3;
- });
- assert.ok(v);
- done();
- });
- }
+ await t.save();
+ await Promise.all(Array(3).fill(null).map(async(_, i) => {
+ const doc = await Temp.findOne({ _id: t.get('_id') });
+ doc.get('nums').push(i + 1);
+ await doc.save();
+ }));
- function save(doc) {
- saveQueue.push(doc);
- if (saveQueue.length === totalDocs) {
- saveQueue.forEach(function(doc) {
- doc.save(function(err) {
- assert.ifError(err);
- --totalDocs || complete();
- });
- });
- }
- }
+ const doc = await Temp.findById(t._id);
+ assert.equal(doc.get('nums').length, 3);
- t.save(function(err) {
- assert.ifError(err);
+ let v = doc.get('nums').some(function(num) {
+ return num.valueOf() === 1;
+ });
+ assert.ok(v);
- Temp.findOne({ _id: t.get('_id') }, function(err, doc) {
- assert.ifError(err);
- doc.get('nums').push(1);
- save(doc);
- });
+ v = doc.get('nums').some(function(num) {
+ return num.valueOf() === 2;
+ });
+ assert.ok(v);
- Temp.findOne({ _id: t.get('_id') }, function(err, doc) {
- assert.ifError(err);
- doc.get('nums').push(2, 3);
- save(doc);
- });
+ v = doc.get('nums').some(function(num) {
+ return num.valueOf() === 3;
});
+ assert.ok(v);
});
- it('of Strings atomically', function(done) {
+ it('of Strings atomically', async function() {
const StrListSchema = new Schema({
strings: [String]
});
- let totalDocs = 2;
- const saveQueue = [];
const StrList = db.model('Test', StrListSchema);
const t = new StrList();
- function complete() {
- StrList.findOne({ _id: t.get('_id') }, function(err, doc) {
- assert.ifError(err);
-
- assert.equal(doc.get('strings').length, 3);
+ await t.save();
- let v = doc.get('strings').some(function(str) {
- return str === 'a';
- });
- assert.ok(v);
-
- v = doc.get('strings').some(function(str) {
- return str === 'b';
- });
- assert.ok(v);
+ await Promise.all(Array(3).fill(null).map(async(_, i) => {
+ const doc = await StrList.findOne({ _id: t.get('_id') });
+ doc.get('strings').push(['a', 'b', 'c'][i]);
+ await doc.save();
+ }));
- v = doc.get('strings').some(function(str) {
- return str === 'c';
- });
- assert.ok(v);
- done();
- });
- }
+ const doc = await StrList.findById(t);
- function save(doc) {
- saveQueue.push(doc);
- if (saveQueue.length === totalDocs) {
- saveQueue.forEach(function(doc) {
- doc.save(function(err) {
- assert.ifError(err);
- --totalDocs || complete();
- });
- });
- }
- }
+ assert.equal(doc.get('strings').length, 3);
- t.save(function(err) {
- assert.ifError(err);
+ let v = doc.get('strings').some(function(str) {
+ return str === 'a';
+ });
+ assert.ok(v);
- StrList.findOne({ _id: t.get('_id') }, function(err, doc) {
- assert.ifError(err);
- doc.get('strings').push('a');
- save(doc);
- });
+ v = doc.get('strings').some(function(str) {
+ return str === 'b';
+ });
+ assert.ok(v);
- StrList.findOne({ _id: t.get('_id') }, function(err, doc) {
- assert.ifError(err);
- doc.get('strings').push('b', 'c');
- save(doc);
- });
+ v = doc.get('strings').some(function(str) {
+ return str === 'c';
});
+ assert.ok(v);
});
- it('of Buffers atomically', function(done) {
+ it('of Buffers atomically', async function() {
const BufListSchema = new Schema({
buffers: [Buffer]
});
- let totalDocs = 2;
- const saveQueue = [];
const BufList = db.model('Test', BufListSchema);
const t = new BufList();
- function complete() {
- BufList.findOne({ _id: t.get('_id') }, function(err, doc) {
- assert.ifError(err);
-
- assert.equal(doc.get('buffers').length, 3);
+ await t.save();
- let v = doc.get('buffers').some(function(buf) {
- return buf[0] === 140;
- });
- assert.ok(v);
-
- v = doc.get('buffers').some(function(buf) {
- return buf[0] === 141;
- });
- assert.ok(v);
-
- v = doc.get('buffers').some(function(buf) {
- return buf[0] === 142;
- });
- assert.ok(v);
+ await Promise.all(Array(3).fill(null).map(async(_, i) => {
+ const doc = await BufList.findOne({ _id: t.get('_id') });
+ doc.get('buffers').push(Buffer.from([140 + i]));
+ await doc.save();
+ }));
- done();
- });
- }
+ const doc = await BufList.findById(t);
- function save(doc) {
- saveQueue.push(doc);
- if (saveQueue.length === totalDocs) {
- saveQueue.forEach(function(doc) {
- doc.save(function(err) {
- assert.ifError(err);
- --totalDocs || complete();
- });
- });
- }
- }
+ assert.equal(doc.get('buffers').length, 3);
- t.save(function(err) {
- assert.ifError(err);
+ let v = doc.get('buffers').some(function(buf) {
+ return buf[0] === 140;
+ });
+ assert.ok(v);
- BufList.findOne({ _id: t.get('_id') }, function(err, doc) {
- assert.ifError(err);
- doc.get('buffers').push(Buffer.from([140]));
- save(doc);
- });
+ v = doc.get('buffers').some(function(buf) {
+ return buf[0] === 141;
+ });
+ assert.ok(v);
- BufList.findOne({ _id: t.get('_id') }, function(err, doc) {
- assert.ifError(err);
- doc.get('buffers').push(Buffer.from([141]), Buffer.from([142]));
- save(doc);
- });
+ v = doc.get('buffers').some(function(buf) {
+ return buf[0] === 142;
});
+ assert.ok(v);
});
- it('works with modified element properties + doc removal (gh-975)', function(done) {
+ it('works with modified element properties + doc removal (gh-975)', async function() {
const B = BlogPost;
const b = new B({ comments: [{ title: 'gh-975' }] });
- b.save(function(err) {
- assert.ifError(err);
-
- b.comments[0].title = 'changed';
- b.save(function(err) {
- assert.ifError(err);
-
- b.comments[0].remove();
- b.save(function(err) {
- assert.ifError(err);
-
- B.findByIdAndUpdate({ _id: b._id }, { $set: { comments: [{ title: 'a' }] } }, { new: true }, function(err, doc) {
- assert.ifError(err);
- doc.comments[0].title = 'differ';
- doc.comments[0].remove();
- doc.save(function(err) {
- assert.ifError(err);
- B.findById(doc._id, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.comments.length, 0);
- done();
- });
- });
- });
- });
- });
- });
- });
-
- it('updating an embedded document in an embedded array with set call', function(done) {
- BlogPost.create({
+ await b.save();
+ b.comments[0].title = 'changed';
+ await b.save();
+ await b.comments[0].deleteOne();
+ await b.save();
+ const check = await B.findByIdAndUpdate({ _id: b._id }, { $set: { comments: [{ title: 'a' }] } }, { new: true });
+ check.comments[0].title = 'differ';
+ await check.comments[0].deleteOne();
+ await check.save();
+ const final = await B.findById(check._id);
+ assert.equal(final.comments.length, 0);
+ });
+
+ it('updating an embedded document in an embedded array with set call', async function() {
+ const post = await BlogPost.create({
comments: [{
title: 'before-change'
}]
- }, function(err, post) {
- assert.ifError(err);
- BlogPost.findById(post._id, function(err, found) {
- assert.ifError(err);
- assert.equal(found.comments[0].title, 'before-change');
- const subDoc = [{
- _id: found.comments[0]._id,
- title: 'after-change'
- }];
- found.set('comments', subDoc);
-
- found.save(function(err) {
- assert.ifError(err);
- BlogPost.findById(found._id, function(err, updated) {
- assert.ifError(err);
- assert.equal(updated.comments[0].title, 'after-change');
- done();
- });
- });
- });
});
+ const found = await BlogPost.findById(post._id);
+ assert.equal(found.comments[0].title, 'before-change');
+ const subDoc = [{
+ _id: found.comments[0]._id,
+ title: 'after-change'
+ }];
+ found.set('comments', subDoc);
+
+ await found.save();
+ const updated = await BlogPost.findById(found._id);
+ assert.equal(updated.comments[0].title, 'after-change');
});
});
- it('updating an embedded document in an embedded array (gh-255)', function(done) {
- BlogPost.create({ comments: [{ title: 'woot' }] }, function(err, post) {
- assert.ifError(err);
- BlogPost.findById(post._id, function(err, found) {
- assert.ifError(err);
- assert.equal(found.comments[0].title, 'woot');
- found.comments[0].title = 'notwoot';
- found.save(function(err) {
- assert.ifError(err);
- BlogPost.findById(found._id, function(err, updated) {
- assert.ifError(err);
- assert.equal(updated.comments[0].title, 'notwoot');
- done();
- });
- });
- });
- });
+ it('updating an embedded document in an embedded array (gh-255)', async function() {
+ const post = await BlogPost.create({ comments: [{ title: 'woot' }] });
+ const found = await BlogPost.findById(post._id);
+ assert.equal(found.comments[0].title, 'woot');
+ found.comments[0].title = 'notwoot';
+ await found.save();
+ const updated = await BlogPost.findById(found._id);
+ assert.equal(updated.comments[0].title, 'notwoot');
});
- it('updating an embedded array document to an Object value (gh-334)', function(done) {
+ it('updating an embedded array document to an Object value (gh-334)', async function() {
const SubSchema = new Schema({
name: String,
subObj: { subName: String }
@@ -2641,48 +1904,30 @@ describe('Model', function() {
const instance = new AModel();
instance.set({ name: 'name-value', arrData: [{ name: 'arrName1', subObj: { subName: 'subName1' } }] });
- instance.save(function(err) {
- assert.ifError(err);
- AModel.findById(instance.id, function(err, doc) {
- assert.ifError(err);
- doc.arrData[0].set('subObj', { subName: 'modified subName' });
- doc.save(function(err) {
- assert.ifError(err);
- AModel.findById(instance.id, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.arrData[0].subObj.subName, 'modified subName');
- done();
- });
- });
- });
- });
+ await instance.save();
+ let doc = await AModel.findById(instance.id);
+ doc.arrData[0].set('subObj', { subName: 'modified subName' });
+ await doc.save();
+ doc = await AModel.findById(instance.id);
+ assert.equal(doc.arrData[0].subObj.subName, 'modified subName');
});
- it('saving an embedded document twice should not push that doc onto the parent doc twice (gh-267)', function(done) {
+ it('saving an embedded document twice should not push that doc onto the parent doc twice (gh-267)', async function() {
const post = new BlogPost();
post.comments.push({ title: 'woot' });
- post.save(function(err) {
- assert.ifError(err);
- assert.equal(post.comments.length, 1);
- BlogPost.findById(post.id, function(err, found) {
- assert.ifError(err);
- assert.equal(found.comments.length, 1);
- post.save(function(err) {
- assert.ifError(err);
- assert.equal(post.comments.length, 1);
- BlogPost.findById(post.id, function(err, found) {
- assert.ifError(err);
- assert.equal(found.comments.length, 1);
- done();
- });
- });
- });
- });
+ await post.save();
+ assert.equal(post.comments.length, 1);
+ let found = await BlogPost.findById(post.id);
+ assert.equal(found.comments.length, 1);
+ await post.save();
+ assert.equal(post.comments.length, 1);
+ found = await BlogPost.findById(post.id);
+ assert.equal(found.comments.length, 1);
});
describe('embedded array filtering', function() {
- it('by the id shortcut function', function(done) {
+ it('by the id shortcut function', async function() {
const post = new BlogPost();
post.comments.push({ title: 'woot' });
@@ -2691,185 +1936,107 @@ describe('Model', function() {
const subdoc1 = post.comments[0];
const subdoc2 = post.comments[1];
- post.save(function(err) {
- assert.ifError(err);
+ await post.save();
- BlogPost.findById(post.get('_id'), function(err, doc) {
- assert.ifError(err);
+ const doc = await BlogPost.findById(post.get('_id'));
- // test with an objectid
- assert.equal(doc.comments.id(subdoc1.get('_id')).title, 'woot');
+ // test with an objectid
+ assert.equal(doc.comments.id(subdoc1.get('_id')).title, 'woot');
- // test with a string
- const id = subdoc2._id.toString();
- assert.equal(doc.comments.id(id).title, 'aaaa');
- done();
- });
- });
+ // test with a string
+ const id = subdoc2._id.toString();
+ assert.equal(doc.comments.id(id).title, 'aaaa');
});
- it('by the id with cast error', function(done) {
+ it('by the id with cast error', async function() {
const post = new BlogPost();
- post.save(function(err) {
- assert.ifError(err);
-
- BlogPost.findById(post.get('_id'), function(err, doc) {
- assert.ifError(err);
- assert.strictEqual(doc.comments.id(null), null);
- done();
- });
- });
+ await post.save();
+ const doc = await BlogPost.findById(post.get('_id'));
+ assert.strictEqual(doc.comments.id(null), null);
});
- it('by the id shortcut with no match', function(done) {
+ it('by the id shortcut with no match', async function() {
const post = new BlogPost();
- post.save(function(err) {
- assert.ifError(err);
-
- BlogPost.findById(post.get('_id'), function(err, doc) {
- assert.ifError(err);
- assert.strictEqual(doc.comments.id(new DocumentObjectId()), null);
- done();
- });
- });
+ await post.save();
+ const doc = await BlogPost.findById(post.get('_id'));
+ assert.strictEqual(doc.comments.id(new DocumentObjectId()), null);
});
});
- it('removing a subdocument atomically', function(done) {
+ it('removing a subdocument atomically', async function() {
const post = new BlogPost();
post.title = 'hahaha';
post.comments.push({ title: 'woot' });
post.comments.push({ title: 'aaaa' });
- post.save(function(err) {
- assert.ifError(err);
-
- BlogPost.findById(post.get('_id'), function(err, doc) {
- assert.ifError(err);
+ await post.save();
+ let doc = await BlogPost.findById(post.get('_id'));
- doc.comments[0].remove();
- doc.save(function(err) {
- assert.ifError(err);
-
- BlogPost.findById(post.get('_id'), function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.comments.length, 1);
- assert.equal(doc.comments[0].title, 'aaaa');
- done();
- });
- });
- });
- });
+ doc.comments[0].deleteOne();
+ await doc.save();
+ doc = await BlogPost.findById(post.get('_id'));
+ assert.equal(doc.comments.length, 1);
+ assert.equal(doc.comments[0].title, 'aaaa');
});
- it('single pull embedded doc', function(done) {
+ it('single pull embedded doc', async function() {
const post = new BlogPost();
post.title = 'hahaha';
post.comments.push({ title: 'woot' });
post.comments.push({ title: 'aaaa' });
- post.save(function(err) {
- assert.ifError(err);
-
- BlogPost.findById(post.get('_id'), function(err, doc) {
- assert.ifError(err);
-
- doc.comments.pull(doc.comments[0]);
- doc.comments.pull(doc.comments[0]);
- doc.save(function(err) {
- assert.ifError(err);
+ await post.save();
+ let doc = await BlogPost.findById(post.get('_id'));
- BlogPost.findById(post.get('_id'), function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.comments.length, 0);
- done();
- });
- });
- });
- });
+ doc.comments.pull(doc.comments[0]);
+ doc.comments.pull(doc.comments[0]);
+ await doc.save();
+ doc = await BlogPost.findById(post.get('_id'));
+ assert.equal(doc.comments.length, 0);
});
- it('saving mixed data', function(done) {
- let count = 3;
-
+ it('saving mixed data', async function() {
// string
const post = new BlogPost();
post.mixed = 'woot';
- post.save(function(err) {
- assert.ifError(err);
-
- BlogPost.findById(post._id, function(err) {
- assert.ifError(err);
- if (--count) {
- return;
- }
- done();
- });
- });
+ await post.save();
+ await BlogPost.findById(post._id);
// array
const post2 = new BlogPost();
post2.mixed = { name: 'mr bungle', arr: [] };
- post2.save(function(err) {
- assert.ifError(err);
-
- BlogPost.findById(post2._id, function(err, doc) {
- assert.ifError(err);
+ await post2.save();
+ let doc = await BlogPost.findById(post2._id);
- assert.equal(Array.isArray(doc.mixed.arr), true);
+ assert.equal(Array.isArray(doc.mixed.arr), true);
- doc.mixed = [{ foo: 'bar' }];
- doc.save(function(err) {
- assert.ifError(err);
-
- BlogPost.findById(doc._id, function(err, doc) {
- assert.ifError(err);
+ doc.mixed = [{ foo: 'bar' }];
+ await doc.save();
+ doc = await BlogPost.findById(doc._id);
- assert.equal(Array.isArray(doc.mixed), true);
- doc.mixed.push({ hello: 'world' });
- doc.mixed.push(['foo', 'bar']);
- doc.markModified('mixed');
+ assert.equal(Array.isArray(doc.mixed), true);
+ doc.mixed.push({ hello: 'world' });
+ doc.mixed.push(['foo', 'bar']);
+ doc.markModified('mixed');
- doc.save(function(err) {
- assert.ifError(err);
+ await doc.save();
+ doc = await BlogPost.findById(post2._id);
+ assert.deepEqual(doc.mixed[0], { foo: 'bar' });
+ assert.deepEqual(doc.mixed[1], { hello: 'world' });
+ assert.deepEqual(doc.mixed[2], ['foo', 'bar']);
+
+
+ // date
+ const post3 = new BlogPost();
+ post3.mixed = new Date();
+ await post3.save();
+ doc = await BlogPost.findById(post3._id);
+ assert.ok(doc.mixed instanceof Date);
+ });
- BlogPost.findById(post2._id, function(err, doc) {
- assert.ifError(err);
-
- assert.deepEqual(doc.mixed[0], { foo: 'bar' });
- assert.deepEqual(doc.mixed[1], { hello: 'world' });
- assert.deepEqual(doc.mixed[2], ['foo', 'bar']);
- if (--count) {
- return;
- }
- done();
- });
- });
- });
-
- // date
- const post3 = new BlogPost();
- post3.mixed = new Date();
- post3.save(function(err) {
- assert.ifError(err);
-
- BlogPost.findById(post3._id, function(err, doc) {
- assert.ifError(err);
- assert.ok(doc.mixed instanceof Date);
- if (--count) {
- return;
- }
- done();
- });
- });
- });
- });
- });
- });
-
- it('populating mixed data from the constructor (gh-200)', function(done) {
+ it('populating mixed data from the constructor (gh-200)', function() {
const post = new BlogPost({
mixed: {
type: 'test',
@@ -2883,10 +2050,10 @@ describe('Model', function() {
assert.equal(post.mixed.type, 'test');
assert.equal(post.mixed.github, 'rules');
assert.equal(post.mixed.nested.number, 3);
- done();
+
});
- it('"type" is allowed as a key', function(done) {
+ it('"type" is allowed as a key', async function() {
mongoose.model('TestTypeDefaults', new Schema({
type: { type: String, default: 'YES!' }
}));
@@ -2908,38 +2075,23 @@ describe('Model', function() {
post = new TestDefaults2();
post.x.y.type = '#402';
post.x.y.owner = 'me';
- post.save(function(err) {
- assert.ifError(err);
- done();
- });
+ await post.save();
});
- it('unaltered model does not clear the doc (gh-195)', function(done) {
+ it('unaltered model does not clear the doc (gh-195)', async function() {
const post = new BlogPost();
post.title = 'woot';
- post.save(function(err) {
- assert.ifError(err);
-
- BlogPost.findById(post._id, function(err, doc) {
- assert.ifError(err);
-
- // we deliberately make no alterations
- doc.save(function(err) {
- assert.ifError(err);
-
- BlogPost.findById(doc._id, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.title, 'woot');
- done();
- });
- });
- });
- });
+ await post.save();
+ let doc = await BlogPost.findById(post._id);
+ // we deliberately make no alterations
+ await doc.save();
+ doc = await BlogPost.findById(doc._id);
+ assert.equal(doc.title, 'woot');
});
describe('hooks', function() {
describe('pre', function() {
- it('with undefined and null', function(done) {
+ it('with undefined and null', async function() {
const schema = new Schema({ name: String });
let called = 0;
@@ -2956,43 +2108,11 @@ describe('Model', function() {
const S = db.model('Test', schema);
const s = new S({ name: 'zupa' });
- s.save(function(err) {
- assert.ifError(err);
- assert.equal(called, 2);
- done();
- });
- });
-
-
- it('with an async waterfall', function(done) {
- const schema = new Schema({ name: String });
- let called = 0;
-
- schema.pre('save', true, function(next, done) {
- called++;
- process.nextTick(function() {
- next();
- done();
- });
- });
-
- schema.pre('save', function(next) {
- called++;
- return next();
- });
-
- const S = db.model('Test', schema);
- const s = new S({ name: 'zupa' });
-
- const p = s.save();
- p.then(function() {
- assert.equal(called, 2);
- done();
- }).catch(done);
+ await s.save();
+ assert.equal(called, 2);
});
-
- it('called on all sub levels', function(done) {
+ it('called on all sub levels', async function() {
const grandSchema = new Schema({ name: String });
grandSchema.pre('save', function(next) {
this.name = 'grand';
@@ -3015,17 +2135,13 @@ describe('Model', function() {
const S = db.model('Test', schema);
const s = new S({ name: 'a', child: [{ name: 'b', grand: [{ name: 'c' }] }] });
- s.save(function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.name, 'parent');
- assert.equal(doc.child[0].name, 'child');
- assert.equal(doc.child[0].grand[0].name, 'grand');
- done();
- });
+ await s.save();
+ assert.equal(s.name, 'parent');
+ assert.equal(s.child[0].name, 'child');
+ assert.equal(s.child[0].grand[0].name, 'grand');
});
-
- it('error on any sub level', function(done) {
+ it('error on any sub level', async function() {
const grandSchema = new Schema({ name: String });
grandSchema.pre('save', function(next) {
next(new Error('Error 101'));
@@ -3046,15 +2162,12 @@ describe('Model', function() {
const S = db.model('Test', schema);
const s = new S({ name: 'a', child: [{ name: 'b', grand: [{ name: 'c' }] }] });
- s.save(function(err) {
- assert.ok(err instanceof Error);
- assert.equal(err.message, 'Error 101');
- done();
- });
+ const err = await s.save().then(() => null, err => err);
+ assert.equal(err.message, 'Error 101');
});
describe('init', function() {
- it('has access to the true ObjectId when used with querying (gh-289)', function(done) {
+ it('has access to the true ObjectId when used with querying (gh-289)', async function() {
const PreInitSchema = new Schema({});
let preId = null;
@@ -3065,25 +2178,20 @@ describe('Model', function() {
const PreInit = db.model('Test', PreInitSchema);
const doc = new PreInit();
- doc.save(function(err) {
- assert.ifError(err);
- PreInit.findById(doc._id, function(err) {
- assert.ifError(err);
- assert.strictEqual(undefined, preId);
- done();
- });
- });
+ await doc.save();
+ await PreInit.findById(doc._id);
+ assert.strictEqual(undefined, preId);
});
});
});
describe('post', function() {
- it('works', function(done) {
+ it('works', async function() {
const schema = new Schema({
title: String
});
let save = false;
- let remove = false;
+ let deleteOne = false;
let init = false;
let post = undefined;
@@ -3096,38 +2204,25 @@ describe('Model', function() {
init = true;
});
- schema.post('remove', function(arg) {
+ schema.post('deleteOne', { document: true, query: false }, function(arg) {
assert.equal(arg.id, post.id);
- remove = true;
+ deleteOne = true;
});
const BlogPost = db.model('Test', schema);
post = new BlogPost();
- post.save(function(err) {
- process.nextTick(function() {
- assert.ifError(err);
- assert.ok(save);
- BlogPost.findById(post._id, function(err, doc) {
- process.nextTick(function() {
- assert.ifError(err);
- assert.ok(init);
-
- doc.remove(function(err) {
- process.nextTick(function() {
- assert.ifError(err);
- assert.ok(remove);
- done();
- });
- });
- });
- });
- });
- });
+ await post.save();
+ assert.ok(save);
+ const doc = await BlogPost.findById(post._id);
+ assert.ok(init);
+
+ await doc.deleteOne();
+ assert.ok(deleteOne);
});
- it('on embedded docs', function(done) {
+ it('on embedded docs', async function() {
let save = false;
const EmbeddedSchema = new Schema({
@@ -3148,11 +2243,8 @@ describe('Model', function() {
parent.embeds.push({ title: 'Testing post hooks for embedded docs' });
- parent.save(function(err) {
- assert.ifError(err);
- assert.ok(save);
- done();
- });
+ await parent.save();
+ assert.ok(save);
});
it('callback should receive parameter of type document after bulkSave (gh-13026)', async function() {
@@ -3180,18 +2272,6 @@ describe('Model', function() {
});
describe('#exec()', function() {
- it.skip('count()', function(done) {
- BlogPost.create({ title: 'interoperable count as promise' }, function(err) {
- assert.ifError(err);
- const query = BlogPost.count({ title: 'interoperable count as promise' });
- query.exec(function(err, count) {
- assert.ifError(err);
- assert.equal(count, 1);
- done();
- });
- });
- });
-
it('countDocuments()', function() {
return BlogPost.create({ title: 'foo' }).
then(() => BlogPost.countDocuments({ title: 'foo' }).exec()).
@@ -3208,113 +2288,49 @@ describe('Model', function() {
});
});
- it('update()', function(done) {
- BlogPost.create({ title: 'interoperable update as promise' }, function(err) {
- assert.ifError(err);
- const query = BlogPost.update({ title: 'interoperable update as promise' }, { title: 'interoperable update as promise delta' });
- query.exec(function(err, res) {
- assert.ifError(err);
- assert.equal(res.matchedCount, 1);
- assert.equal(res.modifiedCount, 1);
- BlogPost.count({ title: 'interoperable update as promise delta' }, function(err, count) {
- assert.ifError(err);
- assert.equal(count, 1);
- done();
- });
- });
- });
+ it('updateOne()', async function() {
+ await BlogPost.create({ title: 'interoperable update as promise' });
+ const query = BlogPost.updateOne({ title: 'interoperable update as promise' }, { title: 'interoperable update as promise delta' });
+ const res = await query.exec();
+ assert.equal(res.matchedCount, 1);
+ assert.equal(res.modifiedCount, 1);
+ const count = await BlogPost.countDocuments({ title: 'interoperable update as promise delta' });
+ assert.equal(count, 1);
});
- it('findOne()', function(done) {
- BlogPost.create({ title: 'interoperable findOne as promise' }, function(err, created) {
- assert.ifError(err);
- const query = BlogPost.findOne({ title: 'interoperable findOne as promise' });
- query.exec(function(err, found) {
- assert.ifError(err);
- assert.equal(found.id, created.id);
- done();
- });
- });
+ it('findOne()', async function() {
+ const created = await BlogPost.create({ title: 'interoperable findOne as promise' });
+ const query = BlogPost.findOne({ title: 'interoperable findOne as promise' });
+ const found = await query.exec();
+ assert.equal(found.id, created.id);
});
- it('find()', function(done) {
- BlogPost.create(
- { title: 'interoperable find as promise' },
+ it('find()', async function() {
+ const [createdOne, createdTwo] = await BlogPost.create([
{ title: 'interoperable find as promise' },
- function(err, createdOne, createdTwo) {
- assert.ifError(err);
- const query = BlogPost.find({ title: 'interoperable find as promise' }).sort('_id');
- query.exec(function(err, found) {
- assert.ifError(err);
- assert.equal(found.length, 2);
- const ids = {};
- ids[String(found[0]._id)] = 1;
- ids[String(found[1]._id)] = 1;
- assert.ok(String(createdOne._id) in ids);
- assert.ok(String(createdTwo._id) in ids);
- done();
- });
- });
- });
+ { title: 'interoperable find as promise' }
+ ]);
- it.skip('remove()', function(done) {
- BlogPost.create(
- { title: 'interoperable remove as promise' },
- function(err) {
- assert.ifError(err);
- const query = BlogPost.remove({ title: 'interoperable remove as promise' });
- query.exec(function(err) {
- assert.ifError(err);
- BlogPost.count({ title: 'interoperable remove as promise' }, function(err, count) {
- assert.equal(count, 0);
- done();
- });
- });
- });
+ const query = BlogPost.find({ title: 'interoperable find as promise' }).sort('_id');
+ const found = await query.exec();
+ assert.equal(found.length, 2);
+ const ids = {};
+ ids[String(found[0]._id)] = 1;
+ ids[String(found[1]._id)] = 1;
+ assert.ok(String(createdOne._id) in ids);
+ assert.ok(String(createdTwo._id) in ids);
});
- it('op can be changed', function(done) {
+ it('op can be changed', async function() {
const title = 'interop ad-hoc as promise';
- BlogPost.create({ title: title }, function(err, created) {
- assert.ifError(err);
- const query = BlogPost.count({ title: title });
- query.exec('findOne', function(err, found) {
- assert.ifError(err);
- assert.equal(found.id, created.id);
- done();
- });
- });
+ const created = await BlogPost.create({ title: title });
+ const query = BlogPost.countDocuments({ title: title });
+ const found = await query.exec('findOne');
+ assert.equal(found.id, created.id);
});
describe('promises', function() {
- it.skip('count()', function(done) {
- BlogPost.create({ title: 'interoperable count as promise 2' }, function(err) {
- assert.ifError(err);
- const query = BlogPost.count({ title: 'interoperable count as promise 2' });
- const promise = query.exec();
- promise.then(function(count) {
- assert.equal(count, 1);
- done();
- }).catch(done);
- });
- });
-
- it.skip('update()', function(done) {
- BlogPost.create({ title: 'interoperable update as promise 2' }, function(err) {
- assert.ifError(err);
- const query = BlogPost.update({ title: 'interoperable update as promise 2' }, { title: 'interoperable update as promise delta 2' });
- const promise = query.exec();
- promise.then(function() {
- BlogPost.count({ title: 'interoperable update as promise delta 2' }, function(err, count) {
- assert.ifError(err);
- assert.equal(count, 1);
- done();
- });
- });
- });
- });
-
it('findOne()', function() {
let created;
return BlogPost.create({ title: 'interoperable findOne as promise 2' }).
@@ -3329,76 +2345,23 @@ describe('Model', function() {
});
});
- it('find()', function(done) {
- BlogPost.create(
- { title: 'interoperable find as promise 2' },
+ it('find()', async function() {
+ const [createdOne, createdTwo] = await BlogPost.create(
{ title: 'interoperable find as promise 2' },
- function(err, createdOne, createdTwo) {
- assert.ifError(err);
- const query = BlogPost.find({ title: 'interoperable find as promise 2' }).sort('_id');
- const promise = query.exec();
- promise.then(function(found) {
- assert.ifError(err);
- assert.equal(found.length, 2);
- assert.equal(found[0].id, createdOne.id);
- assert.equal(found[1].id, createdTwo.id);
- done();
- }).catch(done);
- });
- });
-
- it.skip('remove()', function() {
- return BlogPost.create({ title: 'interoperable remove as promise 2' }).
- then(() => {
- return BlogPost.remove({ title: 'interoperable remove as promise 2' });
- }).
- then(() => {
- return BlogPost.count({ title: 'interoperable remove as promise 2' });
- }).
- then(count => {
- assert.equal(count, 0);
- });
- });
+ { title: 'interoperable find as promise 2' }
+ );
- it('are thenable', function(done) {
- const peopleSchema = new Schema({ name: String, likes: ['ObjectId'] });
- const P = db.model('Test', peopleSchema);
- BlogPost.create(
- { title: 'then promise 1' },
- { title: 'then promise 2' },
- { title: 'then promise 3' },
- function(err, d1, d2, d3) {
- assert.ifError(err);
-
- P.create(
- { name: 'brandon', likes: [d1] },
- { name: 'ben', likes: [d2] },
- { name: 'bernie', likes: [d3] },
- function(err) {
- assert.ifError(err);
-
- const promise = BlogPost.find({ title: /^then promise/ }).select('_id').exec();
- promise.then(function(blogs) {
- const ids = blogs.map(function(m) {
- return m._id;
- });
- return P.where('likes').in(ids).exec();
- }).then(function(people) {
- assert.equal(people.length, 3);
- return people;
- }).then(function() {
- done();
- }, function(err) {
- done(new Error(err));
- });
- });
- });
+ const query = BlogPost.find({ title: 'interoperable find as promise 2' }).sort('_id');
+ const found = await query.exec();
+ assert.equal(found.length, 2);
+ assert.equal(found[0].id, createdOne.id);
+ assert.equal(found[1].id, createdTwo.id);
});
});
});
describe('console.log', function() {
- it('hides private props', function(done) {
+ it('hides private props', function() {
const date = new Date(1305730951086);
const id0 = new DocumentObjectId('4dd3e169dbfb13b4570000b9');
const id1 = new DocumentObjectId('4dd3e169dbfb13b4570000b6');
@@ -3422,12 +2385,12 @@ describe('Model', function() {
assert.deepEqual(out.numbers, Array.prototype.slice.call(post.numbers));
assert.equal(out.date.valueOf(), post.date.valueOf());
assert.equal(out.activePaths, undefined);
- done();
+
});
});
describe('pathnames', function() {
- it('named path can be used', function(done) {
+ it('named path can be used', function() {
const P = db.model('Test', new Schema({ path: String }));
let threw = false;
@@ -3438,105 +2401,77 @@ describe('Model', function() {
}
assert.ok(!threw);
- done();
+
});
});
- it('subdocuments with changed values should persist the values', function(done) {
+ it('subdocuments with changed values should persist the values', async function() {
const Subdoc = new Schema({ name: String, mixed: Schema.Types.Mixed });
const T = db.model('Test', new Schema({ subs: [Subdoc] }));
- const t = new T({ subs: [{ name: 'Hubot', mixed: { w: 1, x: 2 } }] });
+ let t = new T({ subs: [{ name: 'Hubot', mixed: { w: 1, x: 2 } }] });
assert.equal(t.subs[0].name, 'Hubot');
assert.equal(t.subs[0].mixed.w, 1);
assert.equal(t.subs[0].mixed.x, 2);
- t.save(function(err) {
- assert.ifError(err);
-
- T.findById(t._id, function(err, t) {
- assert.ifError(err);
- assert.equal(t.subs[0].name, 'Hubot');
- assert.equal(t.subs[0].mixed.w, 1);
- assert.equal(t.subs[0].mixed.x, 2);
-
- const sub = t.subs[0];
- sub.name = 'Hubot1';
- assert.equal(sub.name, 'Hubot1');
- assert.ok(sub.isModified('name'));
- assert.ok(t.isModified());
-
- t.save(function(err) {
- assert.ifError(err);
-
- T.findById(t._id, function(err, t) {
- assert.ifError(err);
- assert.strictEqual(t.subs[0].name, 'Hubot1');
-
- const sub = t.subs[0];
- sub.mixed.w = 5;
- assert.equal(sub.mixed.w, 5);
- assert.ok(!sub.isModified('mixed'));
- sub.markModified('mixed');
- assert.ok(sub.isModified('mixed'));
- assert.ok(sub.isModified());
- assert.ok(t.isModified());
-
- t.save(function(err) {
- assert.ifError(err);
-
- T.findById(t._id, function(err, t) {
- assert.ifError(err);
- assert.strictEqual(t.subs[0].mixed.w, 5);
- done();
- });
- });
- });
- });
- });
- });
+ await t.save();
+ t = await T.findById(t._id);
+ assert.equal(t.subs[0].name, 'Hubot');
+ assert.equal(t.subs[0].mixed.w, 1);
+ assert.equal(t.subs[0].mixed.x, 2);
+
+ let sub = t.subs[0];
+ sub.name = 'Hubot1';
+ assert.equal(sub.name, 'Hubot1');
+ assert.ok(sub.isModified('name'));
+ assert.ok(t.isModified());
+
+ await t.save();
+ t = await T.findById(t._id);
+ assert.strictEqual(t.subs[0].name, 'Hubot1');
+
+ sub = t.subs[0];
+ sub.mixed.w = 5;
+ assert.equal(sub.mixed.w, 5);
+ assert.ok(!sub.isModified('mixed'));
+ sub.markModified('mixed');
+ assert.ok(sub.isModified('mixed'));
+ assert.ok(sub.isModified());
+ assert.ok(t.isModified());
+
+ await t.save();
+ t = await T.findById(t._id);
+ assert.strictEqual(t.subs[0].mixed.w, 5);
});
describe('RegExps', function() {
- it('can be saved', function(done) {
- const post = new BlogPost({ mixed: { rgx: /^asdf$/ } });
+ it('can be saved', async function() {
+ let post = new BlogPost({ mixed: { rgx: /^asdf$/ } });
+ assert.ok(post.mixed.rgx instanceof RegExp);
+ assert.equal(post.mixed.rgx.source, '^asdf$');
+ await post.save();
+ post = await BlogPost.findById(post._id);
assert.ok(post.mixed.rgx instanceof RegExp);
assert.equal(post.mixed.rgx.source, '^asdf$');
- post.save(function(err) {
- assert.ifError(err);
- BlogPost.findById(post._id, function(err, post) {
- assert.ifError(err);
- assert.ok(post.mixed.rgx instanceof RegExp);
- assert.equal(post.mixed.rgx.source, '^asdf$');
- done();
- });
- });
});
});
// Demonstration showing why GH-261 is a misunderstanding
- it('a single instantiated document should be able to update its embedded documents more than once', function(done) {
+ it('a single instantiated document should be able to update its embedded documents more than once', async function() {
const post = new BlogPost();
post.comments.push({ title: 'one' });
- post.save(function(err) {
- assert.ifError(err);
- assert.equal(post.comments[0].title, 'one');
- post.comments[0].title = 'two';
- assert.equal(post.comments[0].title, 'two');
- post.save(function(err) {
- assert.ifError(err);
- BlogPost.findById(post._id, function(err, found) {
- assert.ifError(err);
- assert.equal(found.comments[0].title, 'two');
- done();
- });
- });
- });
+ await post.save();
+ assert.equal(post.comments[0].title, 'one');
+ post.comments[0].title = 'two';
+ assert.equal(post.comments[0].title, 'two');
+ await post.save();
+ const found = await BlogPost.findById(post._id);
+ assert.equal(found.comments[0].title, 'two');
});
describe('save()', function() {
describe('when no callback is passed', function() {
- it('should emit error on its Model when there are listeners', function(done) {
+ it('should emit error on its Model when there are listeners', async function() {
const DefaultErrSchema = new Schema({});
DefaultErrSchema.pre('save', function(next) {
next(new Error());
@@ -3544,50 +2479,27 @@ describe('Model', function() {
const DefaultErr = db.model('Test', DefaultErrSchema);
- DefaultErr.on('error', function(err) {
- assert.ok(err instanceof Error);
- done();
- });
-
new DefaultErr().save().catch(() => {});
- });
- });
-
- it('saved changes made within callback of a previous no-op save gh-1139', function(done) {
- const post = new BlogPost({ title: 'first' });
- post.save(function(err) {
- assert.ifError(err);
-
- // no op
- post.save(function(err) {
- assert.ifError(err);
- post.title = 'changed';
- post.save(function(err) {
- assert.ifError(err);
-
- BlogPost.findById(post, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.title, 'changed');
- done();
- });
+ await new Promise(resolve => {
+ DefaultErr.once('error', function(err) {
+ assert.ok(err instanceof Error);
+ resolve();
});
});
});
});
- it('rejects new documents that have no _id set (1595)', function(done) {
+ it('rejects new documents that have no _id set (1595)', async function() {
const s = new Schema({ _id: { type: String } });
const B = db.model('Test', s);
const b = new B();
- b.save(function(err) {
- assert.ok(err);
- assert.ok(/must have an _id/.test(err));
- done();
- });
+ const err = await b.save().then(() => null, err => err);
+ assert.ok(err);
+ assert.ok(/must have an _id/.test(err));
});
- it('no TypeError when attempting to save more than once after using atomics', function(done) {
+ it('no TypeError when attempting to save more than once after using atomics', async function() {
const M = db.model('Test', new Schema({
test: { type: 'string', unique: true },
elements: [{
@@ -3602,25 +2514,17 @@ describe('Model', function() {
test: 'b',
elements: [{ el: 'c' }]
});
- M.init(function() {
- a.save(function() {
- b.save(function() {
- b.elements.push({ el: 'd' });
- b.test = 'a';
- b.save(function(error, res) {
- assert.ok(error);
- assert.strictEqual(res, undefined);
- b.save(function(error, res) {
- assert.ok(error);
- assert.strictEqual(res, undefined);
- M.collection.drop(done);
- });
- });
- });
- });
- });
+ await M.init();
+ await a.save();
+ await b.save();
+
+ b.elements.push({ el: 'd' });
+ b.test = 'a';
+
+ const error = await b.save().then(() => null, err => err);
+ assert.ok(error);
});
- it('should clear $versionError and saveOptions after saved (gh-8040)', function(done) {
+ it('should clear $versionError and saveOptions after saved (gh-8040)', async function() {
const schema = new Schema({ name: String });
const Model = db.model('Test', schema);
const doc = new Model({
@@ -3631,116 +2535,176 @@ describe('Model', function() {
assert.ok(doc.$__.$versionError);
assert.ok(doc.$__.saveOptions);
- savePromise.then(function() {
- assert.ok(!doc.$__.$versionError);
- assert.ok(!doc.$__.saveOptions);
- done();
- }).catch(done);
+ await savePromise;
+ assert.ok(!doc.$__.$versionError);
+ assert.ok(!doc.$__.saveOptions);
+ });
+ it('should only save paths specificed in the `pathsToSave` array (gh-9583)', async function() {
+ const schema = new Schema({ name: String, age: Number, weight: { type: Number, validate: v => v == null || v >= 140 }, location: String });
+ const Test = db.model('Test', schema);
+ await Test.create({ name: 'Test Testerson', age: 1, weight: 180, location: 'Florida' });
+ const doc = await Test.findOne();
+ doc.name = 'Test';
+ doc.age = 100;
+ doc.weight = 80;
+ await doc.save({ pathsToSave: ['name'] });
+ const check = await Test.findOne();
+ assert.equal(check.name, 'Test');
+ assert.equal(check.weight, 180);
+ assert.equal(check.age, 1);
+ });
+ it('should have `pathsToSave` work with subdocs (gh-9583)', async function() {
+ const locationSchema = new Schema({ state: String, city: String, zip: { type: Number, validate: v => v == null || v.toString().length == 5 } });
+ const schema = new Schema({
+ name: String,
+ nickname: String,
+ age: Number,
+ weight: { type: Number, validate: v => v == null || v >= 140 },
+ location: locationSchema
+ });
+ const Test = db.model('Test', schema);
+ await Test.create({ name: 'Test Testerson', nickname: 'test', age: 1, weight: 180, location: { state: 'FL', city: 'Miami', zip: 33330 } });
+ let doc = await Test.findOne();
+ doc.name = 'Test';
+ doc.nickname = 'Test2';
+ doc.age = 100;
+ doc.weight = 80;
+ doc.location.state = 'Ohio';
+ doc.location.zip = 0;
+ await doc.save({ pathsToSave: ['name', 'location.state'] });
+ let check = await Test.findOne();
+ assert.equal(check.name, 'Test');
+ assert.equal(check.nickname, 'test');
+ assert.equal(check.weight, 180);
+ assert.equal(check.age, 1);
+ assert.equal(check.location.state, 'Ohio');
+ assert.equal(check.location.zip, 33330);
+ check.location = { state: 'Georgia', city: 'Athens', zip: 34512 };
+ check.name = 'Quiz';
+ check.age = 50;
+ await check.save({ pathsToSave: ['name', 'location'] });
+ const nestedCheck = await Test.findOne();
+ assert.equal(nestedCheck.location.state, 'Georgia');
+ assert.equal(nestedCheck.location.city, 'Athens');
+ assert.equal(nestedCheck.location.zip, 34512);
+ assert.equal(nestedCheck.name, 'Quiz');
+
+ doc = await Test.findOne();
+ doc.name = 'foobar';
+ doc.location.city = 'Reynolds';
+ await doc.save({ pathsToSave: ['location'] });
+ check = await Test.findById(doc._id);
+ assert.equal(check.name, 'Quiz');
+ assert.equal(check.location.city, 'Reynolds');
+ assert.equal(check.location.state, 'Georgia');
+ });
+ it('should have `pathsToSave` work with doc arrays (gh-9583)', async function() {
+ const locationSchema = new Schema({ state: String, city: String, zip: { type: Number, validate: v => v == null || v.toString().length == 5 } });
+ const schema = new Schema({ name: String, age: Number, weight: { type: Number, validate: v => v == null || v >= 140 }, location: [locationSchema] });
+ const Test = db.model('Test', schema);
+ await Test.create({ name: 'Test Testerson', age: 1, weight: 180, location: [{ state: 'FL', city: 'Miami', zip: 33330 }, { state: 'New York', city: 'Albany', zip: 34567 }] });
+ const doc = await Test.findOne();
+ doc.name = 'Test';
+ doc.age = 100;
+ doc.weight = 80;
+ doc.location[0].state = 'Ohio';
+ doc.location[0].zip = 0;
+ doc.location[1].state = 'Ohio';
+ await doc.save({ pathsToSave: ['name', 'location.0.state'] });
+ const check = await Test.findOne();
+ assert.equal(check.name, 'Test');
+ assert.equal(check.weight, 180);
+ assert.equal(check.age, 1);
+ assert.equal(check.location[0].state, 'Ohio');
+ assert.equal(check.location[0].zip, 33330);
+ assert.equal(check.location[1].state, 'New York');
+ check.location[0] = { state: 'Georgia', city: 'Athens', zip: 34512 };
+ check.name = 'Quiz';
+ check.age = 50;
+ await check.save({ pathsToSave: ['name', 'location'] });
+ const nestedCheck = await Test.findOne();
+ assert.equal(nestedCheck.location[0].state, 'Georgia');
+ assert.equal(nestedCheck.location[0].city, 'Athens');
+ assert.equal(nestedCheck.location[0].zip, 34512);
+ assert.equal(nestedCheck.name, 'Quiz');
});
});
describe('_delta()', function() {
- it('should overwrite arrays when directly set (gh-1126)', function(done) {
- BlogPost.create({ title: 'gh-1126', numbers: [1, 2] }, function(err, b) {
- assert.ifError(err);
- BlogPost.findById(b._id, function(err, b) {
- assert.ifError(err);
- assert.deepEqual([1, 2].join(), b.numbers.join());
-
- b.numbers = [];
- b.numbers.push(3);
-
- const d = b.$__delta()[1];
- assert.ok('$set' in d, 'invalid delta ' + JSON.stringify(d));
- assert.ok(Array.isArray(d.$set.numbers));
- assert.equal(d.$set.numbers.length, 1);
- assert.equal(d.$set.numbers[0], 3);
-
- b.save(function(err) {
- assert.ifError(err);
-
- BlogPost.findById(b._id, function(err, b) {
- assert.ifError(err);
- assert.ok(Array.isArray(b.numbers));
- assert.equal(b.numbers.length, 1);
- assert.equal(b.numbers[0], 3);
-
- b.numbers = [3];
- const d = b.$__delta();
- assert.ok(!d);
-
- b.numbers = [4];
- b.numbers.push(5);
- b.save(function(err) {
- assert.ifError(err);
- BlogPost.findById(b._id, function(err, b) {
- assert.ifError(err);
- assert.ok(Array.isArray(b.numbers));
- assert.equal(b.numbers.length, 2);
- assert.equal(b.numbers[0], 4);
- assert.equal(b.numbers[1], 5);
- done();
- });
- });
- });
- });
- });
- });
- });
-
- it('should use $set when subdoc changed before pulling (gh-1303)', function(done) {
+ it('should overwrite arrays when directly set (gh-1126)', async function() {
+ let b = await BlogPost.create({ title: 'gh-1126', numbers: [1, 2] });
+ b = await BlogPost.findById(b._id);
+ assert.deepEqual([1, 2].join(), b.numbers.join());
+
+ b.numbers = [];
+ b.numbers.push(3);
+
+ let d = b.$__delta()[1];
+ assert.ok('$set' in d, 'invalid delta ' + JSON.stringify(d));
+ assert.ok(Array.isArray(d.$set.numbers));
+ assert.equal(d.$set.numbers.length, 1);
+ assert.equal(d.$set.numbers[0], 3);
+
+ await b.save();
+ b = await BlogPost.findById(b._id);
+ assert.ok(Array.isArray(b.numbers));
+ assert.equal(b.numbers.length, 1);
+ assert.equal(b.numbers[0], 3);
+
+ b.numbers = [3];
+ d = b.$__delta();
+ assert.ok(!d);
+
+ b.numbers = [4];
+ b.numbers.push(5);
+ await b.save();
+ b = await BlogPost.findById(b._id);
+ assert.ok(Array.isArray(b.numbers));
+ assert.equal(b.numbers.length, 2);
+ assert.equal(b.numbers[0], 4);
+ assert.equal(b.numbers[1], 5);
+ });
+
+ it('should use $set when subdoc changed before pulling (gh-1303)', async function() {
const B = BlogPost;
- B.create(
- { title: 'gh-1303', comments: [{ body: 'a' }, { body: 'b' }, { body: 'c' }] },
- function(err, b) {
- assert.ifError(err);
- B.findById(b._id, function(err, b) {
- assert.ifError(err);
-
- b.comments[2].body = 'changed';
- b.comments.pull(b.comments[1]);
-
- assert.equal(b.comments.length, 2);
- assert.equal(b.comments[0].body, 'a');
- assert.equal(b.comments[1].body, 'changed');
-
- const d = b.$__delta()[1];
- assert.ok('$set' in d, 'invalid delta ' + JSON.stringify(d));
- assert.ok(Array.isArray(d.$set.comments));
- assert.equal(d.$set.comments.length, 2);
-
- b.save(function(err) {
- assert.ifError(err);
-
- B.findById(b._id, function(err, b) {
- assert.ifError(err);
- assert.ok(Array.isArray(b.comments));
- assert.equal(b.comments.length, 2);
- assert.equal(b.comments[0].body, 'a');
- assert.equal(b.comments[1].body, 'changed');
- done();
- });
- });
- });
- });
+ let b = await B.create(
+ { title: 'gh-1303', comments: [{ body: 'a' }, { body: 'b' }, { body: 'c' }] }
+ );
+
+ b = await B.findById(b._id);
+ b.comments[2].body = 'changed';
+ b.comments.pull(b.comments[1]);
+
+ assert.equal(b.comments.length, 2);
+ assert.equal(b.comments[0].body, 'a');
+ assert.equal(b.comments[1].body, 'changed');
+
+ const d = b.$__delta()[1];
+ assert.ok('$set' in d, 'invalid delta ' + JSON.stringify(d));
+ assert.ok(Array.isArray(d.$set.comments));
+ assert.equal(d.$set.comments.length, 2);
+
+ await b.save();
+ b = await B.findById(b._id);
+ assert.ok(Array.isArray(b.comments));
+ assert.equal(b.comments.length, 2);
+ assert.equal(b.comments[0].body, 'a');
+ assert.equal(b.comments[1].body, 'changed');
});
});
describe('backward compatibility', function() {
- it('with conflicted data in db', function(done) {
+ it('with conflicted data in db', async function() {
const M = db.model('Test', new Schema({ namey: { first: String, last: String } }));
const m = new M({ namey: '[object Object]' });
m.namey = { first: 'GI', last: 'Joe' };// <-- should overwrite the string
- m.save(function(err) {
- assert.strictEqual(err, null);
- assert.strictEqual('GI', m.namey.first);
- assert.strictEqual('Joe', m.namey.last);
- done();
- });
+ await m.save();
+ assert.strictEqual('GI', m.namey.first);
+ assert.strictEqual('Joe', m.namey.last);
});
- it('with positional notation on path not existing in schema (gh-1048)', function(done) {
+ it('with positional notation on path not existing in schema (gh-1048)', async function() {
const M = db.model('Test', Schema({ name: 'string' }));
const o = {
name: 'gh-1048',
@@ -3751,59 +2715,40 @@ describe('Model', function() {
}
};
- M.updateOne({ _id: o._id }, o, { upsert: true, strict: false }, function(err) {
- assert.ifError(err);
- M.findById(o._id, function(err, doc) {
- assert.ifError(err);
- assert.ok(doc);
- assert.ok(doc._doc.databases);
- assert.ok(doc._doc.databases['0']);
- assert.ok(doc._doc.databases['15']);
- assert.equal(doc.databases, undefined);
- done();
- });
- });
+ await M.updateOne({ _id: o._id }, o, { upsert: true, strict: false });
+ const doc = await M.findById(o._id);
+ assert.ok(doc);
+ assert.ok(doc._doc.databases);
+ assert.ok(doc._doc.databases['0']);
+ assert.ok(doc._doc.databases['15']);
+ assert.equal(doc.databases, undefined);
});
});
describe('non-schema adhoc property assignments', function() {
- it('are not saved', function(done) {
+ it('are not saved', async function() {
const B = BlogPost;
const b = new B();
b.whateveriwant = 10;
- b.save(function(err) {
- assert.ifError(err);
- B.collection.findOne({ _id: b._id }, function(err, doc) {
- assert.ifError(err);
- assert.ok(!('whateveriwant' in doc));
- done();
- });
- });
+ await b.save();
+ const doc = await B.collection.findOne({ _id: b._id });
+ assert.ok(!('whateveriwant' in doc));
});
});
- it('should not throw range error when using Number _id and saving existing doc (gh-691)', function(done) {
+ it('should not throw range error when using Number _id and saving existing doc (gh-691)', async function() {
const T = new Schema({ _id: Number, a: String });
const D = db.model('Test', T);
- const d = new D({ _id: 1 });
- d.save(function(err) {
- assert.ifError(err);
-
- D.findById(d._id, function(err, d) {
- assert.ifError(err);
-
- d.a = 'yo';
- d.save(function(err) {
- assert.ifError(err);
- done();
- });
- });
- });
+ let d = new D({ _id: 1 });
+ await d.save();
+ d = await D.findById(d._id);
+ d.a = 'yo';
+ await d.save();
});
describe('setting an unset value', function() {
- it('is saved (gh-742)', function(done) {
+ it('is saved (gh-742)', async function() {
const DefaultTestObject = db.model('Test',
new Schema({
score: { type: Number, default: 55 }
@@ -3812,30 +2757,18 @@ describe('Model', function() {
const myTest = new DefaultTestObject();
- myTest.save(function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.score, 55);
+ await myTest.save();
+ assert.equal(myTest.score, 55);
- DefaultTestObject.findById(doc._id, function(err, doc) {
- assert.ifError(err);
+ let doc = await DefaultTestObject.findById(myTest._id);
- doc.score = undefined; // unset
- doc.save(function(err) {
- assert.ifError(err);
-
- DefaultTestObject.findById(doc._id, function(err, doc) {
- assert.ifError(err);
+ doc.score = undefined; // unset
+ await doc.save();
+ doc = await DefaultTestObject.findById(doc._id);
- doc.score = 55;
- doc.save(function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.score, 55);
- done();
- });
- });
- });
- });
- });
+ doc.score = 55;
+ await doc.save();
+ assert.equal(doc.score, 55);
});
it('is saved object with proper defaults', async function() {
const schema = new Schema({
@@ -3880,20 +2813,15 @@ describe('Model', function() {
});
- it('path is cast to correct value when retreived from db', function(done) {
+ it('path is cast to correct value when retreived from db', async function() {
const schema = new Schema({ title: { type: 'string', index: true } });
const T = db.model('Test', schema);
- T.collection.insertOne({ title: 234 }, function(err) {
- assert.ifError(err);
- T.findOne(function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.title, '234');
- done();
- });
- });
+ await T.collection.insertOne({ title: 234 });
+ const doc = await T.findOne();
+ assert.equal(doc.title, '234');
});
- it('setting a path to undefined should retain the value as undefined', function(done) {
+ it('setting a path to undefined should retain the value as undefined', async function() {
const B = BlogPost;
const doc = new B();
doc.title = 'css3';
@@ -3910,72 +2838,55 @@ describe('Model', function() {
doc.comments = [{ title: 'thanksgiving', body: 'yuuuumm' }];
doc.comments.push({ title: 'turkey', body: 'cranberries' });
- doc.save(function(err) {
- assert.ifError(err);
- B.findById(doc._id, function(err, b) {
- assert.ifError(err);
- assert.equal(b.title, 'css3');
- assert.equal(b.author, 'aaron');
- assert.equal(b.meta.date.toString(), doc.meta.date.toString());
- assert.equal(b.meta.visitors.valueOf(), doc.meta.visitors.valueOf());
- assert.equal(b.comments.length, 2);
- assert.equal(b.comments[0].title, 'thanksgiving');
- assert.equal(b.comments[0].body, 'yuuuumm');
- assert.equal(b.comments[1].title, 'turkey');
- assert.equal(b.comments[1].body, 'cranberries');
- b.title = undefined;
- b.author = null;
- b.meta.date = undefined;
- b.meta.visitors = null;
- b.comments[0].title = null;
- b.comments[0].body = undefined;
- b.save(function(err) {
- assert.ifError(err);
- B.findById(b._id, function(err, b) {
- assert.ifError(err);
- assert.strictEqual(undefined, b.title);
- assert.strictEqual(null, b.author);
-
- assert.strictEqual(undefined, b.meta.date);
- assert.strictEqual(null, b.meta.visitors);
- assert.strictEqual(null, b.comments[0].title);
- assert.strictEqual(undefined, b.comments[0].body);
- assert.equal(b.comments[1].title, 'turkey');
- assert.equal(b.comments[1].body, 'cranberries');
-
- b.meta = undefined;
- b.comments = undefined;
- b.save(function(err) {
- assert.ifError(err);
- B.collection.findOne({ _id: b._id }, function(err, b) {
- assert.ifError(err);
- assert.strictEqual(undefined, b.meta);
- assert.strictEqual(undefined, b.comments);
- done();
- });
- });
- });
- });
- });
- });
+ await doc.save();
+ let b = await B.findById(doc._id);
+ assert.equal(b.title, 'css3');
+ assert.equal(b.author, 'aaron');
+ assert.equal(b.meta.date.toString(), doc.meta.date.toString());
+ assert.equal(b.meta.visitors.valueOf(), doc.meta.visitors.valueOf());
+ assert.equal(b.comments.length, 2);
+ assert.equal(b.comments[0].title, 'thanksgiving');
+ assert.equal(b.comments[0].body, 'yuuuumm');
+ assert.equal(b.comments[1].title, 'turkey');
+ assert.equal(b.comments[1].body, 'cranberries');
+ b.title = undefined;
+ b.author = null;
+ b.meta.date = undefined;
+ b.meta.visitors = null;
+ b.comments[0].title = null;
+ b.comments[0].body = undefined;
+ await b.save();
+ b = await B.findById(b._id);
+ assert.strictEqual(undefined, b.title);
+ assert.strictEqual(null, b.author);
+
+ assert.strictEqual(undefined, b.meta.date);
+ assert.strictEqual(null, b.meta.visitors);
+ assert.strictEqual(null, b.comments[0].title);
+ assert.strictEqual(undefined, b.comments[0].body);
+ assert.equal(b.comments[1].title, 'turkey');
+ assert.equal(b.comments[1].body, 'cranberries');
+
+ b.meta = undefined;
+ b.comments = undefined;
+ await b.save();
+ b = await B.collection.findOne({ _id: b._id });
+ assert.strictEqual(undefined, b.meta);
+ assert.strictEqual(undefined, b.comments);
+
});
describe('unsetting a default value', function() {
- it('should be ignored (gh-758)', function(done) {
+ it('should be ignored (gh-758)', async function() {
const M = db.model('Test', new Schema({ s: String, n: Number, a: Array }));
- M.collection.insertOne({}, function(err) {
- assert.ifError(err);
- M.findOne(function(err, m) {
- assert.ifError(err);
- m.s = m.n = m.a = undefined;
- assert.equal(m.$__delta(), undefined);
- done();
- });
- });
+ await M.collection.insertOne({});
+ const m = await M.findOne();
+ m.s = m.n = m.a = undefined;
+ assert.equal(m.$__delta(), undefined);
});
});
- it('allow for object passing to ref paths (gh-1606)', function(done) {
+ it('allow for object passing to ref paths (gh-1606)', function() {
const schA = new Schema({ title: String });
const schma = new Schema({
thing: { type: Schema.Types.ObjectId, ref: 'Test' },
@@ -3998,11 +2909,9 @@ describe('Model', function() {
assert.equal(thing.thing, a._id);
assert.equal(thing.subdoc.thing[0], a._id);
-
- done();
});
- it('setters trigger on null values (gh-1445)', function(done) {
+ it('setters trigger on null values (gh-1445)', function() {
const calls = [];
const OrderSchema = new Schema({
total: {
@@ -4020,7 +2929,7 @@ describe('Model', function() {
assert.deepEqual(calls, [0, null]);
assert.equal(o.total, 10);
- done();
+
});
describe('Skip setting default value for Geospatial-indexed fields (gh-1668)', function() {
@@ -4391,77 +3300,8 @@ describe('Model', function() {
});
});
- describe('insertMany()', function() {
- it('with timestamps (gh-723)', function() {
- const schema = new Schema({ name: String }, { timestamps: true });
- const Movie = db.model('Movie', schema);
- const start = Date.now();
-
- const arr = [{ name: 'Star Wars' }, { name: 'The Empire Strikes Back' }];
- return Movie.insertMany(arr).
- then(docs => {
- assert.equal(docs.length, 2);
- assert.ok(!docs[0].isNew);
- assert.ok(!docs[1].isNew);
- assert.ok(docs[0].createdAt.valueOf() >= start);
- assert.ok(docs[1].createdAt.valueOf() >= start);
- }).
- then(() => Movie.find()).
- then(docs => {
- assert.equal(docs.length, 2);
- assert.ok(docs[0].createdAt.valueOf() >= start);
- assert.ok(docs[1].createdAt.valueOf() >= start);
- });
- });
-
- it('timestamps respect $timestamps() (gh-12117)', async function() {
- const schema = new Schema({ name: String }, { timestamps: true });
- const Movie = db.model('Movie', schema);
- const start = Date.now();
-
- const arr = [
- new Movie({ name: 'Star Wars' }),
- new Movie({ name: 'The Empire Strikes Back' })
- ];
- arr[1].$timestamps(false);
-
- await Movie.insertMany(arr);
- const docs = await Movie.find().sort({ name: 1 });
- assert.ok(docs[0].createdAt.valueOf() >= start);
- assert.ok(!docs[1].createdAt);
- });
-
- it('insertMany() with nested timestamps (gh-12060)', async function() {
- const childSchema = new Schema({ name: { type: String } }, {
- _id: false,
- timestamps: true
- });
-
- const parentSchema = new Schema({ child: childSchema }, {
- timestamps: true
- });
-
- const Test = db.model('Test', parentSchema);
-
- await Test.insertMany([{ child: { name: 'test' } }]);
- let docs = await Test.find();
-
- assert.equal(docs.length, 1);
- assert.equal(docs[0].child.name, 'test');
- assert.ok(docs[0].child.createdAt);
- assert.ok(docs[0].child.updatedAt);
-
- await Test.insertMany([{ child: { name: 'test2' } }], { timestamps: false });
- docs = await Test.find({ 'child.name': 'test2' });
- assert.equal(docs.length, 1);
- assert.equal(docs[0].child.name, 'test2');
- assert.ok(!docs[0].child.createdAt);
- assert.ok(!docs[0].child.updatedAt);
- });
- });
-
describe('bug fixes', function() {
- it('doesnt crash (gh-1920)', function(done) {
+ it('doesnt crash (gh-1920)', async function() {
const parentSchema = new Schema({
children: [new Schema({
name: String
@@ -4472,14 +3312,9 @@ describe('Model', function() {
const parent = new Parent();
parent.children.push({ name: 'child name' });
- parent.save(function(err, it) {
- assert.ifError(err);
- parent.children.push({ name: 'another child' });
- Parent.findByIdAndUpdate(it._id, { $set: { children: parent.children } }, function(err) {
- assert.ifError(err);
- done();
- });
- });
+ await parent.save();
+ parent.children.push({ name: 'another child' });
+ await Parent.findByIdAndUpdate(parent._id, { $set: { children: parent.children } });
});
it('doesnt reset "modified" status for fields', async function() {
@@ -4521,683 +3356,59 @@ describe('Model', function() {
await Unique.collection.drop();
});
- it('insertMany() (gh-723)', function(done) {
+ it('deleteOne() with options (gh-7857)', async function() {
const schema = new Schema({
name: String
- }, { timestamps: true });
- const Movie = db.model('Movie', schema);
-
- const arr = [{ name: 'Star Wars' }, { name: 'The Empire Strikes Back' }];
- Movie.insertMany(arr, function(error, docs) {
- assert.ifError(error);
- assert.equal(docs.length, 2);
- assert.ok(!docs[0].isNew);
- assert.ok(!docs[1].isNew);
- assert.ok(docs[0].createdAt);
- assert.ok(docs[1].createdAt);
- assert.strictEqual(docs[0].__v, 0);
- assert.strictEqual(docs[1].__v, 0);
- Movie.find({}, function(error, docs) {
- assert.ifError(error);
- assert.equal(docs.length, 2);
- assert.ok(docs[0].createdAt);
- assert.ok(docs[1].createdAt);
- done();
- });
});
- });
-
- it('insertMany() ordered option for constraint errors (gh-3893)', async function() {
- const version = await start.mongodVersion();
+ const Character = db.model('Test', schema);
- const mongo34 = version[0] > 3 || (version[0] === 3 && version[1] >= 4);
- if (!mongo34) {
- return;
- }
+ const arr = [
+ { name: 'Tyrion Lannister' },
+ { name: 'Cersei Lannister' },
+ { name: 'Jon Snow' },
+ { name: 'Daenerys Targaryen' }
+ ];
+ await Character.insertMany(arr);
+ await Character.deleteOne({ name: 'Jon Snow' }, { w: 1 });
+ const docs = await Character.find();
+ assert.equal(docs.length, 3);
+ });
+ it('deleteMany() with options (gh-6805)', async function() {
const schema = new Schema({
- name: { type: String, unique: true }
+ name: String
});
- const Movie = db.model('Movie', schema);
+ const Character = db.model('Test', schema);
const arr = [
- { name: 'Star Wars' },
- { name: 'Star Wars' },
- { name: 'The Empire Strikes Back' }
+ { name: 'Tyrion Lannister' },
+ { name: 'Cersei Lannister' },
+ { name: 'Jon Snow' },
+ { name: 'Daenerys Targaryen' }
];
- await Movie.init();
-
- const error = await Movie.insertMany(arr, { ordered: false }).then(() => null, err => err);
-
- assert.equal(error.message.indexOf('E11000'), 0);
- assert.equal(error.results.length, 3);
- assert.equal(error.results[0].name, 'Star Wars');
- assert.ok(error.results[1].err);
- assert.ok(error.results[1].err.errmsg.includes('E11000'));
- assert.equal(error.results[2].name, 'The Empire Strikes Back');
- const docs = await Movie.find({}).sort({ name: 1 }).exec();
+ await Character.insertMany(arr);
+ await Character.deleteMany({ name: /Lannister/ }, { w: 1 });
+ const docs = await Character.find();
assert.equal(docs.length, 2);
- assert.equal(docs[0].name, 'Star Wars');
- assert.equal(docs[1].name, 'The Empire Strikes Back');
- await Movie.collection.drop();
});
- describe('insertMany() lean option to bypass validation (gh-8234)', () => {
- const gh8234Schema = new Schema({
- name: String,
- age: { type: Number, required: true }
- });
- const arrGh8234 = [{ name: 'Rigas' }, { name: 'Tonis', age: 9 }];
- let Gh8234;
- before('init model', () => {
- Gh8234 = db.model('Test', gh8234Schema);
-
- return Gh8234.deleteMany({});
- });
- afterEach('delete inserted data', function() {
- return Gh8234.deleteMany({});
- });
-
- it('insertMany() should bypass validation if lean option set to `true`', (done) => {
- Gh8234.insertMany(arrGh8234, { lean: true }, (error, docs) => {
- assert.ifError(error);
- assert.equal(docs.length, 2);
- Gh8234.find({}, (error, docs) => {
- assert.ifError(error);
- assert.equal(docs.length, 2);
- assert.equal(arrGh8234[0].age, undefined);
- assert.equal(arrGh8234[1].age, 9);
- done();
- });
- });
- });
-
- it('insertMany() should validate if lean option not set', (done) => {
- Gh8234.insertMany(arrGh8234, (error) => {
- assert.ok(error);
- assert.equal(error.name, 'ValidationError');
- assert.equal(error.errors.age.kind, 'required');
- done();
- });
- });
-
- it('insertMany() should validate if lean option set to `false`', (done) => {
- Gh8234.insertMany(arrGh8234, { lean: false }, (error) => {
- assert.ok(error);
- assert.equal(error.name, 'ValidationError');
- assert.equal(error.errors.age.kind, 'required');
- done();
- });
- });
- });
-
- it('insertMany() ordered option for validation errors (gh-5068)', async function() {
- const version = await start.mongodVersion();
-
- const mongo34 = version[0] > 3 || (version[0] === 3 && version[1] >= 4);
- if (!mongo34) {
- return;
- }
-
- const schema = new Schema({
- name: { type: String, required: true }
- });
- const Movie = db.model('Movie', schema);
-
- const arr = [
- { name: 'Star Wars' },
- { foo: 'Star Wars' },
- { name: 'The Empire Strikes Back' }
- ];
- await Movie.insertMany(arr, { ordered: false });
-
- const docs = await Movie.find({}).sort({ name: 1 }).exec();
- assert.equal(docs.length, 2);
- assert.equal(docs[0].name, 'Star Wars');
- assert.equal(docs[1].name, 'The Empire Strikes Back');
- });
-
- it('insertMany() `writeErrors` if only one error (gh-8938)', async function() {
- const QuestionType = new mongoose.Schema({
- code: { type: String, required: true, unique: true },
- text: String
- });
- const Question = db.model('Test', QuestionType);
-
-
- await Question.init();
-
- await Question.create({ code: 'MEDIUM', text: '123' });
- const data = [
- { code: 'MEDIUM', text: '1111' },
- { code: 'test', text: '222' },
- { code: 'HARD', text: '2222' }
- ];
- const opts = { ordered: false, rawResult: true };
- let err = await Question.insertMany(data, opts).catch(err => err);
- assert.ok(Array.isArray(err.writeErrors));
- assert.equal(err.writeErrors.length, 1);
- assert.equal(err.insertedDocs.length, 2);
- assert.equal(err.insertedDocs[0].code, 'test');
- assert.equal(err.insertedDocs[1].code, 'HARD');
-
- assert.equal(err.results.length, 3);
- assert.ok(err.results[0].err.errmsg.includes('E11000'));
- assert.equal(err.results[1].code, 'test');
- assert.equal(err.results[2].code, 'HARD');
-
- await Question.deleteMany({});
- await Question.create({ code: 'MEDIUM', text: '123' });
- await Question.create({ code: 'HARD', text: '123' });
-
- err = await Question.insertMany(data, opts).catch(err => err);
- assert.ok(Array.isArray(err.writeErrors));
- assert.equal(err.writeErrors.length, 2);
- assert.equal(err.insertedDocs.length, 1);
- assert.equal(err.insertedDocs[0].code, 'test');
-
- });
-
- it('insertMany() ordered option for single validation error', async function() {
- const version = start.mongodVersion();
-
- const mongo34 = version[0] > 3 || (version[0] === 3 && version[1] >= 4);
- if (!mongo34) {
- return;
- }
-
- const schema = new Schema({
- name: { type: String, required: true }
- });
- const Movie = db.model('Movie', schema);
-
- const arr = [
- { foo: 'Star Wars' },
- { foo: 'The Fast and the Furious' }
- ];
- await Movie.insertMany(arr, { ordered: false });
-
- const docs = await Movie.find({}).sort({ name: 1 }).exec();
-
- assert.equal(docs.length, 0);
- });
-
- it('insertMany() hooks (gh-3846)', function(done) {
- const schema = new Schema({
- name: String
- });
- let calledPre = 0;
- let calledPost = 0;
- schema.pre('insertMany', function(next, docs) {
- assert.equal(docs.length, 2);
- assert.equal(docs[0].name, 'Star Wars');
- ++calledPre;
- next();
- });
- schema.pre('insertMany', function(next, docs) {
- assert.equal(docs.length, 2);
- assert.equal(docs[0].name, 'Star Wars');
- docs[0].name = 'A New Hope';
- ++calledPre;
- next();
- });
- schema.post('insertMany', function() {
- ++calledPost;
- });
- const Movie = db.model('Movie', schema);
-
- const arr = [{ name: 'Star Wars' }, { name: 'The Empire Strikes Back' }];
- Movie.insertMany(arr, function(error, docs) {
- assert.ifError(error);
- assert.equal(docs.length, 2);
- assert.equal(calledPre, 2);
- assert.equal(calledPost, 1);
- Movie.find({}).sort({ name: 1 }).exec(function(error, docs) {
- assert.ifError(error);
- assert.equal(docs[0].name, 'A New Hope');
- assert.equal(docs[1].name, 'The Empire Strikes Back');
- done();
- });
- });
- });
-
- it('returns empty array if no documents (gh-8130)', function() {
- const Movie = db.model('Movie', Schema({ name: String }));
- return Movie.insertMany([]).then(docs => assert.deepEqual(docs, []));
- });
-
- it('insertMany() multi validation error with ordered false (gh-5337)', function(done) {
- const schema = new Schema({
- name: { type: String, required: true }
- });
- const Movie = db.model('Movie', schema);
-
- const arr = [
- { foo: 'The Phantom Menace' },
- { name: 'Star Wars' },
- { name: 'The Empire Strikes Back' },
- { foobar: 'The Force Awakens' }
- ];
- const opts = { ordered: false, rawResult: true };
- Movie.insertMany(arr, opts, function(error, res) {
- assert.ifError(error);
- assert.equal(res.mongoose.validationErrors.length, 2);
- assert.equal(res.mongoose.validationErrors[0].name, 'ValidationError');
- assert.equal(res.mongoose.validationErrors[1].name, 'ValidationError');
- done();
- });
- });
-
- it('insertMany() validation error with ordered true when all documents are invalid', function(done) {
- const schema = new Schema({
- name: { type: String, required: true }
- });
- const Movie = db.model('Movie', schema);
-
- const arr = [
- { foo: 'The Phantom Menace' },
- { foobar: 'The Force Awakens' }
- ];
- const opts = { ordered: true };
- Movie.insertMany(arr, opts, function(error, res) {
- assert.ok(error);
- assert.equal(res, undefined);
- assert.equal(error.name, 'ValidationError');
- done();
- });
- });
-
- it('insertMany() validation error with ordered false when all documents are invalid', function(done) {
- const schema = new Schema({
- name: { type: String, required: true }
- });
- const Movie = db.model('Movie', schema);
-
- const arr = [
- { foo: 'The Phantom Menace' },
- { foobar: 'The Force Awakens' }
- ];
- const opts = { ordered: false };
- Movie.insertMany(arr, opts, function(error, res) {
- assert.ifError(error);
- assert.equal(res.length, 0);
- assert.equal(error, null);
- done();
- });
- });
-
- it('insertMany() validation error with ordered false and rawResult for checking which documents failed (gh-12791)', async function() {
- const schema = new Schema({
- name: { type: String, required: true },
- year: { type: Number, required: true }
- });
- const Movie = db.model('Movie', schema);
-
- const id1 = new mongoose.Types.ObjectId();
- const id2 = new mongoose.Types.ObjectId();
- const id3 = new mongoose.Types.ObjectId();
- const arr = [
- { _id: id1, foo: 'The Phantom Menace', year: 1999 },
- { _id: id2, name: 'The Force Awakens', bar: 2015 },
- { _id: id3, name: 'The Empire Strikes Back', year: 1980 }
- ];
- const opts = { ordered: false, rawResult: true };
- const res = await Movie.insertMany(arr, opts);
- // {
- // acknowledged: true,
- // insertedCount: 1,
- // insertedIds: { '0': new ObjectId("63b34b062cfe38622738e510") },
- // mongoose: { validationErrors: [ [Error], [Error] ] }
- // }
- assert.equal(res.insertedCount, 1);
- assert.equal(res.insertedIds[0].toHexString(), id3.toHexString());
- assert.equal(res.mongoose.validationErrors.length, 2);
- assert.ok(res.mongoose.validationErrors[0].errors['name']);
- assert.ok(!res.mongoose.validationErrors[0].errors['year']);
- assert.ok(res.mongoose.validationErrors[1].errors['year']);
- assert.ok(!res.mongoose.validationErrors[1].errors['name']);
-
- assert.equal(res.mongoose.results.length, 3);
- assert.ok(res.mongoose.results[0].errors['name']);
- assert.ok(res.mongoose.results[1].errors['year']);
- assert.ok(res.mongoose.results[2].$__);
- assert.equal(res.mongoose.results[2].name, 'The Empire Strikes Back');
- });
-
- it('insertMany() validation error with ordered false and rawResult for mixed write and validation error (gh-12791)', async function() {
- const schema = new Schema({
- name: { type: String, required: true, unique: true },
- year: { type: Number, required: true }
- });
- const Movie = db.model('Movie', schema);
- await Movie.init();
-
- const arr = [
- { foo: 'The Phantom Menace', year: 1999 },
- { name: 'The Force Awakens', bar: 2015 },
- { name: 'The Empire Strikes Back', year: 1980 },
- { name: 'The Empire Strikes Back', year: 1980 }
- ];
- const opts = { ordered: false, rawResult: true };
- const err = await Movie.insertMany(arr, opts).then(() => null, err => err);
-
- assert.ok(err);
- assert.equal(err.insertedDocs.length, 1);
- assert.equal(err.insertedDocs[0].name, 'The Empire Strikes Back');
- assert.equal(err.writeErrors.length, 1);
- assert.equal(err.writeErrors[0].index, 3);
- assert.equal(err.mongoose.validationErrors.length, 2);
- assert.ok(err.mongoose.validationErrors[0].errors['name']);
- assert.ok(!err.mongoose.validationErrors[0].errors['year']);
- assert.ok(err.mongoose.validationErrors[1].errors['year']);
- assert.ok(!err.mongoose.validationErrors[1].errors['name']);
-
- assert.equal(err.mongoose.results.length, 4);
- assert.ok(err.mongoose.results[0].errors['name']);
- assert.ok(err.mongoose.results[1].errors['year']);
- assert.ok(err.mongoose.results[2].$__);
- assert.equal(err.mongoose.results[2].name, 'The Empire Strikes Back');
- assert.ok(err.mongoose.results[3].err);
- });
-
- it('insertMany() populate option (gh-9720)', async function() {
- const schema = new Schema({
- name: { type: String, required: true }
- });
- const Movie = db.model('Movie', schema);
- const Person = db.model('Person', Schema({
- name: String,
- favoriteMovie: {
- type: 'ObjectId',
- ref: 'Movie'
- }
- }));
-
-
- const movies = await Movie.create([
- { name: 'The Empire Strikes Back' },
- { name: 'Jingle All The Way' }
- ]);
- const people = await Person.insertMany([
- { name: 'Test1', favoriteMovie: movies[1]._id },
- { name: 'Test2', favoriteMovie: movies[0]._id }
- ], { populate: 'favoriteMovie' });
-
- assert.equal(people.length, 2);
- assert.equal(people[0].favoriteMovie.name, 'Jingle All The Way');
- assert.equal(people[1].favoriteMovie.name, 'The Empire Strikes Back');
-
- });
-
- it('insertMany() sets `isNew` for inserted documents with `ordered = false` (gh-9677)', async function() {
- const schema = new Schema({
- title: { type: String, required: true, unique: true }
- });
- const Movie = db.model('Movie', schema);
-
- const arr = [{ title: 'The Phantom Menace' }, { title: 'The Phantom Menace' }];
- const opts = { ordered: false };
-
- await Movie.init();
- const err = await Movie.insertMany(arr, opts).then(() => err, err => err);
- assert.ok(err);
- assert.ok(err.insertedDocs);
-
- assert.equal(err.insertedDocs.length, 1);
- assert.strictEqual(err.insertedDocs[0].isNew, false);
-
- });
-
- it('insertMany() returns only inserted docs with `ordered = true`', async function() {
- const schema = new Schema({
- name: { type: String, required: true, unique: true }
- });
- const Movie = db.model('Movie', schema);
-
- const arr = [
- { name: 'The Phantom Menace' },
- { name: 'The Empire Strikes Back' },
- { name: 'The Phantom Menace' },
- { name: 'Jingle All The Way' }
- ];
- const opts = { ordered: true };
-
- await Movie.init();
- const err = await Movie.insertMany(arr, opts).then(() => err, err => err);
- assert.ok(err);
- assert.ok(err.insertedDocs);
-
- assert.equal(err.insertedDocs.length, 2);
- assert.strictEqual(err.insertedDocs[0].name, 'The Phantom Menace');
- assert.strictEqual(err.insertedDocs[1].name, 'The Empire Strikes Back');
-
- });
-
- it('insertMany() validation error with ordered true and rawResult true when all documents are invalid', function(done) {
- const schema = new Schema({
- name: { type: String, required: true }
- });
- const Movie = db.model('Movie', schema);
-
- const arr = [
- { foo: 'The Phantom Menace' },
- { foobar: 'The Force Awakens' }
- ];
- const opts = { ordered: true, rawResult: true };
- Movie.insertMany(arr, opts, function(error, res) {
- assert.ok(error);
- assert.equal(res, undefined);
- assert.equal(error.name, 'ValidationError');
- done();
- });
- });
-
- it('insertMany() validation error with ordered false and rawResult true when all documents are invalid', function(done) {
- const schema = new Schema({
- name: { type: String, required: true }
- });
- const Movie = db.model('Movie', schema);
-
- const arr = [
- { foo: 'The Phantom Menace' },
- { foobar: 'The Force Awakens' }
- ];
- const opts = { ordered: false, rawResult: true };
- Movie.insertMany(arr, opts, function(error, res) {
- assert.ifError(error);
- assert.equal(res.mongoose.validationErrors.length, 2);
- assert.equal(res.mongoose.validationErrors[0].name, 'ValidationError');
- assert.equal(res.mongoose.validationErrors[1].name, 'ValidationError');
- done();
- });
- });
-
- it('insertMany() depopulate (gh-4590)', function(done) {
- const personSchema = new Schema({
- name: String
- });
- const movieSchema = new Schema({
- name: String,
- leadActor: {
- type: Schema.Types.ObjectId,
- ref: 'gh4590'
- }
- });
-
- const Person = db.model('Person', personSchema);
- const Movie = db.model('Movie', movieSchema);
-
- const arnold = new Person({ name: 'Arnold Schwarzenegger' });
- const movies = [{ name: 'Predator', leadActor: arnold }];
- Movie.insertMany(movies, function(error, docs) {
- assert.ifError(error);
- assert.equal(docs.length, 1);
- Movie.findOne({ name: 'Predator' }, function(error, doc) {
- assert.ifError(error);
- assert.equal(doc.leadActor.toHexString(), arnold._id.toHexString());
- done();
- });
- });
- });
-
- it('insertMany() with promises (gh-4237)', function(done) {
- const schema = new Schema({
- name: String
- });
- const Movie = db.model('Movie', schema);
-
- const arr = [{ name: 'Star Wars' }, { name: 'The Empire Strikes Back' }];
- Movie.insertMany(arr).then(function(docs) {
- assert.equal(docs.length, 2);
- assert.ok(!docs[0].isNew);
- assert.ok(!docs[1].isNew);
- Movie.find({}, function(error, docs) {
- assert.ifError(error);
- assert.equal(docs.length, 2);
- done();
- });
- });
- });
-
- it('insertMany() with error handlers (gh-6228)', async function() {
- const schema = new Schema({
- name: { type: String, unique: true }
- }, { autoIndex: false });
-
- let postCalled = 0;
- let postErrorCalled = 0;
- schema.post('insertMany', (doc, next) => {
- ++postCalled;
- next();
- });
-
- schema.post('insertMany', (err, doc, next) => {
- ++postErrorCalled;
- next(err);
- });
-
- const Movie = db.model('Movie', schema);
-
-
- await Movie.createIndexes();
-
- let threw = false;
- try {
- await Movie.insertMany([
- { name: 'Star Wars' },
- { name: 'Star Wars' }
- ]);
- } catch (error) {
- assert.ok(error);
- threw = true;
- }
-
- assert.ok(threw);
- assert.equal(postCalled, 0);
- assert.equal(postErrorCalled, 1);
-
- await Movie.collection.drop();
-
- });
-
- it('insertMany() with non object array error can be catched (gh-8363)', function(done) {
- const schema = mongoose.Schema({
- _id: mongoose.Schema.Types.ObjectId,
- url: { type: String }
- });
- const Image = db.model('Test', schema);
- Image.insertMany(['a', 'b', 'c']).catch((error) => {
- assert.equal(error.name, 'ObjectParameterError');
- done();
- });
- });
-
- it('insertMany() return docs with empty modifiedPaths (gh-7852)', async function() {
- const schema = new Schema({
- name: { type: String }
- });
-
- const Food = db.model('Test', schema);
-
-
- const foods = await Food.insertMany([
- { name: 'Rice dumplings' },
- { name: 'Beef noodle' }
- ]);
- assert.equal(foods[0].modifiedPaths().length, 0);
- assert.equal(foods[1].modifiedPaths().length, 0);
-
- });
-
- it('deleteOne() with options (gh-7857)', function(done) {
- const schema = new Schema({
- name: String
- });
- const Character = db.model('Test', schema);
-
- const arr = [
- { name: 'Tyrion Lannister' },
- { name: 'Cersei Lannister' },
- { name: 'Jon Snow' },
- { name: 'Daenerys Targaryen' }
- ];
- Character.insertMany(arr, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 4);
- Character.deleteOne({ name: 'Jon Snow' }, { w: 1 }, function(err) {
- assert.ifError(err);
- Character.find({}, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 3);
- done();
- });
- });
- });
- });
-
- it('deleteMany() with options (gh-6805)', function(done) {
- const schema = new Schema({
- name: String
- });
- const Character = db.model('Test', schema);
-
- const arr = [
- { name: 'Tyrion Lannister' },
- { name: 'Cersei Lannister' },
- { name: 'Jon Snow' },
- { name: 'Daenerys Targaryen' }
- ];
- Character.insertMany(arr, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 4);
- Character.deleteMany({ name: /Lannister/ }, { w: 1 }, function(err) {
- assert.ifError(err);
- Character.find({}, function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 2);
- done();
- });
- });
- });
- });
-
- it('run default function with correct this scope in DocumentArray (gh-6840)', function() {
- const schema = new Schema({
- title: String,
- actors: {
- type: [{ name: String, character: String }],
- default: function() {
- // `this` should be root document and has initial data
- if (this.title === 'Passengers') {
- return [
- { name: 'Jennifer Lawrence', character: 'Aurora Lane' },
- { name: 'Chris Pratt', character: 'Jim Preston' }
- ];
- }
- return [];
- }
- }
+ it('run default function with correct this scope in DocumentArray (gh-6840)', function() {
+ const schema = new Schema({
+ title: String,
+ actors: {
+ type: [{ name: String, character: String }],
+ default: function() {
+ // `this` should be root document and has initial data
+ if (this.title === 'Passengers') {
+ return [
+ { name: 'Jennifer Lawrence', character: 'Aurora Lane' },
+ { name: 'Chris Pratt', character: 'Jim Preston' }
+ ];
+ }
+ return [];
+ }
+ }
});
const Movie = db.model('Movie', schema);
@@ -5206,15 +3417,6 @@ describe('Model', function() {
});
describe('3.6 features', function() {
- before(async function() {
- const version = await start.mongodVersion();
- const mongo36 = version[0] > 3 || (version[0] === 3 && version[1] >= 6);
-
- if (!mongo36) {
- this.skip();
- }
- });
-
it('arrayFilter (gh-5965)', async function() {
const MyModel = db.model('Test', new Schema({
@@ -5297,7 +3499,7 @@ describe('Model', function() {
let listener;
before(function() {
- if (!process.env.REPLICA_SET) {
+ if (!process.env.REPLICA_SET && !process.env.START_REPLICA_SET) {
this.skip();
}
});
@@ -5308,6 +3510,9 @@ describe('Model', function() {
}
changeStream.removeListener('change', listener);
listener = null;
+ // Change stream may still emit "MongoAPIError: ChangeStream is closed" because change stream
+ // may still poll after close.
+ changeStream.on('error', () => {});
changeStream.close();
changeStream = null;
});
@@ -5315,7 +3520,7 @@ describe('Model', function() {
it('watch() (gh-5964)', async function() {
const MyModel = db.model('Test', new Schema({ name: String }));
- const changed = new global.Promise(resolve => {
+ const changed = new Promise(resolve => {
changeStream = MyModel.watch();
listener = data => resolve(data);
changeStream.once('change', listener);
@@ -5329,6 +3534,20 @@ describe('Model', function() {
doc._id.toHexString());
});
+ it('bubbles up resumeTokenChanged events (gh-13607)', async function() {
+ const MyModel = db.model('Test', new Schema({ name: String }));
+
+ const resumeTokenChangedEvent = new Promise(resolve => {
+ changeStream = MyModel.watch();
+ listener = data => resolve(data);
+ changeStream.once('resumeTokenChanged', listener);
+ });
+
+ await MyModel.create({ name: 'test' });
+ const { _data } = await resumeTokenChangedEvent;
+ assert.ok(_data);
+ });
+
it('using next() and hasNext() (gh-11527)', async function() {
const MyModel = db.model('Test', new Schema({ name: String }));
@@ -5346,14 +3565,54 @@ describe('Model', function() {
it('fullDocument (gh-11936)', async function() {
const MyModel = db.model('Test', new Schema({ name: String }));
+ const doc = await MyModel.create({ name: 'Ned Stark' });
const changeStream = await MyModel.watch([], {
fullDocument: 'updateLookup',
hydrate: true
});
+ await changeStream.$driverChangeStreamPromise;
+
+ const p = new Promise((resolve) => {
+ changeStream.once('change', change => {
+ resolve(change);
+ });
+ });
+ // Need to wait for resume token to be set after the event listener,
+ // otherwise change stream might not pick up the update.
+ await once(changeStream.driverChangeStream, 'resumeTokenChanged');
+ await MyModel.updateOne({ _id: doc._id }, { name: 'Tony Stark' });
+
+ const changeData = await p;
+ assert.equal(changeData.operationType, 'update');
+ assert.equal(changeData.fullDocument._id.toHexString(),
+ doc._id.toHexString());
+ assert.ok(changeData.fullDocument.$__);
+ assert.equal(changeData.fullDocument.get('name'), 'Tony Stark');
+
+ await changeStream.close();
+ });
+
+ it('fullDocument with immediate watcher and hydrate (gh-14049)', async function() {
+ const MyModel = db.model('Test', new Schema({ name: String }));
const doc = await MyModel.create({ name: 'Ned Stark' });
- const p = changeStream.next();
+ let changeStream = null;
+ const p = new Promise((resolve) => {
+ changeStream = MyModel.watch([], {
+ fullDocument: 'updateLookup',
+ hydrate: true
+ });
+
+ changeStream.on('change', change => {
+ resolve(change);
+ });
+ });
+
+ // Need to wait for cursor to be initialized and for resume token to
+ // be set, otherwise change stream might not pick up the update.
+ await changeStream.$driverChangeStreamPromise;
+ await once(changeStream.driverChangeStream, 'resumeTokenChanged');
await MyModel.updateOne({ _id: doc._id }, { name: 'Tony Stark' });
const changeData = await p;
@@ -5362,13 +3621,15 @@ describe('Model', function() {
doc._id.toHexString());
assert.ok(changeData.fullDocument.$__);
assert.equal(changeData.fullDocument.get('name'), 'Tony Stark');
+
+ await changeStream.close();
});
it('respects discriminators (gh-11007)', async function() {
const BaseModel = db.model('Test', new Schema({ name: String }));
const ChildModel = BaseModel.discriminator('Test1', new Schema({ email: String }));
- const changed = new global.Promise(resolve => {
+ const changed = new Promise(resolve => {
changeStream = ChildModel.watch();
listener = data => resolve(data);
changeStream.once('change', listener);
@@ -5377,10 +3638,97 @@ describe('Model', function() {
await BaseModel.create({ name: 'Base' });
await ChildModel.create({ name: 'Child', email: 'test' });
- const changeData = await changed;
- // Should get change data from 2nd insert, skipping first insert
- assert.equal(changeData.operationType, 'insert');
- assert.equal(changeData.fullDocument.name, 'Child');
+ const changeData = await changed;
+ // Should get change data from 2nd insert, skipping first insert
+ assert.equal(changeData.operationType, 'insert');
+ assert.equal(changeData.fullDocument.name, 'Child');
+ });
+
+ it('watch() before connecting (gh-5964)', async function() {
+ const db = start();
+
+ const MyModel = db.model('Test5964', new Schema({ name: String }));
+
+ // Synchronous, before connection happens
+ const changeStream = MyModel.watch();
+ const changed = new Promise(resolve => {
+ changeStream.once('change', data => resolve(data));
+ });
+
+ await db;
+ await MyModel.create({ name: 'Ned Stark' });
+
+ const changeData = await changed;
+ assert.equal(changeData.operationType, 'insert');
+ assert.equal(changeData.fullDocument.name, 'Ned Stark');
+
+ // Change stream may still emit "MongoAPIError: ChangeStream is closed" because change stream
+ // may still poll after close.
+ changeStream.on('error', () => {});
+ await changeStream.close();
+ await db.close();
+ });
+
+ it('watch() close() prevents buffered watch op from running (gh-7022)', async function() {
+ const db = start();
+ const MyModel = db.model('Test', new Schema({}));
+ const changeStream = MyModel.watch();
+ const ready = new Promise(resolve => {
+ changeStream.once('data', () => {
+ resolve(true);
+ });
+ setTimeout(resolve, 500, false);
+ });
+
+ // Change stream may still emit "MongoAPIError: ChangeStream is closed" because change stream
+ // may still poll after close.
+ changeStream.on('error', () => {});
+
+ const close = changeStream.close();
+ await db.asPromise();
+ const readyCalled = await ready;
+ assert.strictEqual(readyCalled, false);
+
+ await close;
+ await db.close();
+ });
+
+ it('watch() close() closes the stream (gh-7022)', async function() {
+ const db = await start();
+ const MyModel = db.model('Test', new Schema({ name: String }));
+
+ await MyModel.init();
+
+ const changeStream = MyModel.watch();
+ const closed = new Promise(resolve => {
+ changeStream.once('close', () => resolve(true));
+ });
+
+ await MyModel.create({ name: 'Hodor' });
+
+ // Change stream may still emit "MongoAPIError: ChangeStream is closed" because change stream
+ // may still poll after close.
+ changeStream.on('error', () => {});
+
+ changeStream.close();
+ const closedData = await closed;
+ assert.strictEqual(closedData, true);
+
+ await db.close();
+ });
+
+ it('bubbles up resumeTokenChanged events (gh-14349)', async function() {
+ const MyModel = db.model('Test', new Schema({ name: String }));
+
+ const resumeTokenChangedEvent = new Promise(resolve => {
+ changeStream = MyModel.watch();
+ listener = data => resolve(data);
+ changeStream.once('resumeTokenChanged', listener);
+ });
+
+ await MyModel.create({ name: 'test' });
+ const { _data } = await resumeTokenChangedEvent;
+ assert.ok(_data);
});
});
@@ -5565,7 +3913,7 @@ describe('Model', function() {
});
});
- it('method with same name as prop should throw (gh-4475)', function(done) {
+ it('method with same name as prop should throw (gh-4475)', function() {
const testSchema = new mongoose.Schema({
isPaid: Boolean
});
@@ -5582,52 +3930,21 @@ describe('Model', function() {
'your schema both named "isPaid"');
}
assert.ok(threw);
- done();
- });
-
- it('emits errors in create cb (gh-3222) (gh-3478)', function(done) {
- const schema = new Schema({ name: 'String' });
- const Movie = db.model('Movie', schema);
-
- Movie.on('error', function(error) {
- assert.equal(error.message, 'fail!');
- done();
- });
- Movie.create({ name: 'Conan the Barbarian' }, function(error) {
- assert.ifError(error);
- throw new Error('fail!');
- });
});
- it('create() reuses existing doc if one passed in (gh-4449)', function(done) {
+ it('create() reuses existing doc if one passed in (gh-4449)', async function() {
const testSchema = new mongoose.Schema({
name: String
});
const Test = db.model('Test', testSchema);
const t = new Test();
- Test.create(t, function(error, t2) {
- assert.ifError(error);
- assert.ok(t === t2);
- done();
- });
- });
-
- it('emits errors correctly from exec (gh-4500)', function(done) {
- const someModel = db.model('Test', new Schema({}));
-
- someModel.on('error', function(error) {
- assert.equal(error.message, 'This error will not disappear');
- done();
- });
-
- someModel.findOne().exec(function() {
- throw new Error('This error will not disappear');
- });
+ const t2 = await Test.create(t);
+ assert.ok(t === t2);
});
- it('disabling id getter with .set() (gh-5548)', function(done) {
+ it('disabling id getter with .set() (gh-5548)', function() {
const ChildSchema = new mongoose.Schema({
name: String,
_id: false
@@ -5652,10 +3969,10 @@ describe('Model', function() {
assert.ok(!('id' in obj));
assert.ok(!('id' in obj.child));
- done();
+
});
- it('creates new array when initializing from existing doc (gh-4449)', function(done) {
+ it('creates new array when initializing from existing doc (gh-4449)', async function() {
const TodoSchema = new mongoose.Schema({
title: String
}, { _id: false });
@@ -5666,34 +3983,23 @@ describe('Model', function() {
});
const User = db.model('User', UserSchema);
- const val = new User({ name: 'Val' });
- User.create(val, function(error, val) {
- assert.ifError(error);
- val.todos.push({ title: 'Groceries' });
- val.save(function(error) {
- assert.ifError(error);
- User.findById(val, function(error, val) {
- assert.ifError(error);
- assert.deepEqual(val.toObject().todos, [{ title: 'Groceries' }]);
- const u2 = new User();
- val.todos = u2.todos;
- val.todos.push({ title: 'Cook' });
- val.save(function(error) {
- assert.ifError(error);
- User.findById(val, function(error, val) {
- assert.ifError(error);
- assert.equal(val.todos.length, 1);
- assert.equal(val.todos[0].title, 'Cook');
- done();
- });
- });
- });
- });
- });
+ let val = new User({ name: 'Val' });
+ await User.create(val);
+ val.todos.push({ title: 'Groceries' });
+ await val.save();
+ val = await User.findById(val);
+ assert.deepEqual(val.toObject().todos, [{ title: 'Groceries' }]);
+ const u2 = new User();
+ val.todos = u2.todos;
+ val.todos.push({ title: 'Cook' });
+ await val.save();
+ val = await User.findById(val);
+ assert.equal(val.todos.length, 1);
+ assert.equal(val.todos[0].title, 'Cook');
});
describe('bulkWrite casting', function() {
- it('basic casting (gh-3998)', function(done) {
+ it('basic casting (gh-3998)', async function() {
const schema = new Schema({
str: String,
num: Number
@@ -5716,18 +4022,13 @@ describe('Model', function() {
}
}
];
- M.bulkWrite(ops, function(error) {
- assert.ifError(error);
- M.findOne({}, function(error, doc) {
- assert.ifError(error);
- assert.strictEqual(doc.str, '1');
- assert.strictEqual(doc.num, 2);
- done();
- });
- });
+ await M.bulkWrite(ops);
+ const doc = await M.findOne({});
+ assert.strictEqual(doc.str, '1');
+ assert.strictEqual(doc.num, 2);
});
- it('setDefaultsOnInsert (gh-5708)', function(done) {
+ it('setDefaultsOnInsert (gh-5708)', async function() {
const schema = new Schema({
str: { type: String, default: 'test' },
num: Number
@@ -5746,15 +4047,11 @@ describe('Model', function() {
}
}
];
- M.bulkWrite(ops, function(error) {
- assert.ifError(error);
- M.findOne({}).lean().exec(function(error, doc) {
- assert.ifError(error);
- assert.strictEqual(doc.str, 'test');
- assert.strictEqual(doc.num, 1);
- done();
- });
- });
+ await M.bulkWrite(ops);
+ const doc = await M.findOne({}).lean().exec();
+ assert.strictEqual(doc.str, 'test');
+ assert.strictEqual(doc.num, 1);
+
});
it('timestamps (gh-5708)', async function() {
@@ -5800,7 +4097,126 @@ describe('Model', function() {
assert.ok(doc.createdAt.valueOf() >= now.valueOf());
assert.ok(doc.updatedAt);
assert.ok(doc.updatedAt.valueOf() >= now.valueOf());
+ });
+
+ it('throwOnValidationError (gh-14572)', async function() {
+ const schema = new Schema({
+ num: Number
+ });
+
+ const M = db.model('Test', schema);
+
+ const ops = [
+ {
+ insertOne: {
+ document: {
+ num: 'not a number'
+ }
+ }
+ }
+ ];
+
+ const err = await M.bulkWrite(
+ ops,
+ { ordered: false, throwOnValidationError: true }
+ ).then(() => null, err => err);
+ assert.ok(err);
+ assert.equal(err.name, 'MongooseBulkWriteError');
+ assert.equal(err.validationErrors[0].errors['num'].name, 'CastError');
+ });
+
+ it('handles array filters (gh-14978)', async function() {
+ const embedDiscriminatorSchema = new mongoose.Schema({
+ field1: String
+ });
+
+ const embedSchema = new mongoose.Schema({
+ field: String,
+ key: String
+ }, { discriminatorKey: 'key' });
+ embedSchema.discriminator('Type1', embedDiscriminatorSchema);
+
+ const testSchema = new mongoose.Schema({
+ testArray: [embedSchema]
+ });
+ const TestModel = db.model('Test', testSchema);
+
+ const test = new TestModel({
+ testArray: [{
+ key: 'Type1',
+ field: 'field',
+ field1: 'field1'
+ }]
+ });
+ const r1 = await test.save();
+ assert.equal(r1.testArray[0].field1, 'field1');
+
+ const field1update = 'field1 update';
+ await TestModel.bulkWrite([{
+ updateOne: {
+ filter: { _id: r1._id },
+ update: {
+ $set: {
+ 'testArray.$[element].field1': field1update,
+ 'testArray.$[element].nonexistentProp': field1update
+ }
+ },
+ arrayFilters: [
+ {
+ 'element._id': r1.testArray[0]._id,
+ 'element.key': 'Type1'
+ }
+ ]
+ }
+ }]);
+ const r2 = await TestModel.findById(r1._id).lean();
+ assert.equal(r2.testArray[0].field1, field1update);
+ assert.strictEqual(r2.testArray[0].nonexistentProp, undefined);
+ });
+
+ it('handles overwriteDiscriminatorKey (gh-15040)', async function() {
+ const dSchema1 = new mongoose.Schema({
+ field1: String
+ });
+ const dSchema2 = new mongoose.Schema({
+ field2: String
+ });
+ const baseSchema = new mongoose.Schema({
+ field: String,
+ key: String
+ }, { discriminatorKey: 'key' });
+ const type1Key = 'Type1';
+ const type2Key = 'Type2';
+
+ baseSchema.discriminator(type1Key, dSchema1);
+ baseSchema.discriminator(type2Key, dSchema2);
+
+ const TestModel = db.model('Test', baseSchema);
+
+ const test = new TestModel({
+ field: 'base field',
+ key: type1Key,
+ field1: 'field1'
+ });
+ const r1 = await test.save();
+ assert.equal(r1.field1, 'field1');
+ assert.equal(r1.key, type1Key);
+
+ const field2 = 'field2';
+ await TestModel.bulkWrite([{
+ updateOne: {
+ filter: { _id: r1._id },
+ update: {
+ key: type2Key,
+ field2
+ },
+ overwriteDiscriminatorKey: true
+ }
+ }]);
+ const r2 = await TestModel.findById(r1._id);
+ assert.equal(r2.key, type2Key);
+ assert.equal(r2.field2, field2);
});
it('with child timestamps and array filters (gh-7032)', async function() {
@@ -5829,7 +4245,14 @@ describe('Model', function() {
const doc = await Parent.findOne();
assert.ok(doc.children[0].updatedAt.valueOf() > end);
+ });
+ it('throws readable error if invalid op', async function() {
+ const Test = db.model('Test', Schema({ name: String }));
+ await assert.rejects(
+ () => Test.bulkWrite([Promise.resolve(42)]),
+ 'MongooseError: Invalid op passed to `bulkWrite()`: Promise { 42 }'
+ );
});
it('with timestamps and replaceOne (gh-5708)', async function() {
@@ -5837,7 +4260,6 @@ describe('Model', function() {
const M = db.model('Test', schema);
-
await M.create({ num: 42 });
await new Promise((resolve) => setTimeout(resolve, 10));
@@ -5855,7 +4277,29 @@ describe('Model', function() {
assert.ok(doc.createdAt.valueOf() >= now.valueOf());
assert.ok(doc.updatedAt);
assert.ok(doc.updatedAt.valueOf() >= now.valueOf());
+ });
+
+ it('with timestamps from merged schema (gh-13409)', async function() {
+ const schema = new Schema({ num: Number });
+ schema.add(new Schema({}, { timestamps: true }));
+
+ const M = db.model('Test', schema);
+
+ await M.create({ num: 42 });
+
+ await new Promise((resolve) => setTimeout(resolve, 10));
+ const now = Date.now();
+
+ await M.bulkWrite([{
+ updateOne: {
+ filter: { num: 42 },
+ update: { num: 100 }
+ }
+ }]);
+ const doc = await M.findOne({ num: 100 });
+ assert.ok(doc.updatedAt);
+ assert.ok(doc.updatedAt.valueOf() >= now.valueOf());
});
it('with child timestamps (gh-7032)', async function() {
@@ -5885,6 +4329,49 @@ describe('Model', function() {
});
+ it('sets version key (gh-13944)', async function() {
+ const userSchema = new Schema({
+ firstName: { type: String, required: true },
+ lastName: { type: String }
+ });
+ const User = db.model('User', userSchema);
+
+ await User.bulkWrite([
+ {
+ updateOne: {
+ filter: { lastName: 'Gibbons' },
+ update: { firstName: 'Peter' },
+ upsert: true
+ }
+ },
+ {
+ insertOne: {
+ document: {
+ firstName: 'Michael',
+ lastName: 'Bolton'
+ }
+ }
+ },
+ {
+ replaceOne: {
+ filter: { lastName: 'Lumbergh' },
+ replacement: { firstName: 'Bill', lastName: 'Lumbergh' },
+ upsert: true
+ }
+ }
+ ], { ordered: false });
+
+ const users = await User.find();
+ assert.deepStrictEqual(
+ users.map(user => user.firstName).sort(),
+ ['Bill', 'Michael', 'Peter']
+ );
+ assert.deepStrictEqual(
+ users.map(user => user.__v),
+ [0, 0, 0]
+ );
+ });
+
it('with single nested and setOnInsert (gh-7534)', function() {
const nested = new Schema({ name: String });
const schema = new Schema({ nested: nested });
@@ -6073,25 +4560,150 @@ describe('Model', function() {
const { num } = await Test.findById(_id);
assert.equal(num, 99);
});
- });
- it('insertMany with Decimal (gh-5190)', async function() {
- const version = start.mongodVersion();
+ it('bulkWrite should throw an error if there were operations that failed validation, ' +
+ 'but all operations that passed validation succeeded (gh-13256)', async function() {
+ const userSchema = new Schema({ age: { type: Number } });
+ const User = db.model('User', userSchema);
- const mongo34 = version[0] > 3 || (version[0] === 3 && version[1] >= 4);
- if (!mongo34) {
- return;
- }
+ const createdUser = await User.create({ name: 'Test' });
- const schema = new mongoose.Schema({
- amount: mongoose.Schema.Types.Decimal
+ const err = await User.bulkWrite([
+ {
+ updateOne: {
+ filter: { _id: createdUser._id },
+ update: { $set: { age: 'NaN' } },
+ upsert: true
+ }
+ },
+ {
+ updateOne: {
+ filter: { _id: createdUser._id },
+ update: { $set: { age: 13 } },
+ upsert: true
+ }
+ },
+ {
+ updateOne: {
+ filter: { _id: createdUser._id },
+ update: { $set: { age: 12 } },
+ upsert: true
+ }
+ }
+ ], { ordered: false, throwOnValidationError: true })
+ .then(() => null)
+ .catch(err => err);
+
+ assert.ok(err);
+ assert.equal(err.name, 'MongooseBulkWriteError');
+ assert.equal(err.validationErrors[0].path, 'age');
+ assert.equal(err.results[0].path, 'age');
+ });
+
+ it('casts $elemMatch filter (gh-14678)', async function() {
+ const schema = new mongoose.Schema({
+ name: String,
+ ids: [String]
+ });
+ const TestModel = db.model('Test', schema);
+
+ const { _id } = await TestModel.create({ ids: ['1'] });
+ await TestModel.bulkWrite([
+ {
+ updateOne: {
+ filter: {
+ ids: {
+ $elemMatch: {
+ $in: [1]
+ }
+ }
+ },
+ update: {
+ $set: {
+ name: 'test'
+ },
+ $addToSet: {
+ ids: {
+ $each: [1, '2', 3]
+ }
+ }
+ }
+ }
+ }
+ ]);
+
+ const doc = await TestModel.findById(_id).orFail();
+ assert.strictEqual(doc.name, 'test');
+ assert.deepStrictEqual(doc.ids, ['1', '2', '3']);
+ });
+
+ it('throwOnValidationError (gh-14572) (gh-13256)', async function() {
+ const schema = new Schema({
+ num: Number
+ });
+
+ const M = db.model('Test', schema);
+
+ const ops = [
+ {
+ insertOne: {
+ document: {
+ num: 'not a number'
+ }
+ }
+ }
+ ];
+
+ const err = await M.bulkWrite(
+ ops,
+ { ordered: false, throwOnValidationError: true }
+ ).then(() => null, err => err);
+ assert.ok(err);
+ assert.equal(err.name, 'MongooseBulkWriteError');
+ assert.equal(err.validationErrors[0].errors['num'].name, 'CastError');
});
- const Money = db.model('Test', schema);
- await Money.insertMany([{ amount: '123.45' }]);
+ it('bulkWrite should throw an error if there were operations that failed validation, ' +
+ 'but all operations that passed validation succeeded (gh-13256)', async function() {
+ const userSchema = new Schema({ age: { type: Number } });
+ const User = db.model('User', userSchema);
+
+ const createdUser = await User.create({ name: 'Test' });
+
+ const err = await User.bulkWrite([
+ {
+ updateOne: {
+ filter: { _id: createdUser._id },
+ update: { $set: { age: 'NaN' } },
+ upsert: true
+ }
+ },
+ {
+ updateOne: {
+ filter: { _id: createdUser._id },
+ update: { $set: { age: 13 } },
+ upsert: true
+ }
+ },
+ {
+ updateOne: {
+ filter: { _id: createdUser._id },
+ update: { $set: { age: 12 } },
+ upsert: true
+ }
+ }
+ ], { ordered: false, throwOnValidationError: true })
+ .then(() => null)
+ .catch(err => err);
+
+ assert.ok(err);
+ assert.equal(err.name, 'MongooseBulkWriteError');
+ assert.equal(err.validationErrors[0].path, 'age');
+ assert.equal(err.results[0].path, 'age');
+ });
});
- it('remove with cast error (gh-5323)', function(done) {
+ it('deleteOne with cast error (gh-5323)', async function() {
const schema = new mongoose.Schema({
name: String
});
@@ -6102,81 +4714,24 @@ describe('Model', function() {
{ name: 'test-2' }
];
- Model.create(arr, function(error) {
- assert.ifError(error);
- Model.remove([], function(error) {
- assert.ok(error);
- assert.ok(error.message.indexOf('must be an object') !== -1,
- error.message);
- Model.find({}, function(error, docs) {
- assert.ifError(error);
- assert.equal(docs.length, 2);
- done();
- });
- });
- });
+ await Model.create(arr);
+ await assert.rejects(() => Model.deleteOne([]), /must be an object/);
+ const docs = await Model.find({});
+
+ assert.equal(docs.length, 2);
});
- it('.create() with non-object (gh-2037)', function(done) {
+ it('.create() with non-object (gh-2037)', async function() {
const schema = new mongoose.Schema({ name: String });
const Model = db.model('Test', schema);
- Model.create(1, function(error) {
- assert.ok(error);
- assert.equal(error.name, 'ObjectParameterError');
- done();
- });
- });
-
- it.skip('save() with wtimeout defined in schema (gh-6862)', function(done) {
- // If you want to test this, setup replica set with 1 primary up and 1 secondary down
- this.timeout(5500);
- const schema = new Schema({
- name: String
- }, {
- writeConcern: {
- w: 2,
- wtimeout: 1000
- }
- });
- const User = db.model('User', schema);
- const user = new User();
- user.name = 'Jon Snow';
- user.save(function(error) {
- assert.ok(error);
- assert.equal(error.name, 'MongoWriteConcernError');
-
- // although timeout, the doc have been successfully saved in the primary.
- User.findOne({}, function(err, user) {
- if (err) return done(err);
- assert.equal(user.name, 'Jon Snow');
- done();
- });
- });
- });
-
- it.skip('save with wtimeout in options (gh_6862)', function(done) {
- // If you want to test this, setup replica set with 1 primary up and 1 secondary down
- this.timeout(5500);
- const schema = new Schema({
- name: String
- });
- const User = db.model('User', schema);
- const user = new User();
- user.name = 'Jon Snow';
- user.save({ w: 2, wtimeout: 1000 }, function(error) {
- assert.ok(error);
- assert.equal(error.name, 'MongoWriteConcernError');
- User.findOne({}, function(err, user) {
- if (err) return done(err);
- assert.equal(user.name, 'Jon Snow');
- done();
- });
- });
+ const error = await Model.create(1).then(() => null, err => err);
+ assert.ok(error);
+ assert.equal(error.name, 'ObjectParameterError');
});
- it('bulkWrite casting updateMany, deleteOne, deleteMany (gh-3998)', function(done) {
+ it('bulkWrite casting updateMany, deleteOne, deleteMany (gh-3998)', async function() {
const schema = new Schema({
str: String,
num: Number
@@ -6209,17 +4764,13 @@ describe('Model', function() {
}
}
];
- M.bulkWrite(ops, function(error) {
- assert.ifError(error);
- M.countDocuments({}, function(error, count) {
- assert.ifError(error);
- assert.equal(count, 0);
- done();
- });
- });
+
+ await M.bulkWrite(ops);
+ const count = await M.countDocuments({});
+ assert.equal(count, 0);
});
- it('bulkWrite casting replaceOne (gh-3998)', function(done) {
+ it('bulkWrite casting replaceOne (gh-3998)', async function() {
const schema = new Schema({
str: String,
num: Number
@@ -6240,15 +4791,10 @@ describe('Model', function() {
}
}
];
- M.bulkWrite(ops, function(error) {
- assert.ifError(error);
- M.findOne({}, function(error, doc) {
- assert.ifError(error);
- assert.strictEqual(doc.str, '2');
- assert.strictEqual(doc.num, 2);
- done();
- });
- });
+ await M.bulkWrite(ops);
+ const doc = await M.findOne({});
+ assert.strictEqual(doc.str, '2');
+ assert.strictEqual(doc.num, 2);
});
it('alias with lean virtual (gh-6069)', async function() {
@@ -6267,10 +4813,9 @@ describe('Model', function() {
const res = await Model.findById(doc._id).lean();
assert.equal(schema.virtual('nameAlias').getters[0].call(res), 'Val');
-
});
- it('marks array as modified when initializing non-array from db (gh-2442)', function(done) {
+ it('marks array as modified when initializing non-array from db (gh-2442)', async function() {
const s1 = new Schema({
array: mongoose.Schema.Types.Mixed
}, { minimize: false });
@@ -6291,40 +4836,19 @@ describe('Model', function() {
const M1 = db.model('Test', s1);
const M2 = db.model('Test1', s2, M1.collection.name);
- M1.create({ array: {} }, function(err, doc) {
- assert.ifError(err);
- assert.ok(doc.array);
- M2.findOne({ _id: doc._id }, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.array[0].value, 0);
- doc.array[0].value = 1;
- doc.save(function(err) {
- assert.ifError(err);
- M2.findOne({ _id: doc._id }, function(err, doc) {
- assert.ifError(err);
- assert.ok(!doc.isModified('array'));
- assert.deepEqual(doc.array[0].value, 1);
- assert.equal(JSON.stringify(doc.array), '[{"value":1}]');
- done();
- });
- });
- });
- });
+ let doc = await M1.create({ array: {} });
+ assert.ok(doc.array);
+ doc = await M2.findOne({ _id: doc._id });
+ assert.equal(doc.array[0].value, 0);
+ doc.array[0].value = 1;
+ await doc.save();
+ doc = await M2.findOne({ _id: doc._id });
+ assert.ok(!doc.isModified('array'));
+ assert.deepEqual(doc.array[0].value, 1);
+ assert.equal(JSON.stringify(doc.array), '[{"value":1}]');
});
- it('Throws when saving same doc in parallel w/ callback (gh-6456)', function(done) {
- let called = 0;
-
- function counter() {
- if (++called === 2) {
- Test.countDocuments(function(err, cnt) {
- assert.ifError(err);
- assert.strictEqual(cnt, 1);
- done();
- });
- }
- }
-
+ it('Throws when saving same doc in parallel w/ callback (gh-6456)', async function() {
const schema = new Schema({
name: String
});
@@ -6335,19 +4859,14 @@ describe('Model', function() {
name: 'Billy'
});
- test.save(function cb(err, doc) {
- assert.ifError(err);
- assert.strictEqual(doc.name, 'Billy');
- counter();
- });
-
- test.save(function cb(err) {
- assert.strictEqual(err.name, 'ParallelSaveError');
- const regex = new RegExp(test.id);
- assert.ok(regex.test(err.message));
- counter();
- });
+ const promises = [test.save(), test.save()];
+ const err = await Promise.all(promises).then(() => null, err => err);
+ assert.strictEqual(err.name, 'ParallelSaveError');
+ const regex = new RegExp(test.id);
+ assert.ok(regex.test(err.message));
+ await Promise.allSettled(promises);
});
+
describe('Model.syncIndexes()', () => {
it('adds indexes to the collection', async() => {
// Arrange
@@ -6537,6 +5056,31 @@ describe('Model', function() {
assert.strictEqual(indexes[1].background, false);
});
+ it('syncIndexes() supports hideIndexes (gh-14868)', async function() {
+ const opts = { autoIndex: false };
+ const schema = new Schema({ name: String }, opts);
+ schema.index({ name: 1 });
+
+ let M = db.model('Test', schema);
+ await M.syncIndexes({});
+
+ let indexes = await M.listIndexes();
+ assert.deepEqual(indexes[1].key, { name: 1 });
+ assert.ok(!indexes[1].hidden);
+
+ db.deleteModel(/Test/);
+ M = db.model('Test', new Schema({ name: String }, opts));
+ await M.syncIndexes({ hideIndexes: true });
+ indexes = await M.listIndexes();
+ assert.deepEqual(indexes[1].key, { name: 1 });
+ assert.ok(indexes[1].hidden);
+
+ await M.syncIndexes({});
+ indexes = await M.listIndexes();
+ assert.equal(indexes.length, 1);
+ assert.deepEqual(indexes[0].key, { _id: 1 });
+ });
+
it('should not drop a text index on .syncIndexes() call (gh-10850)', async function() {
const collation = { collation: { locale: 'simple' } };
const someSchema = new Schema({
@@ -6923,57 +5467,6 @@ describe('Model', function() {
}, /should not be run with `new`/);
});
- it('throws if non-function passed as callback (gh-6640)', function() {
- const Model = db.model('Test', new Schema({
- name: String
- }));
-
- const doc = new Model({});
-
- assert.throws(function() {
- doc.save({}, {});
- }, /callback must be a function/i);
- });
-
- it('Throws when saving same doc in parallel w/ promises (gh-6456)', function(done) {
- let called = 0;
-
- function counter() {
- if (++called === 2) {
- Test.countDocuments(function(err, cnt) {
- assert.ifError(err);
- assert.strictEqual(cnt, 1);
- done();
- });
- }
- }
-
- const schema = new Schema({
- name: String
- });
-
- const Test = db.model('Test', schema);
-
- const test = new Test({
- name: 'Sarah'
- });
-
- function handler(doc) {
- assert.strictEqual(doc.id, test.id);
- counter();
- }
-
- function error(err) {
- assert.strictEqual(err.name, 'ParallelSaveError');
- const regex = new RegExp(test.id);
- assert.ok(regex.test(err.message));
- counter();
- }
-
- test.save().then(handler);
- test.save().catch(error);
- });
-
it('allows calling save in a post save hook (gh-6611)', async function() {
let called = 0;
const noteSchema = new Schema({
@@ -6991,10 +5484,11 @@ describe('Model', function() {
const Note = db.model('Test', noteSchema);
+ await Note.deleteMany({});
await Note.create({ body: 'a note.' });
+ assert.equal(called, 1);
const doc = await Note.findOne({});
assert.strictEqual(doc.body, 'a note, part deux.');
-
});
it('createCollection() respects schema collation (gh-6489)', async function() {
@@ -7008,10 +5502,6 @@ describe('Model', function() {
await Model.createCollection();
const collectionName = Model.collection.name;
- // If the collection is not created, the following will throw
- // MongoServerError: Collection [mongoose_test.User] not found.
- await db.collection(collectionName).stats();
-
await Model.create([{ name: 'alpha' }, { name: 'Zeta' }]);
// Ensure that the default collation is set. Mongoose will set the
@@ -7165,6 +5655,37 @@ describe('Model', function() {
assert.ok(collOptions.timeseries);
});
+ it('createCollection() respects clusteredIndex', async function() {
+ const version = await start.mongodVersion();
+ if (version[0] < 6) {
+ this.skip();
+ return;
+ }
+
+ const schema = Schema({ name: String, timestamp: Date, metadata: Object }, {
+ clusteredIndex: {
+ key: { _id: 1 },
+ name: 'clustered test'
+ },
+ autoCreate: false,
+ autoIndex: false
+ });
+
+ const Test = db.model('Test', schema, 'Test');
+ await Test.init();
+
+ await Test.collection.drop().catch(() => {});
+ await Test.createCollection();
+
+ const collections = await Test.db.db.listCollections().toArray();
+ const coll = collections.find(coll => coll.name === 'Test');
+ assert.ok(coll);
+ assert.deepEqual(coll.options.clusteredIndex.key, { _id: 1 });
+ assert.equal(coll.options.clusteredIndex.name, 'clustered test');
+
+ await Test.collection.drop().catch(() => {});
+ });
+
it('mongodb actually removes expired documents (gh-11229)', async function() {
this.timeout(1000 * 80); // 80 seconds, see later comments on why
const version = await start.mongodVersion();
@@ -7249,7 +5770,7 @@ describe('Model', function() {
}
]);
- const beforeExpirationCount = await Test.count({});
+ const beforeExpirationCount = await Test.countDocuments({});
assert.ok(beforeExpirationCount === 12);
let intervalid;
@@ -7263,7 +5784,7 @@ describe('Model', function() {
// in case it happens faster, to reduce test time
new Promise(resolve => {
intervalid = setInterval(async() => {
- const count = await Test.count({});
+ const count = await Test.countDocuments({});
if (count === 0) {
resolve();
}
@@ -7273,7 +5794,7 @@ describe('Model', function() {
clearInterval(intervalid);
- const afterExpirationCount = await Test.count({});
+ const afterExpirationCount = await Test.countDocuments({});
assert.equal(afterExpirationCount, 0);
});
@@ -7353,34 +5874,6 @@ describe('Model', function() {
assert.ok(_schema.obj.nested);
});
- it('Model.events() (gh-7125)', async function() {
- const Model = db.model('Test', Schema({
- name: { type: String, validate: () => false }
- }));
-
- let called = [];
- Model.events.on('error', err => { called.push(err); });
-
-
- await Model.findOne({ _id: 'Not a valid ObjectId' }).catch(() => {});
- assert.equal(called.length, 1);
- assert.equal(called[0].name, 'CastError');
-
- called = [];
-
- const doc = new Model({ name: 'fail' });
- await doc.save().catch(() => {});
- assert.equal(called.length, 1);
- assert.equal(called[0].name, 'ValidationError');
-
- called = [];
-
- await Model.aggregate([{ $group: { fail: true } }]).exec().catch(() => {});
- assert.equal(called.length, 1);
- assert.equal(called[0].name, 'MongoServerError');
-
- });
-
it('sets $session() before pre save hooks run (gh-7742)', async function() {
const schema = new Schema({ name: String });
let sessions = [];
@@ -7390,7 +5883,6 @@ describe('Model', function() {
const SampleModel = db.model('Test', schema);
-
await SampleModel.create({ name: 'foo' });
// start session
const session = await db.startSession();
@@ -7408,13 +5900,12 @@ describe('Model', function() {
await doc.save({ session: null });
assert.equal(sessions.length, 1);
assert.strictEqual(sessions[0], null);
-
});
- it('sets $session() before pre remove hooks run (gh-7742)', async function() {
+ it('sets $session() before pre deleteOne hooks run (gh-7742)', async function() {
const schema = new Schema({ name: String });
let sessions = [];
- schema.pre('remove', function() {
+ schema.pre('deleteOne', { document: true, query: false }, function() {
sessions.push(this.$session());
});
@@ -7430,10 +5921,9 @@ describe('Model', function() {
doc.foo = 'bar';
sessions = [];
- await doc.remove({ session });
+ await doc.deleteOne({ session });
assert.equal(sessions.length, 1);
assert.strictEqual(sessions[0], session);
-
});
it('set $session() before pre validate hooks run on bulkWrite and insertMany (gh-7769)', async function() {
@@ -7491,6 +5981,83 @@ describe('Model', function() {
});
+ it('custom statics that overwrite aggregate functions dont get hooks by default (gh-14903)', async function() {
+
+ const schema = new Schema({ name: String });
+
+ schema.statics.aggregate = function(pipeline) {
+ return model.aggregate.apply(this, [pipeline]);
+ };
+
+ let called = 0;
+ schema.pre('aggregate', function(next) {
+ ++called;
+ next();
+ });
+ const Model = db.model('Test', schema);
+
+ await Model.create({ name: 'foo' });
+
+ const res = await Model.aggregate([
+ {
+ $match: {
+ name: 'foo'
+ }
+ }
+ ]);
+
+ assert.ok(res[0].name);
+ assert.equal(called, 1);
+ });
+
+ it('custom statics that overwrite model functions dont get hooks by default', async function() {
+
+ const schema = new Schema({ name: String });
+
+ schema.statics.insertMany = function(docs) {
+ return model.insertMany.apply(this, [docs]);
+ };
+
+ let called = 0;
+ schema.pre('insertMany', function(next) {
+ ++called;
+ next();
+ });
+ const Model = db.model('Test', schema);
+
+ const res = await Model.insertMany([
+ { name: 'foo' },
+ { name: 'boo' }
+ ]);
+
+ assert.ok(res[0].name);
+ assert.ok(res[1].name);
+ assert.equal(called, 1);
+ });
+
+ it('custom statics that overwrite document functions dont get hooks by default', async function() {
+
+ const schema = new Schema({ name: String });
+
+ schema.statics.save = function() {
+ return 'foo';
+ };
+
+ let called = 0;
+ schema.pre('save', function(next) {
+ ++called;
+ next();
+ });
+
+ const Model = db.model('Test', schema);
+
+ const doc = await Model.save();
+
+ assert.ok(doc);
+ assert.equal(doc, 'foo');
+ assert.equal(called, 0);
+ });
+
it('error handling middleware passes saved doc (gh-7832)', async function() {
const schema = new Schema({ _id: Number });
@@ -7546,7 +6113,7 @@ describe('Model', function() {
const docFromCreation = await Model.create({ name: 'foo' });
const existingDocument = await Model.exists({ _id: docFromCreation._id });
assert.equal(existingDocument._id.toString(), docFromCreation._id.toString());
- assert.deepStrictEqual(existingDocument, { _id: docFromCreation._id });
+ assert.deepStrictEqual(Object.keys(existingDocument), ['_id']);
assert.ok(isLean(existingDocument));
});
@@ -7619,6 +6186,39 @@ describe('Model', function() {
});
+ it('bulkWrite skips defaults based on global setDefaultsOnInsert (gh-13823)', async function() {
+ const m = new mongoose.Mongoose();
+ m.set('setDefaultsOnInsert', false);
+ await m.connect(start.uri);
+
+ const Test = m.model('Test', Schema({
+ name: String,
+ age: Number,
+ status: {
+ type: Number,
+ enum: [1, 2],
+ default: 1
+ }
+ }));
+ await Test.bulkWrite([{
+ updateOne: {
+ filter: {
+ name: 'test1'
+ },
+ update: {
+ $set: {
+ age: 19
+ }
+ },
+ upsert: true
+ }
+ }]);
+ const doc = await Test.findOne({ name: 'test1' }).lean();
+ assert.ok(!doc.status);
+
+ await m.disconnect();
+ });
+
it('bulkWrite upsert works when update casts to empty (gh-8698)', async function() {
const userSchema = new Schema({
name: String
@@ -7707,6 +6307,52 @@ describe('Model', function() {
});
+ it('bulkwrite should not change updatedAt on subdocs when timestamps set to false (gh-13611)', async function() {
+
+ const postSchema = new Schema({
+ title: String,
+ category: String,
+ isDeleted: Boolean
+ }, { timestamps: true });
+
+ const userSchema = new Schema({
+ name: String,
+ isDeleted: Boolean,
+ posts: { type: [postSchema] }
+ }, { timestamps: true });
+
+ const User = db.model('gh13611User', userSchema);
+
+ const entry = await User.create({
+ name: 'Test Testerson',
+ posts: [{ title: 'title a', category: 'a', isDeleted: false }, { title: 'title b', category: 'b', isDeleted: false }],
+ isDeleted: false
+ });
+ const initialTime = entry.posts[0].updatedAt;
+ await delay(10);
+
+ await User.bulkWrite([{
+ updateMany: {
+ filter: {
+ isDeleted: false
+ },
+ update: {
+ 'posts.$[post].isDeleted': true
+ },
+ arrayFilters: [
+ {
+ 'post.category': { $eq: 'a' }
+ }
+ ],
+ upsert: false,
+ timestamps: false
+ }
+ }]);
+ const res = await User.findOne({ _id: entry._id });
+ const currentTime = res.posts[0].updatedAt;
+ assert.equal(initialTime.getTime(), currentTime.getTime());
+ });
+
it('bulkWrite can overwrite schema `strict` option for filters and updates (gh-8778)', async function() {
// Arrange
const userSchema = new Schema({
@@ -7852,6 +6498,38 @@ describe('Model', function() {
});
+ it('Model.bulkWrite(...) does not hang with empty array and ordered: false (gh-13664)', async function() {
+ const userSchema = new Schema({ name: String });
+ const User = db.model('User', userSchema);
+
+ const res = await User.bulkWrite([], { ordered: false });
+ assert.deepEqual(
+ res,
+ {
+ result: {
+ ok: 1,
+ writeErrors: [],
+ writeConcernErrors: [],
+ insertedIds: [],
+ nInserted: 0,
+ nUpserted: 0,
+ nMatched: 0,
+ nModified: 0,
+ nRemoved: 0,
+ upserted: []
+ },
+ insertedCount: 0,
+ matchedCount: 0,
+ modifiedCount: 0,
+ deletedCount: 0,
+ upsertedCount: 0,
+ upsertedIds: {},
+ insertedIds: {},
+ n: 0
+ }
+ );
+ });
+
it('allows calling `create()` after `bulkWrite()` (gh-9350)', async function() {
const schema = Schema({ foo: Boolean });
const Model = db.model('Test', schema);
@@ -7983,17 +6661,17 @@ describe('Model', function() {
describe('buildBulkWriteOperations() (gh-9673)', () => {
it('builds write operations', async() => {
-
const userSchema = new Schema({
- name: { type: String }
- });
+ name: { type: String },
+ a: { type: Number }
+ }, { shardKey: { a: 1 } });
const User = db.model('User', userSchema);
const users = [
- new User({ name: 'Hafez1_gh-9673-1' }),
- new User({ name: 'Hafez2_gh-9673-1' }),
- new User({ name: 'I am the third name' })
+ new User({ name: 'Hafez1_gh-9673-1', a: 1 }),
+ new User({ name: 'Hafez2_gh-9673-1', a: 2 }),
+ new User({ name: 'I am the third name', a: 3 })
];
await users[2].save();
@@ -8004,7 +6682,7 @@ describe('Model', function() {
const desiredWriteOperations = [
{ insertOne: { document: users[0] } },
{ insertOne: { document: users[1] } },
- { updateOne: { filter: { _id: users[2]._id }, update: { $set: { name: 'I am the updated third name' } } } }
+ { updateOne: { filter: { _id: users[2]._id, a: 3 }, update: { $set: { name: 'I am the updated third name' } } } }
];
assert.deepEqual(
@@ -8089,6 +6767,27 @@ describe('Model', function() {
assert.equal(writeOperations.length, 3);
});
+ it('saves changes in discriminators if calling `bulkSave()` on base model (gh-13907)', async() => {
+ const schema = new mongoose.Schema(
+ { value: String },
+ { discriminatorKey: 'type' }
+ );
+ const typeASchema = new mongoose.Schema({ aValue: String });
+ schema.discriminator('A', typeASchema);
+
+ const TestModel = db.model('Test', schema);
+ const testData = { value: 'initValue', type: 'A', aValue: 'initAValue' };
+ const doc = await TestModel.create(testData);
+
+ doc.value = 'updatedValue1';
+ doc.aValue = 'updatedValue2';
+ await TestModel.bulkSave([doc]);
+
+ const findDoc = await TestModel.findById(doc._id);
+ assert.strictEqual(findDoc.value, 'updatedValue1');
+ assert.strictEqual(findDoc.aValue, 'updatedValue2');
+ });
+
it('accepts `timestamps: false` (gh-12059)', async() => {
// Arrange
const userSchema = new Schema({
@@ -8155,6 +6854,47 @@ describe('Model', function() {
});
assert.deepEqual(timestampsOptions, [undefined, undefined]);
});
+ it('should not modify the object in the $set clause and not error when dealing with or without timestamps (gh-14164)', async function() {
+ const timeSchema = new Schema({
+ name: String,
+ properties: { type: Schema.Types.Mixed, default: {} }
+ }, { timestamps: true });
+ const timelessSchema = new Schema({
+ name: String,
+ properties: { type: Schema.Types.Mixed, default: {} }
+ });
+
+ const Time = db.model('Time', timeSchema);
+ const Timeless = db.model('Timeless', timelessSchema);
+
+ const timeDoc = await Time.create({
+ name: 'Time Test'
+ });
+ timeDoc.properties.color = 'Red';
+ const beforeSet = {};
+ Object.assign(beforeSet, timeDoc.toObject());
+ await Time.bulkWrite([{
+ updateOne: {
+ filter: { _id: timeDoc._id },
+ update: { $set: timeDoc }
+ }
+ }]);
+ assert.deepStrictEqual(beforeSet, timeDoc.toObject());
+
+ const timelessDoc = await Timeless.create({
+ name: 'Timeless Test'
+ });
+ timelessDoc.properties.color = 'Red';
+ const timelessObj = {};
+ Object.assign(timelessObj, timelessDoc.toObject());
+ await Timeless.bulkWrite([{
+ updateOne: {
+ filter: { _id: timelessDoc._id },
+ update: { $set: timelessDoc }
+ }
+ }]);
+ assert.deepStrictEqual(timelessObj, timelessDoc.toObject());
+ });
});
describe('bulkSave() (gh-9673)', function() {
@@ -8219,6 +6959,45 @@ describe('Model', function() {
});
+ it('insertMany should throw an error if there were operations that failed validation, ' +
+ 'but all operations that passed validation succeeded (gh-14572) (gh-13256)', async function() {
+ const userSchema = new Schema({
+ age: { type: Number }
+ });
+
+ const User = db.model('User', userSchema);
+
+ let err = await User.insertMany([
+ new User({ age: 12 }),
+ new User({ age: 12 }),
+ new User({ age: 'NaN' })
+ ], { ordered: false, throwOnValidationError: true })
+ .then(() => null)
+ .catch(err => err);
+
+ assert.ok(err);
+ assert.equal(err.name, 'MongooseBulkWriteError');
+ assert.equal(err.validationErrors[0].errors['age'].name, 'CastError');
+ assert.ok(err.results[2] instanceof Error);
+ assert.equal(err.results[2].errors['age'].name, 'CastError');
+
+ let docs = await User.find();
+ assert.deepStrictEqual(docs.map(doc => doc.age), [12, 12]);
+
+ err = await User.insertMany([
+ new User({ age: 'NaN' })
+ ], { ordered: false, throwOnValidationError: true })
+ .then(() => null)
+ .catch(err => err);
+
+ assert.ok(err);
+ assert.equal(err.name, 'MongooseBulkWriteError');
+ assert.equal(err.validationErrors[0].errors['age'].name, 'CastError');
+
+ docs = await User.find();
+ assert.deepStrictEqual(docs.map(doc => doc.age), [12, 12]);
+ });
+
it('returns writeResult on success', async() => {
const userSchema = new Schema({
@@ -8372,24 +7151,92 @@ describe('Model', function() {
await User.bulkSave([user1, user2]);
- const usersFromDatabase = await User.find({ _id: { $in: [user1._id, user2._id] } }).sort('_id');
- assert.equal(usersFromDatabase[0].name, 'name from pre-save');
- assert.equal(usersFromDatabase[1].name, 'name from pre-save');
+ const usersFromDatabase = await User.find({ _id: { $in: [user1._id, user2._id] } }).sort('_id');
+ assert.equal(usersFromDatabase[0].name, 'name from pre-save');
+ assert.equal(usersFromDatabase[1].name, 'name from pre-save');
+
+ });
+ it('works if some document is not modified (gh-10437)', async() => {
+ const userSchema = new Schema({
+ name: String
+ });
+
+ const User = db.model('User', userSchema);
+
+
+ const user = await User.create({ name: 'Hafez' });
+
+ const err = await User.bulkSave([user]).then(() => null, err => err);
+ assert.ok(err == null);
+
+ });
+ it('should error if no documents were inserted or updated (gh-14763)', async function() {
+ const fooSchema = new mongoose.Schema({
+ bar: { type: Number }
+ }, { optimisticConcurrency: true });
+ const TestModel = db.model('Test', fooSchema);
+
+ const foo = await TestModel.create({
+ bar: 0
+ });
+
+ // update 1
+ foo.bar = 1;
+ await foo.save();
+
+ // parallel update
+ const fooCopy = await TestModel.findById(foo._id);
+ fooCopy.bar = 99;
+ await fooCopy.save();
+ foo.bar = 2;
+ const err = await TestModel.bulkSave([foo]).then(() => null, err => err);
+ assert.equal(err.name, 'MongooseBulkSaveIncompleteError');
+ assert.equal(err.numDocumentsNotUpdated, 1);
});
- it('works if some document is not modified (gh-10437)', async() => {
- const userSchema = new Schema({
- name: String
- });
+ it('should error if not all documents were inserted or updated (gh-14763)', async function() {
+ const fooSchema = new mongoose.Schema({
+ bar: { type: Number }
+ }, { optimisticConcurrency: true });
+ const TestModel = db.model('Test', fooSchema);
- const User = db.model('User', userSchema);
+ const errorDoc = await TestModel.create({ bar: 0 });
+ const okDoc = await TestModel.create({ bar: 0 });
+ // update 1
+ errorDoc.bar = 1;
+ await errorDoc.save();
- const user = await User.create({ name: 'Hafez' });
+ // parallel update
+ const errorDocCopy = await TestModel.findById(errorDoc._id);
+ errorDocCopy.bar = 99;
+ await errorDocCopy.save();
- const err = await User.bulkSave([user]).then(() => null, err => err);
- assert.ok(err == null);
+ errorDoc.bar = 2;
+ okDoc.bar = 2;
+ const err = await TestModel.bulkSave([errorDoc, okDoc]).then(() => null, err => err);
+ assert.equal(err.name, 'MongooseBulkSaveIncompleteError');
+ assert.equal(err.numDocumentsNotUpdated, 1);
+
+ const updatedOkDoc = await TestModel.findById(okDoc._id);
+ assert.equal(updatedOkDoc.bar, 2);
+ });
+ it('should error if there is a validation error', async function() {
+ const fooSchema = new mongoose.Schema({
+ bar: { type: Number }
+ }, { optimisticConcurrency: true });
+ const TestModel = db.model('Test', fooSchema);
+
+ const docs = [
+ new TestModel({ bar: 42 }),
+ new TestModel({ bar: 'taco' })
+ ];
+ const err = await TestModel.bulkSave(docs).then(() => null, err => err);
+ assert.equal(err.name, 'ValidationError');
+ // bulkSave() does not save any documents if any documents fail validation
+ const fromDb = await TestModel.find();
+ assert.equal(fromDb.length, 0);
});
it('Using bulkSave should not trigger an error (gh-11071)', async function() {
@@ -8870,6 +7717,108 @@ describe('Model', function() {
const ret = Test.castObject(obj, { ignoreCastErrors: true });
assert.deepStrictEqual(ret, { nested: { num: 2 }, docArr: [{ num: 4 }] });
});
+ it('handles discriminators (gh-15075)', async function() {
+ // Create the base shape schema
+ const shapeSchema = new mongoose.Schema({ name: String }, {
+ discriminatorKey: 'kind',
+ _id: false
+ });
+
+ // Main schema with shape array
+ const schema = new mongoose.Schema({
+ shape: [shapeSchema]
+ });
+
+ // Circle discriminator
+ schema
+ .path('shape')
+ .discriminator('Circle', new mongoose.Schema({
+ radius: {
+ type: mongoose.Schema.Types.Number,
+ required: true
+ }
+ }, { _id: false }));
+
+ // PropertyPath schema for Square
+ const propertyPathSchema = new mongoose.Schema({
+ property: {
+ type: mongoose.Schema.Types.String,
+ required: true
+ },
+ path: {
+ type: mongoose.Schema.Types.String,
+ required: true
+ }
+ }, { _id: false });
+
+ // Square discriminator
+ schema
+ .path('shape')
+ .discriminator(
+ 'Square',
+ new mongoose.Schema({
+ propertyPaths: {
+ type: [propertyPathSchema],
+ required: true
+ }
+ }, { _id: false })
+ );
+
+ const TestModel = db.model('Test', schema);
+
+ const circle = { shape: [{ kind: 'Circle', radius: '5' }] };
+ const square = { shape: [{ kind: 'Square', propertyPaths: [{ property: 42 }] }] };
+
+ assert.deepStrictEqual(
+ TestModel.castObject(circle).shape[0],
+ { kind: 'Circle', radius: 5 }
+ );
+ assert.deepStrictEqual(
+ TestModel.castObject(square).shape[0],
+ { kind: 'Square', propertyPaths: [{ property: '42' }] }
+ );
+
+ const square2 = { shape: [{ kind: 'Square', propertyPaths: {} }] };
+ assert.deepStrictEqual(
+ TestModel.castObject(square2).shape[0],
+ { kind: 'Square', propertyPaths: [{}] }
+ );
+ });
+ it('handles castNonArrays when document array is set to non-array value (gh-15075)', function() {
+ const sampleSchema = new mongoose.Schema({
+ sampleArray: {
+ type: [new mongoose.Schema({ name: String })],
+ castNonArrays: false
+ }
+ });
+ const Test = db.model('Test', sampleSchema);
+
+ const obj = { sampleArray: { name: 'Taco' } };
+ assert.throws(() => Test.castObject(obj), /Tried to set nested object field `sampleArray` to primitive value/);
+ });
+ it('handles document arrays (gh-15164)', function() {
+ const barSchema = new mongoose.Schema({
+ foo: {
+ type: mongoose.Schema.Types.String,
+ required: true
+ }
+ }, { _id: false });
+
+ const fooSchema = new mongoose.Schema({
+ bars: {
+ type: [barSchema],
+ required: true
+ }
+ });
+
+ const Test = db.model('Test', fooSchema);
+
+ let obj = Test.castObject({ bars: [] });
+ assert.deepStrictEqual(obj.bars, []);
+
+ obj = Test.castObject({ bars: [{ foo: 'bar' }] });
+ assert.deepStrictEqual(obj.bars, [{ foo: 'bar' }]);
+ });
});
it('works if passing class that extends Document to `loadClass()` (gh-12254)', async function() {
@@ -8958,6 +7907,518 @@ describe('Model', function() {
assert.equal(TestModel.staticFn(), 'Returned from staticFn');
});
});
+
+ describe('Bypass middleware', function() {
+ it('should bypass middleware if save is called on a document with no changes gh-13250', async function() {
+ const testSchema = new Schema({
+ name: String
+ });
+ let bypass = true;
+ testSchema.pre('findOne', function(next) {
+ bypass = false;
+ next();
+ });
+ const Test = db.model('gh13250', testSchema);
+ const doc = await Test.create({
+ name: 'Test Testerson'
+ });
+ await doc.save();
+ assert(bypass);
+ });
+ });
+
+ it('respects schema-level `collectionOptions` for setting options to createCollection()', async function() {
+ const testSchema = new Schema({
+ name: String
+ }, { collectionOptions: { capped: true, size: 1024 } });
+ const TestModel = db.model('Test', testSchema);
+ await TestModel.init();
+ await TestModel.collection.drop();
+ await TestModel.createCollection();
+
+ const isCapped = await TestModel.collection.isCapped();
+ assert.ok(isCapped);
+ });
+
+ it('throws helpful error when calling Model() with string instead of model() (gh-14281)', async function() {
+ assert.throws(
+ () => mongoose.Model('taco'),
+ /First argument to `Model` constructor must be an object/
+ );
+ });
+
+ it('supports recompiling model with new schema additions (gh-14296)', function() {
+ const schema = new mongoose.Schema({ field: String }, { toObject: { virtuals: false } });
+ const TestModel = db.model('Test', schema);
+ TestModel.schema.virtual('myVirtual').get(function() {
+ return this.field + ' from myVirtual';
+ });
+ const doc = new TestModel({ field: 'Hello' });
+ assert.strictEqual(doc.myVirtual, undefined);
+
+ TestModel.recompileSchema();
+ assert.equal(doc.myVirtual, 'Hello from myVirtual');
+ assert.strictEqual(doc.toObject().myVirtual, undefined);
+
+ doc.schema.options.toObject.virtuals = true;
+ TestModel.recompileSchema();
+ assert.equal(doc.myVirtual, 'Hello from myVirtual');
+ assert.equal(doc.toObject().myVirtual, 'Hello from myVirtual');
+ });
+
+ it('supports recompiling model with new discriminators (gh-14444) (gh-14296)', function() {
+ // Define discriminated schema
+ const decoratorSchema = new Schema({
+ type: { type: String, required: true }
+ }, { discriminatorKey: 'type' });
+
+ class Decorator {
+ whoAmI() { return 'I am BaseDeco'; }
+ }
+ decoratorSchema.loadClass(Decorator);
+
+ // Define discriminated class before model is compiled
+ class Deco1 extends Decorator { whoAmI() { return 'I am Test1'; }}
+ const deco1Schema = new Schema({});
+ deco1Schema.loadClass(Deco1);
+ decoratorSchema.discriminator('Test1', deco1Schema);
+
+ // Define model that uses discriminated schema
+ const shopSchema = new Schema({
+ item: { type: decoratorSchema, required: true }
+ });
+ const shopModel = db.model('Test', shopSchema);
+
+ // Define another discriminated class after the model is compiled
+ class Deco2 extends Decorator { whoAmI() { return 'I am Test2'; }}
+ const deco2Schema = new Schema({});
+ deco2Schema.loadClass(Deco2);
+ decoratorSchema.discriminator('Test2', deco2Schema);
+
+ let instance = new shopModel({ item: { type: 'Test1' } });
+ assert.equal(instance.item.whoAmI(), 'I am Test1');
+
+ instance = new shopModel({ item: { type: 'Test2' } });
+ assert.equal(instance.item.whoAmI(), 'I am BaseDeco');
+
+ shopModel.recompileSchema();
+
+ instance = new shopModel({ item: { type: 'Test1' } });
+ assert.equal(instance.item.whoAmI(), 'I am Test1');
+
+ instance = new shopModel({ item: { type: 'Test2' } });
+ assert.equal(instance.item.whoAmI(), 'I am Test2');
+ });
+
+ it('overwrites existing discriminators when calling recompileSchema (gh-14527) (gh-14444)', async function() {
+ const shopItemSchema = new mongoose.Schema({}, { discriminatorKey: 'type' });
+ const shopSchema = new mongoose.Schema({
+ items: { type: [shopItemSchema], required: true }
+ });
+
+ const shopItemSubType = new mongoose.Schema({ prop: Number });
+ shopItemSchema.discriminator(2, shopItemSubType);
+ const shopModel = db.model('shop', shopSchema);
+
+ shopModel.recompileSchema();
+ const doc = new shopModel({
+ items: [{ type: 2, prop: 42 }]
+ });
+ assert.equal(doc.items[0].prop, 42);
+ });
+
+ it('does not throw with multiple self-referencing discriminator schemas applied to schema (gh-15120)', async function() {
+ const baseSchema = new Schema({
+ type: { type: Number, required: true }
+ }, { discriminatorKey: 'type' });
+
+ const selfRefSchema = new Schema({
+ self: { type: [baseSchema] }
+ });
+
+ const anotherSelfRefSchema = new Schema({
+ self2: { type: [baseSchema] }
+ });
+
+ baseSchema.discriminator(5, selfRefSchema);
+ baseSchema.discriminator(6, anotherSelfRefSchema);
+ const Test = db.model('Test', baseSchema);
+
+ const doc = await Test.create({
+ type: 5,
+ self: {
+ type: 6,
+ self2: null
+ }
+ });
+ assert.strictEqual(doc.type, 5);
+ assert.equal(doc.self.length, 1);
+ assert.strictEqual(doc.self[0].type, 6);
+ assert.strictEqual(doc.self[0].self2, null);
+ });
+
+ it('inserts versionKey even if schema has `toObject.versionKey` set to false (gh-14344)', async function() {
+ const schema = new mongoose.Schema(
+ { name: String },
+ { versionKey: '__v', toObject: { versionKey: false } }
+ );
+
+ const Model = db.model('Test', schema);
+
+ await Model.insertMany([{ name: 'x' }]);
+
+ const doc = await Model.findOne();
+
+ assert.strictEqual(doc.__v, 0);
+ });
+
+ it('insertMany should throw an error if there were operations that failed validation, ' +
+ 'but all operations that passed validation succeeded (gh-13256)', async function() {
+ const userSchema = new Schema({
+ age: { type: Number }
+ });
+
+ const User = db.model('User', userSchema);
+
+ let err = await User.insertMany([
+ new User({ age: 12 }),
+ new User({ age: 12 }),
+ new User({ age: 'NaN' })
+ ], { ordered: false, throwOnValidationError: true })
+ .then(() => null)
+ .catch(err => err);
+
+ assert.ok(err);
+ assert.equal(err.name, 'MongooseBulkWriteError');
+ assert.equal(err.validationErrors[0].errors['age'].name, 'CastError');
+ assert.ok(err.results[2] instanceof Error);
+ assert.equal(err.results[2].errors['age'].name, 'CastError');
+
+ let docs = await User.find();
+ assert.deepStrictEqual(docs.map(doc => doc.age), [12, 12]);
+
+ err = await User.insertMany([
+ new User({ age: 'NaN' })
+ ], { ordered: false, throwOnValidationError: true })
+ .then(() => null)
+ .catch(err => err);
+
+ assert.ok(err);
+ assert.equal(err.name, 'MongooseBulkWriteError');
+ assert.equal(err.validationErrors[0].errors['age'].name, 'CastError');
+
+ docs = await User.find();
+ assert.deepStrictEqual(docs.map(doc => doc.age), [12, 12]);
+ });
+
+ describe('applyVirtuals', function() {
+ it('handles basic top-level virtuals', async function() {
+ const userSchema = new Schema({
+ name: String
+ });
+ userSchema.virtual('lowercase').get(function() {
+ return this.name.toLowerCase();
+ });
+ userSchema.virtual('uppercase').get(function() {
+ return this.name.toUpperCase();
+ });
+ const User = db.model('User', userSchema);
+
+ const res = User.applyVirtuals({ name: 'Taco' });
+ assert.equal(res.name, 'Taco');
+ assert.equal(res.lowercase, 'taco');
+ assert.equal(res.uppercase, 'TACO');
+ });
+
+ it('handles virtuals in subdocuments', async function() {
+ const userSchema = new Schema({
+ name: String
+ });
+ userSchema.virtual('lowercase').get(function() {
+ return this.name.toLowerCase();
+ });
+ userSchema.virtual('uppercase').get(function() {
+ return this.name.toUpperCase();
+ });
+ const groupSchema = new Schema({
+ name: String,
+ leader: userSchema,
+ members: [userSchema]
+ });
+ const Group = db.model('Group', groupSchema);
+
+ const res = Group.applyVirtuals({
+ name: 'Microsoft',
+ leader: { name: 'Bill' },
+ members: [{ name: 'John' }, { name: 'Steve' }]
+ });
+ assert.equal(res.name, 'Microsoft');
+ assert.equal(res.leader.name, 'Bill');
+ assert.equal(res.leader.uppercase, 'BILL');
+ assert.equal(res.leader.lowercase, 'bill');
+ assert.equal(res.members[0].name, 'John');
+ assert.equal(res.members[0].uppercase, 'JOHN');
+ assert.equal(res.members[0].lowercase, 'john');
+ assert.equal(res.members[1].name, 'Steve');
+ assert.equal(res.members[1].uppercase, 'STEVE');
+ assert.equal(res.members[1].lowercase, 'steve');
+ });
+
+ it('handles virtuals on nested paths', async function() {
+ const userSchema = new Schema({
+ name: {
+ first: String,
+ last: String
+ }
+ });
+ userSchema.virtual('name.firstUpper').get(function() {
+ return this.name.first.toUpperCase();
+ });
+ userSchema.virtual('name.lastLower').get(function() {
+ return this.name.last.toLowerCase();
+ });
+ const User = db.model('User', userSchema);
+
+ const res = User.applyVirtuals({
+ name: {
+ first: 'Bill',
+ last: 'Gates'
+ }
+ });
+ assert.equal(res.name.first, 'Bill');
+ assert.equal(res.name.last, 'Gates');
+ assert.equal(res.name.firstUpper, 'BILL');
+ assert.equal(res.name.lastLower, 'gates');
+ });
+
+ it('supports passing an array of virtuals to apply', async function() {
+ const userSchema = new Schema({
+ name: {
+ first: String,
+ last: String
+ }
+ });
+ userSchema.virtual('fullName').get(function() {
+ return `${this.name.first} ${this.name.last}`;
+ });
+ userSchema.virtual('name.firstUpper').get(function() {
+ return this.name.first.toUpperCase();
+ });
+ userSchema.virtual('name.lastLower').get(function() {
+ return this.name.last.toLowerCase();
+ });
+ const User = db.model('User', userSchema);
+
+ let res = User.applyVirtuals({
+ name: {
+ first: 'Bill',
+ last: 'Gates'
+ }
+ }, ['fullName', 'name.firstUpper']);
+ assert.strictEqual(res.name.first, 'Bill');
+ assert.strictEqual(res.name.last, 'Gates');
+ assert.strictEqual(res.fullName, 'Bill Gates');
+ assert.strictEqual(res.name.firstUpper, 'BILL');
+ assert.strictEqual(res.name.lastLower, undefined);
+
+ res = User.applyVirtuals({
+ name: {
+ first: 'Bill',
+ last: 'Gates'
+ }
+ }, ['name.lastLower']);
+ assert.strictEqual(res.name.first, 'Bill');
+ assert.strictEqual(res.name.last, 'Gates');
+ assert.strictEqual(res.fullName, undefined);
+ assert.strictEqual(res.name.firstUpper, undefined);
+ assert.strictEqual(res.name.lastLower, 'gates');
+ });
+
+ it('sets populate virtuals to `null` if `justOne`', async function() {
+ const userSchema = new Schema({
+ name: {
+ first: String,
+ last: String
+ },
+ friendId: {
+ type: 'ObjectId'
+ }
+ });
+ userSchema.virtual('fullName').get(function() {
+ return `${this.name.first} ${this.name.last}`;
+ });
+ userSchema.virtual('friend', {
+ ref: 'User',
+ localField: 'friendId',
+ foreignField: '_id',
+ justOne: true
+ });
+ const User = db.model('User', userSchema);
+
+ const friendId = new mongoose.Types.ObjectId();
+ const res = User.applyVirtuals({
+ name: {
+ first: 'Bill',
+ last: 'Gates'
+ },
+ friendId
+ });
+ assert.strictEqual(res.name.first, 'Bill');
+ assert.strictEqual(res.name.last, 'Gates');
+ assert.strictEqual(res.fullName, 'Bill Gates');
+ assert.strictEqual(res.friend, null);
+ });
+ });
+
+ describe('applyTimestamps', function() {
+ it('handles basic top-level timestamps', async function() {
+ const startTime = new Date();
+ const userSchema = new Schema({
+ name: String
+ }, { timestamps: true });
+ const User = db.model('User', userSchema);
+
+ const obj = { name: 'test' };
+ User.applyTimestamps(obj);
+ assert.equal(obj.name, 'test');
+ assert.ok(obj.createdAt instanceof Date);
+ assert.ok(obj.updatedAt instanceof Date);
+ assert.ok(obj.createdAt.valueOf() >= startTime.valueOf());
+ assert.ok(obj.updatedAt.valueOf() >= startTime.valueOf());
+ });
+
+ it('no-op if timestamps not set', async function() {
+ const userSchema = new Schema({
+ name: String
+ });
+ const User = db.model('User', userSchema);
+
+ const obj = { name: 'test' };
+ User.applyTimestamps(obj);
+ assert.equal(obj.name, 'test');
+ assert.ok(!('createdAt' in obj));
+ assert.ok(!('updatedAt' in obj));
+ });
+
+ it('handles custom timestamp property names', async function() {
+ const startTime = new Date();
+ const userSchema = new Schema({
+ name: String
+ }, { timestamps: { createdAt: 'createdOn', updatedAt: 'updatedOn' } });
+ const User = db.model('User', userSchema);
+
+ const obj = { name: 'test' };
+ User.applyTimestamps(obj);
+ assert.equal(obj.name, 'test');
+ assert.ok(obj.createdOn instanceof Date);
+ assert.ok(obj.updatedOn instanceof Date);
+ assert.ok(obj.createdOn.valueOf() >= startTime.valueOf());
+ assert.ok(obj.updatedOn.valueOf() >= startTime.valueOf());
+ assert.ok(!('createdAt' in obj));
+ assert.ok(!('updatedAt' in obj));
+ });
+
+ it('applies timestamps to subdocs', async function() {
+ const startTime = new Date();
+ const userSchema = new Schema({
+ name: String,
+ posts: [new Schema({
+ title: String,
+ content: String
+ }, { timestamps: true })],
+ address: new Schema({
+ city: String,
+ country: String
+ }, { timestamps: true })
+ }, { timestamps: true });
+ const User = db.model('User', userSchema);
+
+ const obj = {
+ name: 'test',
+ posts: [{ title: 'Post 1', content: 'Content 1' }],
+ address: { city: 'New York', country: 'USA' }
+ };
+ User.applyTimestamps(obj);
+ assert.equal(obj.name, 'test');
+ assert.ok(obj.createdAt instanceof Date);
+ assert.ok(obj.updatedAt instanceof Date);
+ assert.ok(obj.createdAt.valueOf() >= startTime.valueOf());
+ assert.ok(obj.updatedAt.valueOf() >= startTime.valueOf());
+ assert.ok(obj.posts[0].createdAt instanceof Date);
+ assert.ok(obj.posts[0].updatedAt instanceof Date);
+ assert.ok(obj.address.createdAt instanceof Date);
+ assert.ok(obj.address.updatedAt instanceof Date);
+ });
+
+ it('supports isUpdate and currentTime options', async function() {
+ const userSchema = new Schema({
+ name: String,
+ post: new Schema({
+ title: String,
+ content: String
+ }, { timestamps: true })
+ }, { timestamps: true });
+ const User = db.model('User', userSchema);
+
+ const obj = {
+ name: 'test',
+ post: { title: 'Post 1', content: 'Content 1' }
+ };
+ User.applyTimestamps(obj, { isUpdate: true, currentTime: () => new Date('2023-06-01T18:00:00.000Z') });
+ assert.equal(obj.name, 'test');
+ assert.ok(!('createdAt' in obj));
+ assert.ok(obj.updatedAt instanceof Date);
+ assert.equal(obj.updatedAt.valueOf(), new Date('2023-06-01T18:00:00.000Z').valueOf());
+ assert.ok(!('createdAt' in obj.post));
+ assert.ok(obj.post.updatedAt.valueOf(), new Date('2023-06-01T18:00:00.000Z').valueOf());
+ });
+ });
+
+ describe('diffIndexes()', function() {
+ it('avoids trying to drop timeseries collections (gh-14984)', async function() {
+ const version = await start.mongodVersion();
+ if (version[0] < 5) {
+ this.skip();
+ return;
+ }
+
+ const schema = new mongoose.Schema(
+ {
+ time: {
+ type: Date
+ },
+ deviceId: {
+ type: String
+ }
+ },
+ {
+ timeseries: {
+ timeField: 'time',
+ metaField: 'deviceId',
+ granularity: 'seconds'
+ },
+ autoCreate: false
+ }
+ );
+
+ const TestModel = db.model(
+ 'TimeSeriesTest',
+ schema,
+ 'gh14984'
+ );
+
+ await db.dropCollection('gh14984').catch(err => {
+ if (err.codeName === 'NamespaceNotFound') {
+ return;
+ }
+ throw err;
+ });
+ await TestModel.createCollection();
+
+ const { toDrop } = await TestModel.diffIndexes();
+ assert.deepStrictEqual(toDrop, []);
+ });
+ });
});
diff --git a/test/model.update.test.js b/test/model.updateOne.test.js
similarity index 56%
rename from test/model.update.test.js
rename to test/model.updateOne.test.js
index 8078b3f0ef9..86982ce7f2b 100644
--- a/test/model.update.test.js
+++ b/test/model.updateOne.test.js
@@ -15,7 +15,7 @@ const ObjectId = Schema.Types.ObjectId;
const DocumentObjectId = mongoose.Types.ObjectId;
const CastError = mongoose.Error.CastError;
-describe('model: update:', function() {
+describe('model: updateOne:', function() {
let post;
const title = 'Tobi';
const author = 'Brian';
@@ -103,152 +103,126 @@ describe('model: update:', function() {
afterEach(() => util.clearTestData(db));
afterEach(() => require('./util').stopRemainingOps(db));
- it('works', function(done) {
- BlogPost.findById(post._id, function(err, cf) {
- assert.ifError(err);
- assert.equal(cf.title, title);
- assert.equal(cf.author, author);
- assert.equal(cf.meta.visitors.valueOf(), 0);
- assert.equal(cf.date.toString(), post.date.toString());
- assert.equal(cf.published, true);
- assert.equal(cf.mixed.x, 'ex');
- assert.deepEqual(cf.numbers.toObject(), [4, 5, 6, 7]);
- assert.equal(cf.owners.length, 2);
- assert.equal(cf.owners[0].toString(), id0.toString());
- assert.equal(cf.owners[1].toString(), id1.toString());
- assert.equal(cf.comments.length, 2);
- assert.equal(cf.comments[0].body, 'been there');
- assert.equal(cf.comments[1].body, 'done that');
- assert.ok(cf.comments[0]._id);
- assert.ok(cf.comments[1]._id);
- assert.ok(cf.comments[0]._id instanceof DocumentObjectId);
- assert.ok(cf.comments[1]._id instanceof DocumentObjectId);
+ it('works', async function() {
+ const cf = await BlogPost.findById(post._id);
+ assert.equal(cf.title, title);
+ assert.equal(cf.author, author);
+ assert.equal(cf.meta.visitors.valueOf(), 0);
+ assert.equal(cf.date.toString(), post.date.toString());
+ assert.equal(cf.published, true);
+ assert.equal(cf.mixed.x, 'ex');
+ assert.deepEqual(cf.numbers.toObject(), [4, 5, 6, 7]);
+ assert.equal(cf.owners.length, 2);
+ assert.equal(cf.owners[0].toString(), id0.toString());
+ assert.equal(cf.owners[1].toString(), id1.toString());
+ assert.equal(cf.comments.length, 2);
+ assert.equal(cf.comments[0].body, 'been there');
+ assert.equal(cf.comments[1].body, 'done that');
+ assert.ok(cf.comments[0]._id);
+ assert.ok(cf.comments[1]._id);
+ assert.ok(cf.comments[0]._id instanceof DocumentObjectId);
+ assert.ok(cf.comments[1]._id instanceof DocumentObjectId);
- const update = {
- title: newTitle, // becomes $set
- $inc: { 'meta.visitors': 2 },
- $set: { date: new Date() },
- published: false, // becomes $set
- mixed: { x: 'ECKS', y: 'why' }, // $set
- $pullAll: { numbers: [4, 6] },
- $pull: { owners: id0 },
- 'comments.1.body': 8 // $set
- };
+ const update = {
+ title: newTitle, // becomes $set
+ $inc: { 'meta.visitors': 2 },
+ $set: { date: new Date() },
+ published: false, // becomes $set
+ mixed: { x: 'ECKS', y: 'why' }, // $set
+ $pullAll: { numbers: [4, 6] },
+ $pull: { owners: id0 },
+ 'comments.1.body': 8 // $set
+ };
- BlogPost.update({ title: title }, update, function(err) {
- assert.ifError(err);
-
- BlogPost.findById(post._id, function(err, up) {
- assert.ifError(err);
- assert.equal(up.title, newTitle);
- assert.equal(up.author, author);
- assert.equal(up.meta.visitors.valueOf(), 2);
- assert.equal(up.date.toString(), update.$set.date.toString());
- assert.equal(up.published, false);
- assert.equal(up.mixed.x, 'ECKS');
- assert.equal(up.mixed.y, 'why');
- assert.deepEqual(up.numbers.toObject(), [5, 7]);
- assert.equal(up.owners.length, 1);
- assert.equal(up.owners[0].toString(), id1.toString());
- assert.equal(up.comments[0].body, 'been there');
- assert.equal(up.comments[1].body, '8');
- assert.ok(up.comments[0]._id);
- assert.ok(up.comments[1]._id);
- assert.ok(up.comments[0]._id instanceof DocumentObjectId);
- assert.ok(up.comments[1]._id instanceof DocumentObjectId);
-
- const update2 = {
- 'comments.body': 'fail'
- };
+ await BlogPost.updateOne({ title: title }, update);
+ let up = await BlogPost.findById(post._id);
+ assert.equal(up.title, newTitle);
+ assert.equal(up.author, author);
+ assert.equal(up.meta.visitors.valueOf(), 2);
+ assert.equal(up.date.toString(), update.$set.date.toString());
+ assert.equal(up.published, false);
+ assert.equal(up.mixed.x, 'ECKS');
+ assert.equal(up.mixed.y, 'why');
+ assert.deepEqual(up.numbers.toObject(), [5, 7]);
+ assert.equal(up.owners.length, 1);
+ assert.equal(up.owners[0].toString(), id1.toString());
+ assert.equal(up.comments[0].body, 'been there');
+ assert.equal(up.comments[1].body, '8');
+ assert.ok(up.comments[0]._id);
+ assert.ok(up.comments[1]._id);
+ assert.ok(up.comments[0]._id instanceof DocumentObjectId);
+ assert.ok(up.comments[1]._id instanceof DocumentObjectId);
+
+ const update2 = {
+ 'comments.body': 'fail'
+ };
- BlogPost.update({ _id: post._id }, update2, function(err) {
- assert.ok(err);
- assert.ok(err.message.length > 0);
- BlogPost.findById(post, function(err) {
- assert.ifError(err);
-
- const update3 = {
- $pull: 'fail'
- };
-
- BlogPost.update({ _id: post._id }, update3, function(err) {
- assert.ok(err);
-
- assert.ok(/Invalid atomic update value for \$pull\. Expected an object, received string/.test(err.message));
-
- const update4 = {
- $inc: { idontexist: 1 }
- };
-
- // should not overwrite doc when no valid paths are submitted
- BlogPost.update({ _id: post._id }, update4, function(err) {
- assert.ifError(err);
-
- BlogPost.findById(post._id, function(err, up) {
- assert.ifError(err);
-
- assert.equal(up.title, newTitle);
- assert.equal(up.author, author);
- assert.equal(up.meta.visitors.valueOf(), 2);
- assert.equal(up.date.toString(), update.$set.date.toString());
- assert.equal(up.published, false);
- assert.equal(up.mixed.x, 'ECKS');
- assert.equal(up.mixed.y, 'why');
- assert.deepEqual(up.numbers.toObject(), [5, 7]);
- assert.equal(up.owners.length, 1);
- assert.equal(up.owners[0].toString(), id1.toString());
- assert.equal(up.comments[0].body, 'been there');
- assert.equal(up.comments[1].body, '8');
- // non-schema data was still stored in mongodb
- assert.strictEqual(1, up._doc.idontexist);
-
- done();
- });
- });
- });
- });
- });
- });
- });
- });
+ let err = await BlogPost.updateOne({ _id: post._id }, update2).then(() => null, err => err);
+ assert.ok(err.message.length > 0);
+ await BlogPost.findById(post);
+
+ const update3 = {
+ $pull: 'fail'
+ };
+
+ err = await BlogPost.updateOne({ _id: post._id }, update3).then(() => null, err => err);
+ assert.ok(err);
+
+ assert.ok(/Invalid atomic update value for \$pull\. Expected an object, received string/.test(err.message));
+
+ const update4 = {
+ $inc: { idontexist: 1 }
+ };
+
+ // should not overwrite doc when no valid paths are submitted
+ await BlogPost.updateOne({ _id: post._id }, update4);
+
+ up = await BlogPost.findById(post._id);
+
+ assert.equal(up.title, newTitle);
+ assert.equal(up.author, author);
+ assert.equal(up.meta.visitors.valueOf(), 2);
+ assert.equal(up.date.toString(), update.$set.date.toString());
+ assert.equal(up.published, false);
+ assert.equal(up.mixed.x, 'ECKS');
+ assert.equal(up.mixed.y, 'why');
+ assert.deepEqual(up.numbers.toObject(), [5, 7]);
+ assert.equal(up.owners.length, 1);
+ assert.equal(up.owners[0].toString(), id1.toString());
+ assert.equal(up.comments[0].body, 'been there');
+ assert.equal(up.comments[1].body, '8');
+ // non-schema data was still stored in mongodb
+ assert.strictEqual(1, up._doc.idontexist);
});
- it('casts doc arrays', function(done) {
+ it('casts doc arrays', async function() {
const update = {
comments: [{ body: 'worked great' }],
$set: { 'numbers.1': 100 },
$inc: { idontexist: 1 }
};
- BlogPost.update({ _id: post._id }, update, function(err) {
- assert.ifError(err);
+ await BlogPost.updateOne({ _id: post._id }, update);
- // get the underlying doc
- BlogPost.collection.findOne({ _id: post._id }, function(err, doc) {
- assert.ifError(err);
+ // get the underlying doc
+ const doc = await BlogPost.collection.findOne({ _id: post._id });
- const up = new BlogPost();
- up.init(doc);
- assert.equal(up.comments.length, 1);
- assert.equal(up.comments[0].body, 'worked great');
- assert.strictEqual(true, !!doc.comments[0]._id);
-
- done();
- });
- });
+ const up = new BlogPost();
+ up.init(doc);
+ assert.equal(up.comments.length, 1);
+ assert.equal(up.comments[0].body, 'worked great');
+ assert.strictEqual(true, !!doc.comments[0]._id);
});
- it('makes copy of conditions and update options', function(done) {
+ it('makes copy of conditions and update options', async function() {
const conditions = { _id: post._id.toString() };
const update = { $set: { some_attrib: post._id.toString() } };
- BlogPost.update(conditions, update, function(err) {
- assert.ifError(err);
- assert.equal(typeof conditions._id, 'string');
- done();
- });
+ await BlogPost.updateOne(conditions, update);
+ assert.equal(typeof conditions._id, 'string');
+
});
- it('$addToSet with $ (gh-479)', function(done) {
+ it('$addToSet with $ (gh-479)', async function() {
function a() {
}
@@ -262,53 +236,39 @@ describe('model: update:', function() {
$set: { 'comments.$.title': crazy }
};
- BlogPost.update({ _id: post._id, 'comments.body': 'been there' }, update, function(err) {
- assert.ifError(err);
- BlogPost.findById(post, function(err, ret) {
- assert.ifError(err);
- assert.equal(ret.comments.length, 2);
- assert.equal(ret.comments[0].body, 'been there');
- assert.equal(ret.comments[0].title, 'MongoDB++');
- assert.strictEqual(true, !!ret.comments[0].comments);
- assert.equal(ret.comments[0].comments.length, 1);
- assert.strictEqual(ret.comments[0].comments[0].body, 'The Ring Of Power');
- done();
- });
- });
+ await BlogPost.updateOne({ _id: post._id, 'comments.body': 'been there' }, update);
+ const ret = await BlogPost.findById(post);
+ assert.equal(ret.comments.length, 2);
+ assert.equal(ret.comments[0].body, 'been there');
+ assert.equal(ret.comments[0].title, 'MongoDB++');
+ assert.strictEqual(true, !!ret.comments[0].comments);
+ assert.equal(ret.comments[0].comments.length, 1);
+ assert.strictEqual(ret.comments[0].comments[0].body, 'The Ring Of Power');
});
describe('using last', function() {
let last;
- beforeEach(function(done) {
- BlogPost.findOne({ 'owners.1': { $exists: true } }, function(error, doc) {
- assert.ifError(error);
- last = doc;
- done();
- });
+ beforeEach(async function() {
+ const doc = await BlogPost.findOne({ 'owners.1': { $exists: true } });
+ last = doc;
});
- it('handles date casting (gh-479)', function(done) {
+ it('handles date casting (gh-479)', async function() {
const update = {
$inc: { 'comments.$.newprop': '1' },
$set: { date: (new Date()).getTime() } // check for single val casting
};
- BlogPost.update({ _id: post._id, 'comments.body': 'been there' }, update, { strict: false }, function(err) {
- assert.ifError(err);
- BlogPost.findById(post, function(err, ret) {
- assert.ifError(err);
- assert.equal(ret._doc.comments[0]._doc.newprop, 1);
- assert.strictEqual(undefined, ret._doc.comments[1]._doc.newprop);
- assert.ok(ret.date instanceof Date);
- assert.equal(ret.date.toString(), new Date(update.$set.date).toString());
-
- done();
- });
- });
+ await BlogPost.updateOne({ _id: post._id, 'comments.body': 'been there' }, update, { strict: false });
+ const ret = await BlogPost.findById(post);
+ assert.equal(ret._doc.comments[0]._doc.newprop, 1);
+ assert.strictEqual(undefined, ret._doc.comments[1]._doc.newprop);
+ assert.ok(ret.date instanceof Date);
+ assert.equal(ret.date.toString(), new Date(update.$set.date).toString());
});
- it('handles $addToSet (gh-545)', function(done) {
+ it('handles $addToSet (gh-545)', async function() {
const owner = last.owners[0];
assert.ok(owner);
const numOwners = last.owners.length;
@@ -316,19 +276,15 @@ describe('model: update:', function() {
$addToSet: { owners: owner }
};
- BlogPost.update({ _id: last._id }, update, function(err) {
- assert.ifError(err);
- BlogPost.findById(last, function(err, ret) {
- assert.ifError(err);
- assert.equal(ret.owners.length, numOwners);
- assert.equal(ret.owners[0].toString(), owner.toString());
+ await BlogPost.updateOne({ _id: last._id }, update);
- done();
- });
- });
+ const ret = await BlogPost.findById(last);
+
+ assert.equal(ret.owners.length, numOwners);
+ assert.equal(ret.owners[0].toString(), owner.toString());
});
- it('handles $addToSet with $each (gh-545)', function(done) {
+ it('handles $addToSet with $each (gh-545)', async function() {
const owner = post.owners[0];
const newowner = new DocumentObjectId();
const numOwners = post.owners.length;
@@ -337,40 +293,32 @@ describe('model: update:', function() {
$addToSet: { owners: { $each: [owner, newowner] } }
};
- BlogPost.update({ _id: post._id }, update, function(err) {
- assert.ifError(err);
- BlogPost.findById(post, function(err, ret) {
- assert.ifError(err);
- assert.equal(ret.owners.length, numOwners + 1);
- assert.equal(ret.owners[0].toString(), owner.toString());
- assert.equal(ret.owners[2].toString(), newowner.toString());
+ await BlogPost.updateOne({ _id: post._id }, update);
- done();
- });
- });
+ const ret = await BlogPost.findById(post);
+
+ assert.equal(ret.owners.length, numOwners + 1);
+ assert.equal(ret.owners[0].toString(), owner.toString());
+ assert.equal(ret.owners[2].toString(), newowner.toString());
});
- it('handles $pop and $unset (gh-574)', function(done) {
+ it('handles $pop and $unset (gh-574)', async function() {
const update = {
$pop: { owners: -1 },
$unset: { title: 1 }
};
- BlogPost.update({ _id: post._id }, update, function(err) {
- assert.ifError(err);
- BlogPost.findById(post, function(err, ret) {
- assert.ifError(err);
- assert.equal(ret.owners.length, 1);
- assert.equal(ret.owners[0].toString(), post.owners[1].toString());
- assert.strictEqual(ret.title, undefined);
+ await BlogPost.updateOne({ _id: post._id }, update);
- done();
- });
- });
+ const ret = await BlogPost.findById(post);
+
+ assert.equal(ret.owners.length, 1);
+ assert.equal(ret.owners[0].toString(), post.owners[1].toString());
+ assert.strictEqual(ret.title, undefined);
});
});
- it('works with nested positional notation', function(done) {
+ it('works with nested positional notation', async function() {
const update = {
$set: {
'comments.0.comments.0.date': '11/5/2011',
@@ -378,111 +326,72 @@ describe('model: update:', function() {
}
};
- BlogPost.update({ _id: post._id }, update, function(err) {
- assert.ifError(err);
- BlogPost.findById(post, function(err, ret) {
- assert.ifError(err);
- assert.equal(ret.comments.length, 2);
- assert.equal(ret.comments[0].body, 'been there');
- assert.equal(ret.comments[1].body, '9000');
- assert.equal(ret.comments[0].comments[0].date.toString(), new Date('11/5/2011').toString());
- assert.equal(ret.comments[1].comments.length, 0);
- done();
- });
- });
+ await BlogPost.updateOne({ _id: post._id }, update);
+
+ const ret = await BlogPost.findById(post);
+ assert.equal(ret.comments.length, 2);
+ assert.equal(ret.comments[0].body, 'been there');
+ assert.equal(ret.comments[1].body, '9000');
+ assert.equal(ret.comments[0].comments[0].date.toString(), new Date('11/5/2011').toString());
+ assert.equal(ret.comments[1].comments.length, 0);
+
});
- it('handles $pull with obj literal (gh-542)', function(done) {
- BlogPost.findById(post, function(err, doc) {
- assert.ifError(err);
+ it('handles $pull with obj literal (gh-542)', async function() {
+ const doc = await BlogPost.findById(post);
- const update = {
- $pull: { comments: { _id: doc.comments[0].id } }
- };
+ const update = {
+ $pull: { comments: { _id: doc.comments[0].id } }
+ };
- BlogPost.update({ _id: post._id }, update, function(err) {
- assert.ifError(err);
- BlogPost.findById(post, function(err, ret) {
- assert.ifError(err);
- assert.equal(ret.comments.length, 1);
- assert.equal(ret.comments[0].body, 'done that');
- done();
- });
- });
- });
+ await BlogPost.updateOne({ _id: post._id }, update);
+
+ const ret = await BlogPost.findById(post);
+ assert.equal(ret.comments.length, 1);
+ assert.equal(ret.comments[0].body, 'done that');
});
- it('handles $pull of obj literal and nested $in', function(done) {
+ it('handles $pull of obj literal and nested $in', async function() {
const update = {
$pull: { comments: { body: { $in: ['been there'] } } }
};
- BlogPost.update({ _id: post._id }, update, function(err) {
- assert.ifError(err);
- BlogPost.findById(post, function(err, ret) {
- assert.ifError(err);
- assert.equal(ret.comments.length, 1);
- assert.equal(ret.comments[0].body, 'done that');
-
- done();
- });
- });
+ await BlogPost.updateOne({ _id: post._id }, update);
+ const ret = await BlogPost.findById(post);
+ assert.equal(ret.comments.length, 1);
+ assert.equal(ret.comments[0].body, 'done that');
});
- it('handles $pull and nested $nin', function(done) {
- BlogPost.findById(post, function(err, doc) {
- assert.ifError(err);
+ it('handles $pull and nested $nin', async function() {
+ const doc = await BlogPost.findById(post);
- doc.comments.push({ body: 'hi' }, { body: 'there' });
- doc.save(function(err) {
- assert.ifError(err);
- BlogPost.findById(doc, function(err, ret) {
- assert.ifError(err);
- assert.equal(ret.comments.length, 4);
+ doc.comments.push({ body: 'hi' }, { body: 'there' });
+ await doc.save();
+ let ret = await BlogPost.findById(doc);
+ assert.equal(ret.comments.length, 4);
- const update = {
- $pull: { comments: { body: { $nin: ['there'] } } }
- };
+ const update = {
+ $pull: { comments: { body: { $nin: ['there'] } } }
+ };
- BlogPost.update({ _id: ret._id }, update, function(err) {
- assert.ifError(err);
- BlogPost.findById(post, function(err, ret) {
- assert.ifError(err);
- assert.equal(ret.comments.length, 1);
- assert.equal(ret.comments[0].body, 'there');
- done();
- });
- });
- });
- });
- });
- });
+ await BlogPost.updateOne({ _id: ret._id }, update);
+ ret = await BlogPost.findById(post);
+ assert.equal(ret.comments.length, 1);
+ assert.equal(ret.comments[0].body, 'there');
- it('updates numbers atomically', function(done) {
- let totalDocs = 4;
+ });
+ it('updates numbers atomically', async function() {
const post = new BlogPost();
post.set('meta.visitors', 5);
- function complete() {
- BlogPost.findOne({ _id: post.get('_id') }, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.get('meta.visitors'), 9);
- done();
- });
- }
+ await post.save();
+ await Promise.all(Array(4).fill(null).map(() => {
+ return BlogPost.updateOne({ _id: post._id }, { $inc: { 'meta.visitors': 1 } });
+ }));
- post.save(function(err) {
- assert.ifError(err);
- function callback(err) {
- assert.ifError(err);
- --totalDocs || complete();
- }
- for (let i = 0; i < 4; ++i) {
- BlogPost
- .update({ _id: post._id }, { $inc: { 'meta.visitors': 1 } }, callback);
- }
- });
+ const doc = await BlogPost.findOne({ _id: post.get('_id') });
+ assert.equal(doc.get('meta.visitors'), 9);
});
it('passes number of affected docs', async function() {
@@ -493,37 +402,25 @@ describe('model: update:', function() {
assert.equal(res.modifiedCount, 3);
});
- it('updates a number to null (gh-640)', function(done) {
+ it('updates a number to null (gh-640)', async function() {
const B = BlogPost;
- const b = new B({ meta: { visitors: null } });
- b.save(function(err) {
- assert.ifError(err);
- B.findById(b, function(err, b) {
- assert.ifError(err);
- assert.strictEqual(b.meta.visitors, null);
-
- B.update({ _id: b._id }, { meta: { visitors: null } }, function(err) {
- assert.strictEqual(null, err);
- done();
- });
- });
- });
+ let b = new B({ meta: { visitors: null } });
+ await b.save();
+ b = await B.findById(b);
+ assert.strictEqual(b.meta.visitors, null);
+
+ await B.updateOne({ _id: b._id }, { meta: { visitors: null } });
});
- it('handles $pull from Mixed arrays (gh-735)', function(done) {
+ it('handles $pull from Mixed arrays (gh-735)', async function() {
const schema = new Schema({ comments: [] });
const M = db.model('Test', schema);
- M.create({ comments: [{ name: 'node 0.8' }] }, function(err, doc) {
- assert.ifError(err);
- M.update({ _id: doc._id }, { $pull: { comments: { name: 'node 0.8' } } }, function(err, affected) {
- assert.ifError(err);
- assert.equal(affected.modifiedCount, 1);
- done();
- });
- });
+ const doc = await M.create({ comments: [{ name: 'node 0.8' }] });
+ const affected = await M.updateOne({ _id: doc._id }, { $pull: { comments: { name: 'node 0.8' } } });
+ assert.equal(affected.modifiedCount, 1);
});
- it('handles $push with $ positionals (gh-1057)', function(done) {
+ it('handles $push with $ positionals (gh-1057)', async function() {
const taskSchema = new Schema({
name: String
});
@@ -540,295 +437,191 @@ describe('model: update:', function() {
const Project = db.model('Test', projectSchema);
- Project.create({ name: 'my project' }, function(err, project) {
- assert.ifError(err);
- const pid = project.id;
- const comp = project.components.create({ name: 'component' });
- Project.update({ _id: pid }, { $push: { components: comp } }, function(err) {
- assert.ifError(err);
- const task = comp.tasks.create({ name: 'my task' });
- Project.update({ _id: pid, 'components._id': comp._id }, { $push: { 'components.$.tasks': task } }, function(err) {
- assert.ifError(err);
- Project.findById(pid, function(err, proj) {
- assert.ifError(err);
- assert.ok(proj);
- assert.equal(proj.components.length, 1);
- assert.equal(proj.components[0].name, 'component');
- assert.equal(comp.id, proj.components[0].id);
- assert.equal(proj.components[0].tasks.length, 1);
- assert.equal(proj.components[0].tasks[0].name, 'my task');
- assert.equal(task.id, proj.components[0].tasks[0].id);
- done();
- });
- });
- });
- });
+ const project = await Project.create({ name: 'my project' });
+ const pid = project.id;
+ const comp = project.components.create({ name: 'component' });
+ await Project.updateOne({ _id: pid }, { $push: { components: comp } });
+
+ const task = comp.tasks.create({ name: 'my task' });
+ await Project.updateOne({ _id: pid, 'components._id': comp._id }, { $push: { 'components.$.tasks': task } });
+ const proj = await Project.findById(pid);
+ assert.ok(proj);
+ assert.equal(proj.components.length, 1);
+ assert.equal(proj.components[0].name, 'component');
+ assert.equal(comp.id, proj.components[0].id);
+ assert.equal(proj.components[0].tasks.length, 1);
+ assert.equal(proj.components[0].tasks[0].name, 'my task');
+ assert.equal(task.id, proj.components[0].tasks[0].id);
});
- it('handles nested paths starting with numbers (gh-1062)', function(done) {
+ it('handles nested paths starting with numbers (gh-1062)', async function() {
const schema = new Schema({ counts: Schema.Types.Mixed });
const M = db.model('Test', schema);
- M.create({ counts: {} }, function(err, m) {
- assert.ifError(err);
- M.update({}, { $inc: { 'counts.1': 1, 'counts.1a': 10 } }, function(err) {
- assert.ifError(err);
- M.findById(m, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.counts['1'], 1);
- assert.equal(doc.counts['1a'], 10);
- done();
- });
- });
- });
+ const m = await M.create({ counts: {} });
+ await M.updateOne({}, { $inc: { 'counts.1': 1, 'counts.1a': 10 } });
+ const doc = await M.findById(m);
+ assert.equal(doc.counts['1'], 1);
+ assert.equal(doc.counts['1a'], 10);
});
- it('handles positional operators with referenced docs (gh-1572)', function(done) {
+ it('handles positional operators with referenced docs (gh-1572)', async function() {
const so = new Schema({ title: String, obj: [String] });
const Some = db.model('Test', so);
- Some.create({ obj: ['a', 'b', 'c'] }, function(err, s) {
- assert.ifError(err);
-
- Some.update({ _id: s._id, obj: 'b' }, { $set: { 'obj.$': 2 } }, function(err) {
- assert.ifError(err);
+ const s = await Some.create({ obj: ['a', 'b', 'c'] });
+ await Some.updateOne({ _id: s._id, obj: 'b' }, { $set: { 'obj.$': 2 } });
- Some.findById(s._id, function(err, ss) {
- assert.ifError(err);
+ const ss = await Some.findById(s._id);
- assert.strictEqual(ss.obj[1], '2');
- done();
- });
- });
- });
+ assert.strictEqual(ss.obj[1], '2');
});
- it('use .where for update condition (gh-2170)', function(done) {
+ it('use .where for update condition (gh-2170)', async function() {
const so = new Schema({ num: Number });
const Some = db.model('Test', so);
- Some.create([{ num: 1 }, { num: 1 }], function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 2);
- const doc0 = docs[0];
- const doc1 = docs[1];
- const sId0 = doc0._id;
- const sId1 = doc1._id;
- Some.where({ _id: sId0 }).updateOne({}, { $set: { num: '99' } }, { multi: true }, function(err, cnt) {
- assert.ifError(err);
- assert.equal(cnt.modifiedCount, 1);
- Some.findById(sId0, function(err, doc0_1) {
- assert.ifError(err);
- assert.equal(doc0_1.num, 99);
- Some.findById(sId1, function(err, doc1_1) {
- assert.ifError(err);
- assert.equal(doc1_1.num, 1);
- done();
- });
- });
- });
- });
- });
+ const docs = await Some.create([{ num: 1 }, { num: 1 }]);
+ assert.equal(docs.length, 2);
+ let doc0 = docs[0];
+ let doc1 = docs[1];
+ const sId0 = doc0._id;
+ const sId1 = doc1._id;
+ const cnt = await Some.where({ _id: sId0 }).updateOne({}, { $set: { num: '99' } }, { multi: true });
+ assert.equal(cnt.modifiedCount, 1);
+ doc0 = await Some.findById(sId0);
- describe('mongodb 2.4 features', function() {
- let mongo24_or_greater = false;
-
- before(async function() {
- const version = await start.mongodVersion();
+ assert.equal(doc0.num, 99);
+ doc1 = await Some.findById(sId1);
- mongo24_or_greater = version[0] > 2 || (version[0] === 2 && version[1] >= 4);
- });
+ assert.equal(doc1.num, 1);
+ });
- it('$setOnInsert operator', function(done) {
- if (!mongo24_or_greater) {
- console.log('not testing mongodb 2.4 $setOnInsert feature');
- return done();
- }
+ describe('mongodb 2.4 features', function() {
+ it('$setOnInsert operator', async function() {
const schema = new Schema({ name: String, age: Number, x: String });
const M = db.model('Test', schema);
- const match = { name: 'set on insert' };
- const op = { $setOnInsert: { age: '47' }, x: 'inserted' };
- M.update(match, op, { upsert: true }, function(err) {
- assert.ifError(err);
- M.findOne(function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.age, 47);
- assert.equal(doc.name, 'set on insert');
-
- const match = { name: 'set on insert' };
- const op = { $setOnInsert: { age: 108 }, name: 'changed' };
- M.update(match, op, { upsert: true }, function(err) {
- assert.ifError(err);
-
- M.findOne(function(err, doc) {
- assert.equal(doc.age, 47);
- assert.equal(doc.name, 'changed');
- done();
- });
- });
- });
- });
- });
+ let match = { name: 'set on insert' };
+ let op = { $setOnInsert: { age: '47' }, x: 'inserted' };
+ await M.updateOne(match, op, { upsert: true });
+ let doc = await M.findOne();
+ assert.equal(doc.age, 47);
+ assert.equal(doc.name, 'set on insert');
- it('push with $slice', function(done) {
- if (!mongo24_or_greater) {
- console.log('not testing mongodb 2.4 $push with $slice feature');
- return done();
- }
+ match = { name: 'set on insert' };
+ op = { $setOnInsert: { age: 108 }, name: 'changed' };
+ await M.updateOne(match, op, { upsert: true });
+
+ doc = await M.findOne();
+ assert.equal(doc.age, 47);
+ assert.equal(doc.name, 'changed');
+ });
+ it('push with $slice', async function() {
const schema = new Schema({ name: String, n: [{ x: Number }] });
const M = db.model('Test', schema);
- M.create({ name: '2.4' }, function(err, created) {
- assert.ifError(err);
-
- let op = {
- $push: {
- n: {
- $each: [{ x: 10 }, { x: 4 }, { x: 1 }],
- $slice: '-1',
- $sort: { x: 1 }
- }
+ const created = await M.create({ name: '2.4' });
+ let op = {
+ $push: {
+ n: {
+ $each: [{ x: 10 }, { x: 4 }, { x: 1 }],
+ $slice: '-1',
+ $sort: { x: 1 }
}
- };
-
- M.update({ _id: created._id }, op, function(err) {
- assert.ifError(err);
- M.findById(created._id, function(err, doc) {
- assert.ifError(err);
- assert.equal(created.id, doc.id);
- assert.equal(doc.n.length, 1);
- assert.equal(doc.n[0].x, 10);
-
- op = {
- $push: {
- n: {
- $each: [],
- $slice: 0
- }
- }
- };
- M.update({ _id: created._id }, op, function(err) {
- assert.ifError(err);
- M.findById(created._id, function(error, doc) {
- assert.ifError(error);
- assert.equal(doc.n.length, 0);
- done();
- });
- });
- });
- });
- });
- });
- });
+ }
+ };
- describe('mongodb 2.6 features', function() {
- let mongo26_or_greater = false;
+ await M.updateOne({ _id: created._id }, op);
- before(async function() {
- const version = await start.mongodVersion();
+ let doc = await M.findById(created._id);
+ assert.equal(created.id, doc.id);
+ assert.equal(doc.n.length, 1);
+ assert.equal(doc.n[0].x, 10);
+
+ op = {
+ $push: {
+ n: {
+ $each: [],
+ $slice: 0
+ }
+ }
+ };
+ await M.updateOne({ _id: created._id }, op);
+
+ doc = await M.findById(created._id);
+ assert.equal(doc.n.length, 0);
- mongo26_or_greater = version[0] > 2 || (version[0] === 2 && version[1] >= 6);
});
+ });
- it('supports $position', function(done) {
- if (!mongo26_or_greater) {
- return done();
- }
+ describe('mongodb 2.6 features', function() {
+ it('supports $position', async function() {
const schema = new Schema({ name: String, n: [{ x: Number }] });
const M = db.model('Test', schema);
- const m = new M({ name: '2.6', n: [{ x: 0 }] });
- m.save(function(error, m) {
- assert.ifError(error);
- assert.equal(m.n.length, 1);
- M.updateOne(
- { name: '2.6' },
- { $push: { n: { $each: [{ x: 2 }, { x: 1 }], $position: 0 } } },
- function(error) {
- assert.ifError(error);
- M.findOne({ name: '2.6' }, function(error, m) {
- assert.ifError(error);
- assert.equal(m.n.length, 3);
- assert.equal(m.n[0].x, 2);
- assert.equal(m.n[1].x, 1);
- assert.equal(m.n[2].x, 0);
- done();
- });
- });
- });
+ let m = new M({ name: '2.6', n: [{ x: 0 }] });
+ await m.save();
+ assert.equal(m.n.length, 1);
+ await M.updateOne(
+ { name: '2.6' },
+ { $push: { n: { $each: [{ x: 2 }, { x: 1 }], $position: 0 } } }
+ );
+ m = await M.findOne({ name: '2.6' });
+ assert.equal(m.n.length, 3);
+ assert.equal(m.n[0].x, 2);
+ assert.equal(m.n[1].x, 1);
+ assert.equal(m.n[2].x, 0);
+
});
- it('supports $currentDate', function(done) {
- if (!mongo26_or_greater) {
- return done();
- }
+ it('supports $currentDate', async function() {
const schema = new Schema({ name: String, lastModified: Date, lastModifiedTS: Date });
const M = db.model('Test', schema);
- const m = new M({ name: '2.6' });
- m.save(function(error) {
- assert.ifError(error);
- const before = Date.now();
- M.update(
- { name: '2.6' },
- { $currentDate: { lastModified: true, lastModifiedTS: { $type: 'timestamp' } } },
- function(error) {
- assert.ifError(error);
- M.findOne({ name: '2.6' }, function(error, m) {
- const after = Date.now();
- assert.ifError(error);
- assert.ok(m.lastModified.getTime() >= before);
- assert.ok(m.lastModified.getTime() <= after);
- done();
- });
- });
- });
+ let m = new M({ name: '2.6' });
+ await m.save();
+ const before = Date.now();
+ await M.updateOne(
+ { name: '2.6' },
+ { $currentDate: { lastModified: true, lastModifiedTS: { $type: 'timestamp' } } }
+ );
+
+ m = await M.findOne({ name: '2.6' });
+ const after = Date.now();
+ assert.ok(m.lastModified.getTime() >= before);
+ assert.ok(m.lastModified.getTime() <= after);
+
});
});
- it('casts empty arrays', function(done) {
+ it('casts empty arrays', async function() {
const so = new Schema({ arr: [] });
const Some = db.model('Test', so);
- Some.create({ arr: ['a'] }, function(err, s) {
- if (err) {
- return done(err);
- }
+ const s = await Some.create({ arr: ['a'] });
- Some.update({ _id: s._id }, { arr: [] }, function(err) {
- if (err) {
- return done(err);
- }
- Some.findById(s._id, function(err, doc) {
- if (err) {
- return done(err);
- }
- assert.ok(Array.isArray(doc.arr));
- assert.strictEqual(0, doc.arr.length);
- done();
- });
- });
- });
+
+ await Some.updateOne({ _id: s._id }, { arr: [] });
+ const doc = await Some.findById(s._id);
+ assert.ok(Array.isArray(doc.arr));
+ assert.strictEqual(0, doc.arr.length);
});
describe('defaults and validators (gh-860)', function() {
- it('applies defaults on upsert', function(done) {
+ it('applies defaults on upsert', async function() {
const s = new Schema({ topping: { type: String, default: 'bacon' }, base: String });
const Breakfast = db.model('Test', s);
const updateOptions = { upsert: true };
- Breakfast.update({}, { base: 'eggs' }, updateOptions, function(error) {
- assert.ifError(error);
- Breakfast.findOne({}).lean().exec(function(error, breakfast) {
- assert.ifError(error);
- assert.equal(breakfast.base, 'eggs');
- assert.equal(breakfast.topping, 'bacon');
- done();
- });
- });
+ await Breakfast.updateOne({}, { base: 'eggs' }, updateOptions);
+
+ const breakfast = await Breakfast.findOne({}).lean().exec();
+ assert.equal(breakfast.base, 'eggs');
+ assert.equal(breakfast.topping, 'bacon');
+
});
it('avoids nested paths if setting parent path (gh-4911)', function(done) {
@@ -855,39 +648,30 @@ describe('model: update:', function() {
catch(done);
});
- it('doesnt set default on upsert if query sets it', function(done) {
+ it('doesnt set default on upsert if query sets it', async function() {
const s = new Schema({ topping: { type: String, default: 'bacon' }, base: String });
const Breakfast = db.model('Test', s);
const updateOptions = { upsert: true };
- Breakfast.update({ topping: 'sausage' }, { base: 'eggs' }, updateOptions, function(error) {
- assert.ifError(error);
- Breakfast.findOne({}, function(error, breakfast) {
- assert.ifError(error);
- assert.equal(breakfast.base, 'eggs');
- assert.equal(breakfast.topping, 'sausage');
- done();
- });
- });
+ await Breakfast.updateOne({ topping: 'sausage' }, { base: 'eggs' }, updateOptions);
+ const breakfast = await Breakfast.findOne({});
+ assert.equal(breakfast.base, 'eggs');
+ assert.equal(breakfast.topping, 'sausage');
});
- it('properly sets default on upsert if query wont set it', function(done) {
+ it('properly sets default on upsert if query wont set it', async function() {
const s = new Schema({ topping: { type: String, default: 'bacon' }, base: String });
const Breakfast = db.model('Test', s);
const updateOptions = { upsert: true };
- Breakfast.update({ topping: { $ne: 'sausage' } }, { base: 'eggs' }, updateOptions, function(error) {
- assert.ifError(error);
- Breakfast.findOne({}, function(error, breakfast) {
- assert.ifError(error);
- assert.equal(breakfast.base, 'eggs');
- assert.equal(breakfast.topping, 'bacon');
- done();
- });
- });
+ await Breakfast.updateOne({ topping: { $ne: 'sausage' } }, { base: 'eggs' }, updateOptions);
+ const breakfast = await Breakfast.findOne({});
+ assert.equal(breakfast.base, 'eggs');
+ assert.equal(breakfast.topping, 'bacon');
+
});
- it('handles defaults on document arrays (gh-4456)', function(done) {
+ it('handles defaults on document arrays (gh-4456)', async function() {
const schema = new Schema({
arr: {
type: [new Schema({ name: String }, { _id: false })],
@@ -898,17 +682,13 @@ describe('model: update:', function() {
const M = db.model('Test', schema);
const opts = { upsert: true };
- M.update({}, {}, opts, function(error) {
- assert.ifError(error);
- M.findOne({}, function(error, doc) {
- assert.ifError(error);
- assert.deepEqual(doc.toObject().arr, [{ name: 'Val' }]);
- done();
- });
- });
+ await M.updateOne({}, {}, opts);
+ const doc = await M.findOne({});
+ assert.deepEqual(doc.toObject().arr, [{ name: 'Val' }]);
+
});
- it('runs validators if theyre set', function(done) {
+ it('runs validators if theyre set', async function() {
const s = new Schema({
topping: {
type: String,
@@ -926,27 +706,24 @@ describe('model: update:', function() {
const Breakfast = db.model('Test', s);
const updateOptions = { upsert: true, runValidators: true };
- Breakfast.update({}, { topping: 'bacon', base: 'eggs' }, updateOptions, function(error) {
- assert.ok(!!error);
- assert.equal(Object.keys(error.errors).length, 1);
- assert.equal(Object.keys(error.errors)[0], 'topping');
- assert.equal(error.errors.topping.message, 'Validator failed for path `topping` with value `bacon`');
-
- Breakfast.findOne({}, function(error, breakfast) {
- assert.ifError(error);
- assert.ok(!breakfast);
- done();
- });
- });
+ const error = await Breakfast.updateOne({}, { topping: 'bacon', base: 'eggs' }, updateOptions).then(() => null, err => err);
+ assert.ok(!!error);
+ assert.equal(Object.keys(error.errors).length, 1);
+ assert.equal(Object.keys(error.errors)[0], 'topping');
+ assert.equal(error.errors.topping.message, 'Validator failed for path `topping` with value `bacon`');
+
+ const breakfast = await Breakfast.findOne({});
+ assert.ok(!breakfast);
+
});
- it('validators handle $unset and $setOnInsert', function(done) {
+ it('validators handle $unset and $setOnInsert', async function() {
const s = new Schema({
steak: { type: String, required: true },
eggs: {
type: String,
validate: function() {
- assert.ok(this instanceof require('../').Query);
+ assert.ok(this instanceof mongoose.Query);
return false;
}
}
@@ -954,15 +731,14 @@ describe('model: update:', function() {
const Breakfast = db.model('Test', s);
const updateOptions = { runValidators: true };
- Breakfast.update({}, { $unset: { steak: '' }, $setOnInsert: { eggs: 'softboiled' } }, updateOptions, function(error) {
- assert.ok(!!error);
- assert.equal(Object.keys(error.errors).length, 2);
- assert.ok(Object.keys(error.errors).indexOf('eggs') !== -1);
- assert.ok(Object.keys(error.errors).indexOf('steak') !== -1);
- assert.equal(error.errors.eggs.message, 'Validator failed for path `eggs` with value `softboiled`');
- assert.equal(error.errors.steak.message, 'Path `steak` is required.');
- done();
- });
+ const error = await Breakfast.updateOne({}, { $unset: { steak: '' }, $setOnInsert: { eggs: 'softboiled' } }, updateOptions).then(() => null, err => err);
+ assert.ok(!!error);
+ assert.equal(Object.keys(error.errors).length, 2);
+ assert.ok(Object.keys(error.errors).indexOf('eggs') !== -1);
+ assert.ok(Object.keys(error.errors).indexOf('steak') !== -1);
+ assert.equal(error.errors.eggs.message, 'Validator failed for path `eggs` with value `softboiled`');
+ assert.equal(error.errors.steak.message, 'Path `steak` is required.');
+
});
it('global validators option (gh-6578)', async function() {
@@ -974,7 +750,7 @@ describe('model: update:', function() {
const updateOptions = { runValidators: true };
const error = await Breakfast.
- update({}, { $unset: { steak: 1 } }, updateOptions).
+ updateOne({}, { $unset: { steak: 1 } }, updateOptions).
catch(err => err);
assert.ok(!!error);
@@ -982,7 +758,7 @@ describe('model: update:', function() {
assert.ok(Object.keys(error.errors).indexOf('steak') !== -1);
});
- it('min/max, enum, and regex built-in validators work', function(done) {
+ it('min/max, enum, and regex built-in validators work', async function() {
const s = new Schema({
steak: { type: String, enum: ['ribeye', 'sirloin'] },
eggs: { type: Number, min: 4, max: 6 },
@@ -991,31 +767,28 @@ describe('model: update:', function() {
const Breakfast = db.model('Test', s);
const updateOptions = { runValidators: true };
- Breakfast.update({}, { $set: { steak: 'ribeye', eggs: 3, bacon: '3 strips' } }, updateOptions, function(error) {
- assert.ok(!!error);
- assert.equal(Object.keys(error.errors).length, 1);
- assert.equal(Object.keys(error.errors)[0], 'eggs');
- assert.equal(error.errors.eggs.message, 'Path `eggs` (3) is less than minimum allowed value (4).');
-
- Breakfast.update({}, { $set: { steak: 'tofu', eggs: 5, bacon: '3 strips' } }, updateOptions, function(error) {
- assert.ok(!!error);
- assert.equal(Object.keys(error.errors).length, 1);
- assert.equal(Object.keys(error.errors)[0], 'steak');
- assert.equal(error.errors.steak, '`tofu` is not a valid enum value for path `steak`.');
-
- Breakfast.update({}, { $set: { steak: 'sirloin', eggs: 6, bacon: 'none' } }, updateOptions, function(error) {
- assert.ok(!!error);
- assert.equal(Object.keys(error.errors).length, 1);
- assert.equal(Object.keys(error.errors)[0], 'bacon');
- assert.equal(error.errors.bacon.message, 'Path `bacon` is invalid (none).');
-
- done();
- });
- });
- });
+ let error = await Breakfast.updateOne({}, { $set: { steak: 'ribeye', eggs: 3, bacon: '3 strips' } }, updateOptions).then(() => null, err => err);
+ assert.ok(!!error);
+ assert.equal(Object.keys(error.errors).length, 1);
+ assert.equal(Object.keys(error.errors)[0], 'eggs');
+ assert.equal(error.errors.eggs.message, 'Path `eggs` (3) is less than minimum allowed value (4).');
+
+ error = await Breakfast.updateOne({}, { $set: { steak: 'tofu', eggs: 5, bacon: '3 strips' } }, updateOptions).then(() => null, err => err);
+ assert.ok(!!error);
+ assert.equal(Object.keys(error.errors).length, 1);
+ assert.equal(Object.keys(error.errors)[0], 'steak');
+ assert.equal(error.errors.steak, '`tofu` is not a valid enum value for path `steak`.');
+
+ error = await Breakfast.updateOne({}, { $set: { steak: 'sirloin', eggs: 6, bacon: 'none' } }, updateOptions).then(() => null, err => err);
+ assert.ok(!!error);
+ assert.equal(Object.keys(error.errors).length, 1);
+ assert.equal(Object.keys(error.errors)[0], 'bacon');
+ assert.equal(error.errors.bacon.message, 'Path `bacon` is invalid (none).');
+
+
});
- it('multiple validation errors', function(done) {
+ it('multiple validation errors', async function() {
const s = new Schema({
steak: { type: String, enum: ['ribeye', 'sirloin'] },
eggs: { type: Number, min: 4, max: 6 },
@@ -1024,16 +797,14 @@ describe('model: update:', function() {
const Breakfast = db.model('Test', s);
const updateOptions = { runValidators: true };
- Breakfast.update({}, { $set: { steak: 'tofu', eggs: 2, bacon: '3 strips' } }, updateOptions, function(error) {
- assert.ok(!!error);
- assert.equal(Object.keys(error.errors).length, 2);
- assert.ok(Object.keys(error.errors).indexOf('steak') !== -1);
- assert.ok(Object.keys(error.errors).indexOf('eggs') !== -1);
- done();
- });
+ const error = await Breakfast.updateOne({}, { $set: { steak: 'tofu', eggs: 2, bacon: '3 strips' } }, updateOptions).then(() => null, err => err);
+ assert.ok(!!error);
+ assert.equal(Object.keys(error.errors).length, 2);
+ assert.ok(Object.keys(error.errors).indexOf('steak') !== -1);
+ assert.ok(Object.keys(error.errors).indexOf('eggs') !== -1);
});
- it('validators ignore $inc', function(done) {
+ it('validators ignore $inc', async function() {
const s = new Schema({
steak: { type: String, required: true },
eggs: { type: Number, min: 4 }
@@ -1041,28 +812,24 @@ describe('model: update:', function() {
const Breakfast = db.model('Test', s);
const updateOptions = { runValidators: true };
- Breakfast.update({}, { $inc: { eggs: 1 } }, updateOptions, function(error) {
- assert.ifError(error);
- done();
- });
+ await Breakfast.updateOne({}, { $inc: { eggs: 1 } }, updateOptions);
+
});
- it('validators handle positional operator (gh-3167)', function(done) {
+ it('validators handle positional operator (gh-3167)', async function() {
const s = new Schema({
toppings: [{ name: { type: String, enum: ['bacon', 'cheese'] } }]
});
const Breakfast = db.model('Test', s);
const updateOptions = { runValidators: true };
- Breakfast.update(
+ const error = await Breakfast.updateOne(
{ 'toppings.name': 'bacon' },
{ 'toppings.$.name': 'tofu' },
- updateOptions,
- function(error) {
- assert.ok(error);
- assert.ok(error.errors['toppings.0.name']);
- done();
- });
+ updateOptions
+ ).then(() => null, err => err);
+ assert.ok(error);
+ assert.ok(error.errors['toppings.0.name']);
});
it('validators handle arrayFilters (gh-7536)', function() {
@@ -1076,7 +843,7 @@ describe('model: update:', function() {
arrayFilters: [{ 't.name': 'bacon' }]
};
return Breakfast.
- update({}, { 'toppings.$[t].name': 'tofu' }, updateOptions).
+ updateOne({}, { 'toppings.$[t].name': 'tofu' }, updateOptions).
then(
() => assert.ok(false),
err => {
@@ -1086,7 +853,7 @@ describe('model: update:', function() {
});
});
- it('required and single nested (gh-4479)', function(done) {
+ it('required and single nested (gh-4479)', async function() {
const FileSchema = new Schema({
name: {
type: String,
@@ -1101,140 +868,97 @@ describe('model: update:', function() {
const Company = db.model('Test', CompanySchema);
const update = { file: { name: '' } };
const options = { runValidators: true };
- Company.update({}, update, options, function(error) {
- assert.ok(error);
- assert.equal(error.errors['file.name'].message,
- 'Path `name` is required.');
- done();
- });
- });
- });
-
- it('works with $set and overwrite (gh-2515)', function(done) {
- const schema = new Schema({ breakfast: String });
- const M = db.model('Test', schema);
-
- M.create({ breakfast: 'bacon' }, function(error, doc) {
- assert.ifError(error);
- M.update(
- { _id: doc._id },
- { $set: { breakfast: 'eggs' } },
- { overwrite: true },
- function(error) {
- assert.ifError(error);
- M.findOne({ _id: doc._id }, function(error, doc) {
- assert.ifError(error);
- assert.equal(doc.breakfast, 'eggs');
- done();
- });
- });
+ const error = await Company.updateOne({}, update, options).then(() => null, err => err);
+ assert.equal(error.errors['file.name'].message,
+ 'Path `name` is required.');
});
});
- it('successfully casts set with nested mixed objects (gh-2796)', function(done) {
+ it('successfully casts set with nested mixed objects (gh-2796)', async function() {
const schema = new Schema({ breakfast: {} });
const M = db.model('Test', schema);
- M.create({}, function(error, doc) {
- assert.ifError(error);
- M.update(
- { _id: doc._id },
- { breakfast: { eggs: 2, bacon: 3 } },
- function(error, result) {
- assert.ifError(error);
- assert.equal(result.modifiedCount, 1);
- M.findOne({ _id: doc._id }, function(error, doc) {
- assert.ifError(error);
- assert.equal(doc.breakfast.eggs, 2);
- done();
- });
- });
- });
+ let doc = await M.create({});
+ const result = await M.updateOne(
+ { _id: doc._id },
+ { breakfast: { eggs: 2, bacon: 3 } }
+ );
+ assert.equal(result.modifiedCount, 1);
+ doc = await M.findOne({ _id: doc._id });
+ assert.equal(doc.breakfast.eggs, 2);
+
});
- it('handles empty update with promises (gh-2796)', function(done) {
+ it('handles empty update with promises (gh-2796)', async function() {
const schema = new Schema({ eggs: Number });
const M = db.model('Test', schema);
- M.create({}, function(error, doc) {
- assert.ifError(error);
- M.updateOne({ _id: doc._id }, { notInSchema: 1 }).exec().
- then(function(data) {
- assert.ok(!data.acknowledged);
- done();
- }).
- catch(done);
- });
+ const doc = await M.create({});
+ return M.updateOne({ _id: doc._id }, { notInSchema: 1 }).exec();
});
describe('middleware', function() {
- it('can specify pre and post hooks', function(done) {
+ it('can specify pre and post hooks', async function() {
let numPres = 0;
let numPosts = 0;
const band = new Schema({ members: [String] });
- band.pre('update', function(next) {
+ band.pre('updateOne', function(next) {
++numPres;
next();
});
- band.post('update', function() {
+ band.post('updateOne', function() {
++numPosts;
});
const Band = db.model('Band', band);
const gnr = new Band({ members: ['Axl', 'Slash', 'Izzy', 'Duff', 'Adler'] });
- gnr.save(function(error) {
- assert.ifError(error);
- assert.equal(numPres, 0);
- assert.equal(numPosts, 0);
- Band.update(
- { _id: gnr._id },
- { $pull: { members: 'Adler' } },
- function(error) {
- assert.ifError(error);
- assert.equal(numPres, 1);
- assert.equal(numPosts, 1);
- Band.findOne({ _id: gnr._id }, function(error, doc) {
- assert.ifError(error);
- assert.deepEqual(['Axl', 'Slash', 'Izzy', 'Duff'],
- doc.toObject().members);
- done();
- });
- });
- });
+ await gnr.save();
+
+ assert.equal(numPres, 0);
+ assert.equal(numPosts, 0);
+ await Band.updateOne(
+ { _id: gnr._id },
+ { $pull: { members: 'Adler' } }
+ );
+ assert.equal(numPres, 1);
+ assert.equal(numPosts, 1);
+ const doc = await Band.findOne({ _id: gnr._id });
+ assert.deepEqual(['Axl', 'Slash', 'Izzy', 'Duff'],
+ doc.toObject().members);
+
});
- it('runs before validators (gh-2706)', function(done) {
+ it('runs before validators (gh-2706)', async function() {
const bandSchema = new Schema({
lead: { type: String, enum: ['Axl Rose'] }
});
- bandSchema.pre('update', function() {
+ bandSchema.pre('updateOne', function() {
this.options.runValidators = true;
});
const Band = db.model('Band', bandSchema);
- Band.update({}, { $set: { lead: 'Not Axl' } }, function(err) {
- assert.ok(err);
- done();
- });
+ const err = await Band.updateOne({}, { $set: { lead: 'Not Axl' } }).then(() => null, err => err);
+ assert.ok(err);
});
describe('objects and arrays', function() {
- it('embedded objects (gh-2706)', function(done) {
+ it('embedded objects (gh-2706)', async function() {
const bandSchema = new Schema({
singer: {
firstName: { type: String, enum: ['Axl'] },
lastName: { type: String, enum: ['Rose'] }
}
});
- bandSchema.pre('update', function() {
+ bandSchema.pre('updateOne', function() {
this.options.runValidators = true;
});
const Band = db.model('Band', bandSchema);
- Band.update({}, { $set: { singer: { firstName: 'Not', lastName: 'Axl' } } }, function(err) {
- assert.ok(err);
- done();
- });
+ const err = await Band.updateOne(
+ {},
+ { $set: { singer: { firstName: 'Not', lastName: 'Axl' } } }
+ ).then(() => null, err => err);
+ assert.ok(err);
});
it('handles document array validation (gh-2733)', async function() {
@@ -1259,7 +983,7 @@ describe('model: update:', function() {
assert.ok(err);
});
- it('validators on arrays (gh-3724)', function(done) {
+ it('validators on arrays (gh-3724)', async function() {
const schema = new Schema({
arr: [String]
});
@@ -1270,38 +994,31 @@ describe('model: update:', function() {
const M = db.model('Test', schema);
const options = { runValidators: true };
- M.findOneAndUpdate({}, { arr: ['test'] }, options, function(error) {
- assert.ok(error);
- assert.ok(/ValidationError/.test(error.toString()));
- done();
- });
+ const error = await M.findOneAndUpdate({}, { arr: ['test'] }, options).then(() => null, err => err);
+ assert.ok(/ValidationError/.test(error.toString()));
});
});
});
- it('works with undefined date (gh-2833)', function(done) {
+ it('works with undefined date (gh-2833)', async function() {
const dateSchema = {
d: Date
};
const D = db.model('Test', dateSchema);
- assert.doesNotThrow(function() {
- D.update({}, { d: undefined }, function() {
- done();
- });
- });
+ await D.updateOne({}, { d: undefined });
});
describe('set() (gh-5770)', function() {
it('works with middleware and doesn\'t change the op', function() {
const schema = new Schema({ name: String, updatedAt: Date });
const date = new Date();
- schema.pre('update', function() {
+ schema.pre('updateOne', function() {
this.set('updatedAt', date);
});
const M = db.model('Test', schema);
- return M.update({}, { name: 'Test' }, { upsert: true }).
+ return M.updateOne({}, { name: 'Test' }, { upsert: true }).
then(() => M.findOne()).
then(doc => {
assert.equal(doc.updatedAt.valueOf(), date.valueOf());
@@ -1311,12 +1028,12 @@ describe('model: update:', function() {
it('object syntax for path parameter', function() {
const schema = new Schema({ name: String, updatedAt: Date });
const date = new Date();
- schema.pre('update', function() {
+ schema.pre('updateOne', function() {
this.set({ updatedAt: date });
});
const M = db.model('Test', schema);
- return M.update({}, { name: 'Test' }, { upsert: true }).
+ return M.updateOne({}, { name: 'Test' }, { upsert: true }).
then(() => M.findOne()).
then(doc => {
assert.equal(doc.updatedAt.valueOf(), date.valueOf());
@@ -1324,7 +1041,7 @@ describe('model: update:', function() {
});
});
- it('does not add virtuals to update (gh-2046)', function(done) {
+ it('does not add virtuals to update (gh-2046)', async function() {
const childSchema = new Schema({ foo: String }, { toObject: { getters: true } });
const parentSchema = new Schema({ children: [childSchema] });
@@ -1334,21 +1051,16 @@ describe('model: update:', function() {
const Parent = db.model('Parent', parentSchema);
- const update = Parent.update({}, { $push: { children: { foo: 'foo' } } }, { upsert: true });
+ const update = Parent.updateOne({}, { $push: { children: { foo: 'foo' } } }, { upsert: true });
assert.equal(update._update.$push.children.bar, undefined);
- update.exec(function(error) {
- assert.ifError(error);
- Parent.findOne({}, function(error, doc) {
- assert.ifError(error);
- assert.equal(doc.children.length, 1);
- assert.ok(!doc.toObject({ virtuals: false }).children[0].bar);
- done();
- });
- });
+ await update.exec();
+ const doc = await Parent.findOne({});
+ assert.equal(doc.children.length, 1);
+ assert.ok(!doc.toObject({ virtuals: false }).children[0].bar);
});
- it('doesnt modify original argument doc (gh-3008)', function(done) {
+ it('doesnt modify original argument doc (gh-3008)', async function() {
const FooSchema = new mongoose.Schema({
key: Number,
value: String
@@ -1356,106 +1068,97 @@ describe('model: update:', function() {
const Model = db.model('Test', FooSchema);
const update = { $set: { values: 2, value: 2 } };
- Model.update({ key: 1 }, update, function() {
- assert.equal(update.$set.values, 2);
- done();
- });
+ await Model.updateOne({ key: 1 }, update);
+ assert.equal(update.$set.values, 2);
});
describe('bug fixes', function() {
- it('can $rename (gh-1845)', function(done) {
+ it('can $rename (gh-1845)', async function() {
const schema = new Schema({ foo: Date, bar: Date });
const Model = db.model('Test', schema);
const update = { $rename: { foo: 'bar' } };
- Model.create({ foo: Date.now() }, function(error) {
- assert.ifError(error);
- Model.update({}, update, { multi: true }, function(error, res) {
- assert.ifError(error);
- assert.equal(res.modifiedCount, 1);
- done();
- });
- });
+ const foo = Date.now();
+ const { _id } = await Model.create({ foo });
+ await Model.updateOne({}, update);
+ const doc = await Model.findById(_id);
+ assert.equal(doc.bar.valueOf(), foo.valueOf());
+ assert.equal(doc.foo, undefined);
});
- it('allows objects with positional operator (gh-3185)', function(done) {
+ it('throws CastError if $rename fails to cast to string (gh-1845)', async function() {
+ const schema = new Schema({ foo: Date, bar: Date });
+ const Model = db.model('Test', schema);
+
+ let err = await Model.updateOne({}, { $rename: { foo: { prop: 'baz' } } }).then(() => null, err => err);
+ assert.equal(err.name, 'CastError');
+ assert.ok(err.message.includes('foo.$rename'));
+
+ err = await Model.updateOne({}, { $rename: { foo: null } }).then(() => null, err => err);
+ assert.equal(err.name, 'CastError');
+ assert.ok(err.message.includes('foo.$rename'));
+
+ err = await Model.updateOne({}, { $rename: { foo: undefined } }).then(() => null, err => err);
+ assert.equal(err.name, 'CastError');
+ assert.ok(err.message.includes('foo.$rename'));
+ });
+
+ it('allows objects with positional operator (gh-3185)', async function() {
const schema = new Schema({ children: [{ _id: Number }] });
const MyModel = db.model('Test', schema);
- MyModel.create({ children: [{ _id: 1 }] }, function(error, doc) {
- assert.ifError(error);
- MyModel.findOneAndUpdate(
- { _id: doc._id, 'children._id': 1 },
- { $set: { 'children.$': { _id: 2 } } },
- { new: true },
- function(error, doc) {
- assert.ifError(error);
- assert.equal(doc.children[0]._id, 2);
- done();
- });
- });
+ let doc = await MyModel.create({ children: [{ _id: 1 }] });
+ doc = await MyModel.findOneAndUpdate(
+ { _id: doc._id, 'children._id': 1 },
+ { $set: { 'children.$': { _id: 2 } } },
+ { new: true }
+ );
+
+ assert.equal(doc.children[0]._id, 2);
});
- it('mixed type casting (gh-3305)', function(done) {
+ it('mixed type casting (gh-3305)', async function() {
const Schema = mongoose.Schema({}, { strict: false });
const Model = db.model('Test', Schema);
- Model.create({}, function(error, m) {
- assert.ifError(error);
- Model.
- update({ _id: m._id }, { $push: { myArr: { key: 'Value' } } }).
- exec(function(error, res) {
- assert.ifError(error);
- assert.equal(res.modifiedCount, 1);
- done();
- });
- });
+ const m = await Model.create({});
+ const res = await Model.
+ updateOne({ _id: m._id }, { $push: { myArr: { key: 'Value' } } }).
+ exec();
+ assert.equal(res.modifiedCount, 1);
});
- it('replaceOne', function(done) {
+ it('replaceOne', async function() {
const schema = mongoose.Schema({ name: String, age: Number }, {
versionKey: false
});
const Model = db.model('Test', schema);
- Model.create({ name: 'abc', age: 1 }, function(error, m) {
- assert.ifError(error);
- Model.replaceOne({ name: 'abc' }, { name: 'test' }).exec(function(err) {
- assert.ifError(err);
- Model.findById(m._id).exec(function(error, doc) {
- assert.ifError(error);
- assert.deepEqual(doc.toObject({ virtuals: false }), {
- _id: m._id,
- name: 'test'
- });
- done();
- });
- });
+ const m = await Model.create({ name: 'abc', age: 1 });
+ await Model.replaceOne({ name: 'abc' }, { name: 'test' }).exec();
+ const doc = await Model.findById(m._id).exec();
+ assert.deepEqual(doc.toObject({ virtuals: false }), {
+ _id: m._id,
+ name: 'test'
});
+
});
- it('mixed nested type casting (gh-3337)', function(done) {
+ it('mixed nested type casting (gh-3337)', async function() {
const Schema = mongoose.Schema({ attributes: {} }, { strict: true });
const Model = db.model('Test', Schema);
- Model.create({}, function(error, m) {
- assert.ifError(error);
- const update = { $push: { 'attributes.scores.bar': { a: 1 } } };
- Model.
- updateOne({ _id: m._id }, update).
- exec(function(error, res) {
- assert.ifError(error);
- assert.equal(res.modifiedCount, 1);
- Model.findById(m._id, function(error, doc) {
- assert.ifError(error);
- assert.equal(doc.attributes.scores.bar.length, 1);
- done();
- });
- });
- });
+ const m = await Model.create({});
+ const update = { $push: { 'attributes.scores.bar': { a: 1 } } };
+ const res = await Model.
+ updateOne({ _id: m._id }, update).
+ exec();
+ assert.equal(res.modifiedCount, 1);
+ const doc = await Model.findById(m._id);
+ assert.equal(doc.attributes.scores.bar.length, 1);
});
- it('with single nested (gh-3820)', function(done) {
+ it('with single nested (gh-3820)', async function() {
const child = new mongoose.Schema({
item2: {
item3: String,
@@ -1470,22 +1173,15 @@ describe('model: update:', function() {
const Parent = db.model('Parent', parentSchema);
- Parent.create({ name: 'test' }, function(error, doc) {
- assert.ifError(error);
- const update = { 'item1.item2': { item3: 'test1', item4: 'test2' } };
- doc.update(update, function(error) {
- assert.ifError(error);
- Parent.findOne({ _id: doc._id }, function(error, doc) {
- assert.ifError(error);
- assert.equal(doc.item1.item2.item3, 'test1');
- assert.equal(doc.item1.item2.item4, 'test2');
- done();
- });
- });
- });
+ let doc = await Parent.create({ name: 'test' });
+ const update = { 'item1.item2': { item3: 'test1', item4: 'test2' } };
+ await doc.updateOne(update);
+ doc = await Parent.findOne({ _id: doc._id });
+ assert.equal(doc.item1.item2.item3, 'test1');
+ assert.equal(doc.item1.item2.item4, 'test2');
});
- it('with single nested and transform (gh-4621)', function(done) {
+ it('with single nested and transform (gh-4621)', async function() {
const SubdocSchema = new Schema({
name: String
}, {
@@ -1503,40 +1199,32 @@ describe('model: update:', function() {
const Collection = db.model('Test', CollectionSchema);
- Collection.create({}, function(error, doc) {
- assert.ifError(error);
- const update = { field2: { name: 'test' } };
- Collection.update({ _id: doc._id }, update, function(err) {
- assert.ifError(err);
- Collection.collection.findOne({ _id: doc._id }, function(err, doc) {
- assert.ifError(err);
- assert.ok(doc.field2._id);
- assert.ok(!doc.field2.id);
- done();
- });
- });
- });
+ let doc = await Collection.create({});
+ const update = { field2: { name: 'test' } };
+ await Collection.updateOne({ _id: doc._id }, update);
+
+ doc = await Collection.collection.findOne({ _id: doc._id });
+ assert.ok(doc.field2._id);
+ assert.ok(!doc.field2.id);
});
- it('works with buffers (gh-3496)', function(done) {
+ it('works with buffers (gh-3496)', async function() {
const Schema = mongoose.Schema({ myBufferField: Buffer });
const Model = db.model('Test', Schema);
- Model.update({}, { myBufferField: Buffer.alloc(1) }, function(error) {
- assert.ifError(error);
- done();
- });
+ await Model.updateOne({}, { myBufferField: Buffer.alloc(1) });
+
});
- it('.update(doc) (gh-3221)', function() {
+ it('.updateOne(doc) (gh-3221)', function() {
const Schema = mongoose.Schema({ name: String });
const Model = db.model('Test', Schema);
- let query = Model.update({ name: 'Val' });
+ let query = Model.updateOne({ name: 'Val' });
assert.equal(query.getUpdate().name, 'Val');
- query = Model.find().update({ name: 'Val' });
+ query = Model.find().updateOne({ name: 'Val' });
assert.equal(query.getUpdate().name, 'Val');
return query.setOptions({ upsert: true }).
@@ -1546,63 +1234,23 @@ describe('model: update:', function() {
});
});
- it('middleware update with exec (gh-3549)', function(done) {
+ it('middleware update with exec (gh-3549)', async function() {
const Schema = mongoose.Schema({ name: String });
- Schema.pre('update', function(next) {
- this.update({ name: 'Val' });
+ Schema.pre('updateOne', function(next) {
+ this.updateOne({ name: 'Val' });
next();
});
const Model = db.model('Test', Schema);
- Model.create({}, function(error, doc) {
- assert.ifError(error);
- Model.update({ _id: doc._id }, { name: 'test' }).exec(function(error) {
- assert.ifError(error);
- Model.findOne({ _id: doc._id }, function(error, doc) {
- assert.ifError(error);
- assert.equal(doc.name, 'Val');
- done();
- });
- });
- });
+ let doc = await Model.create({});
+ await Model.updateOne({ _id: doc._id }, { name: 'test' }).exec();
+ doc = await Model.findOne({ _id: doc._id });
+ assert.equal(doc.name, 'Val');
});
- it('casting $push with overwrite (gh-3564)', function(done) {
- const schema = mongoose.Schema({
- topicId: Number,
- name: String,
- followers: [Number]
- });
-
- const doc = {
- topicId: 100,
- name: 'name',
- followers: [500]
- };
-
- const M = db.model('Test', schema);
-
- M.create(doc, function(err) {
- assert.ifError(err);
-
- const update = { $push: { followers: 200 } };
- const opts = { overwrite: true, new: true, upsert: false, multi: false };
-
- M.update({ topicId: doc.topicId }, update, opts, function(err) {
- assert.ifError(err);
- M.findOne({ topicId: doc.topicId }, function(error, doc) {
- assert.ifError(error);
- assert.equal(doc.name, 'name');
- assert.deepEqual(doc.followers.toObject(), [500, 200]);
- done();
- });
- });
- });
- });
-
- it('$push with buffer doesnt throw error (gh-3890)', function(done) {
+ it('$push with buffer doesnt throw error (gh-3890)', async function() {
const InfoSchema = new Schema({
prop: { type: Buffer }
});
@@ -1621,13 +1269,11 @@ describe('model: update:', function() {
}
};
- ModelA.update({}, update, function(error) {
- assert.ifError(error);
- done();
- });
+ await ModelA.updateOne({}, update);
+
});
- it('$set with buffer (gh-3961)', function(done) {
+ it('$set with buffer (gh-3961)', async function() {
const schema = {
name: Buffer
};
@@ -1637,18 +1283,15 @@ describe('model: update:', function() {
const value = Buffer.from('aa267824dc1796f265ab47870e279780', 'base64');
const instance = new Model({ name: null });
- instance.save(function(error) {
- assert.ifError(error);
- const query = { _id: instance._id };
- const update = { $set: { name: value } };
- const ok = function() {
- done();
- };
- Model.update(query, update).then(ok, done);
- });
+ await instance.save();
+
+ const query = { _id: instance._id };
+ const update = { $set: { name: value } };
+
+ return Model.updateOne(query, update);
});
- it('versioning with setDefaultsOnInsert (gh-2593)', function(done) {
+ it('versioning with setDefaultsOnInsert (gh-2593)', async function() {
const schema = new Schema({
num: Number,
arr: [{ num: Number }]
@@ -1661,10 +1304,9 @@ describe('model: update:', function() {
new: true,
runValidators: true
};
- Model.update({}, update, options, function(error) {
- assert.ifError(error);
- done();
- });
+ await Model.updateOne({}, update, options);
+
+
});
it('updates with timestamps with $set (gh-4989) (gh-7152)', async function() {
@@ -1681,7 +1323,7 @@ describe('model: update:', function() {
let start = Date.now();
await delay(10);
- await Tag.update({}, { $set: { tags: ['test1'] } });
+ await Tag.updateOne({}, { $set: { tags: ['test1'] } });
let tag = await Tag.findOne();
assert.ok(tag.updatedAt.valueOf() > start);
@@ -1715,24 +1357,21 @@ describe('model: update:', function() {
assert.ok(tag.updatedAt.valueOf() > start);
});
- it('lets $currentDate go through with updatedAt (gh-5222)', function(done) {
+ it('lets $currentDate go through with updatedAt (gh-5222)', async function() {
const testSchema = new Schema({
name: String
}, { timestamps: true });
const Test = db.model('Test', testSchema);
- Test.create({ name: 'test' }, function(error) {
- assert.ifError(error);
- const u = { $currentDate: { updatedAt: true }, name: 'test2' };
- Test.update({}, u, function(error) {
- assert.ifError(error);
- done();
- });
- });
+ await Test.create({ name: 'test' });
+ const u = { $currentDate: { updatedAt: true }, name: 'test2' };
+ await Test.updateOne({}, u);
+
+
});
- it('update validators on single nested (gh-4332)', function(done) {
+ it('update validators on single nested (gh-4332)', async function() {
const AreaSchema = new Schema({
a: String
});
@@ -1761,15 +1400,14 @@ describe('model: update:', function() {
runValidators: true
};
- Company.update({}, update, opts, function(error) {
- assert.ok(error);
- assert.equal(error.errors['area'].message, 'Not valid Area');
- done();
- });
+ const error = await Company.updateOne({}, update, opts).then(() => null, err => err);
+ assert.ok(error);
+ assert.equal(error.errors['area'].message, 'Not valid Area');
+
});
- it('updates child schema timestamps with $push (gh-4049)', function(done) {
- const opts = {
+ it('updates child schema timestamps with $push (gh-4049)', async function() {
+ let opts = {
timestamps: true,
toObject: {
virtuals: true
@@ -1790,23 +1428,19 @@ describe('model: update:', function() {
const Parent = db.model('Parent', parentSchema);
const b2 = new Parent();
- b2.save(function(err, doc) {
- const query = { _id: doc._id };
- const update = { $push: { children: { senderId: '234' } } };
- const opts = { new: true };
- Parent.findOneAndUpdate(query, update, opts).exec(function(error, res) {
- assert.ifError(error);
- assert.equal(res.children.length, 1);
- assert.equal(res.children[0].senderId, '234');
- assert.ok(res.children[0].createdAt);
- assert.ok(res.children[0].updatedAt);
- done();
- });
- });
- });
-
- it('updates child schema timestamps with $set (gh-4049)', function(done) {
- const opts = {
+ const doc = await b2.save();
+ const query = { _id: doc._id };
+ const update = { $push: { children: { senderId: '234' } } };
+ opts = { new: true };
+ const res = await Parent.findOneAndUpdate(query, update, opts);
+ assert.equal(res.children.length, 1);
+ assert.equal(res.children[0].senderId, '234');
+ assert.ok(res.children[0].createdAt);
+ assert.ok(res.children[0].updatedAt);
+ });
+
+ it('updates child schema timestamps with $set (gh-4049)', async function() {
+ let opts = {
timestamps: true,
toObject: {
virtuals: true
@@ -1828,30 +1462,27 @@ describe('model: update:', function() {
const Parent = db.model('Parent', parentSchema);
const b2 = new Parent();
- b2.save(function(err, doc) {
- const query = { _id: doc._id };
- const update = {
- $set: {
- children: [{ senderId: '234' }],
- child: { senderId: '567' }
- }
- };
- const opts = { new: true };
- Parent.findOneAndUpdate(query, update, opts).exec(function(error, res) {
- assert.ifError(error);
- assert.equal(res.children.length, 1);
- assert.equal(res.children[0].senderId, '234');
- assert.ok(res.children[0].createdAt);
- assert.ok(res.children[0].updatedAt);
-
- assert.ok(res.child.createdAt);
- assert.ok(res.child.updatedAt);
- done();
- });
- });
+ const doc = await b2.save();
+ const query = { _id: doc._id };
+ const update = {
+ $set: {
+ children: [{ senderId: '234' }],
+ child: { senderId: '567' }
+ }
+ };
+ opts = { new: true };
+ const res = await Parent.findOneAndUpdate(query, update, opts).exec();
+ assert.equal(res.children.length, 1);
+ assert.equal(res.children[0].senderId, '234');
+ assert.ok(res.children[0].createdAt);
+ assert.ok(res.children[0].updatedAt);
+
+ assert.ok(res.child.createdAt);
+ assert.ok(res.child.updatedAt);
+
});
- it('handles positional operator with timestamps (gh-4418)', function(done) {
+ it('handles positional operator with timestamps (gh-4418)', async function() {
const schema = new Schema({
thing: [{
thing2: { type: String },
@@ -1862,13 +1493,12 @@ describe('model: update:', function() {
const Model = db.model('Test', schema);
const query = { 'thing.thing2': 'test' };
const update = { $set: { 'thing.$.test': 'test' } };
- Model.update(query, update, function(error) {
- assert.ifError(error);
- done();
- });
+ await Model.updateOne(query, update);
+
+
});
- it('push with timestamps (gh-4514)', function(done) {
+ it('push with timestamps (gh-4514)', async function() {
const sampleSchema = new mongoose.Schema({
sampleArray: [{
values: [String]
@@ -1880,20 +1510,16 @@ describe('model: update:', function() {
sampleArray: [{ values: ['record1'] }]
});
- newRecord.save(function(err) {
- assert.ifError(err);
- sampleModel.update({ 'sampleArray.values': 'record1' }, {
- $push: { 'sampleArray.$.values': 'another record' }
- },
- { runValidators: true },
- function(err) {
- assert.ifError(err);
- done();
- });
- });
+ await newRecord.save();
+
+ await sampleModel.updateOne({ 'sampleArray.values': 'record1' }, {
+ $push: { 'sampleArray.$.values': 'another record' }
+ },
+ { runValidators: true }
+ );
});
- it('addToSet (gh-4953)', function(done) {
+ it('addToSet (gh-4953)', async function() {
const childSchema = new mongoose.Schema({
name: {
type: String,
@@ -1915,14 +1541,13 @@ describe('model: update:', function() {
$addToSet: { children: { name: 'Test' } }
};
const opts = { new: true, runValidators: true };
- Model.findOneAndUpdate({}, update, opts, function(error) {
- assert.ok(error);
- assert.ok(error.errors['children.lastName']);
- done();
- });
+ const error = await Model.findOneAndUpdate({}, update, opts).then(() => null, err => err);
+ assert.ok(error);
+ assert.ok(error.errors['children.lastName']);
+
});
- it('overwrite with timestamps (gh-4054)', function(done) {
+ it('overwrite with timestamps (gh-4054)', async function() {
const testSchema = new Schema({
user: String,
something: Number
@@ -1935,18 +1560,14 @@ describe('model: update:', function() {
something: 1
};
- TestModel.replaceOne({ user: 'test' }, update, options, function(error) {
- assert.ifError(error);
- TestModel.findOne({}, function(error, doc) {
- assert.ifError(error);
- assert.ok(doc.createdAt);
- assert.ok(doc.updatedAt);
- done();
- });
- });
+ await TestModel.replaceOne({ user: 'test' }, update, options);
+ const doc = await TestModel.findOne({});
+ assert.ok(doc.createdAt);
+ assert.ok(doc.updatedAt);
+
});
- it('update with buffer and exec (gh-4609)', function(done) {
+ it('update with buffer and exec (gh-4609)', async function() {
const arrSchema = new Schema({
ip: mongoose.SchemaTypes.Buffer
});
@@ -1957,16 +1578,11 @@ describe('model: update:', function() {
const M = db.model('Test', schema);
const m = new M({ arr: [{ ip: Buffer.alloc(1) }] });
- m.save(function(error, m) {
- assert.ifError(error);
- m.update({ $push: { arr: { ip: Buffer.alloc(1) } } }).exec(function(error) {
- assert.ifError(error);
- done();
- });
- });
+ await m.save();
+ await m.updateOne({ $push: { arr: { ip: Buffer.alloc(1) } } }).exec();
});
- it('single nested with runValidators (gh-4420)', function(done) {
+ it('single nested with runValidators (gh-4420)', async function() {
const FileSchema = new Schema({
name: String
});
@@ -1978,15 +1594,10 @@ describe('model: update:', function() {
const Company = db.model('Company', CompanySchema);
- Company.create({ name: 'Booster Fuels' }, function(error) {
- assert.ifError(error);
- const update = { file: { name: 'new-name' } };
- const options = { runValidators: true };
- Company.update({}, update, options, function(error) {
- assert.ifError(error);
- done();
- });
- });
+ await Company.create({ name: 'Booster Fuels' });
+ const update = { file: { name: 'new-name' } };
+ const options = { runValidators: true };
+ await Company.updateOne({}, update, options);
});
it('single nested under doc array with runValidators (gh-4960)', function(done) {
@@ -2004,7 +1615,7 @@ describe('model: update:', function() {
User.create({}).
then(function(user) {
- return User.update({
+ return User.updateOne({
_id: user._id
}, {
sell: [{
@@ -2046,7 +1657,7 @@ describe('model: update:', function() {
}
};
await Earth.updateOne(
- { _id: mongoose.Types.ObjectId() },
+ { _id: new mongoose.Types.ObjectId() },
data,
{
runValidators: true
@@ -2054,21 +1665,18 @@ describe('model: update:', function() {
);
});
- it('single nested schema with geo (gh-4465)', function(done) {
+ it('single nested schema with geo (gh-4465)', async function() {
const addressSchema = new Schema({
geo: { type: [Number], index: '2dsphere' }
}, { _id: false });
const containerSchema = new Schema({ address: addressSchema });
const Container = db.model('Test', containerSchema);
- Container.update({}, { address: { geo: [-120.24, 39.21] } }).
- exec(function(error) {
- assert.ifError(error);
- done();
- });
+ await Container.updateOne({}, { address: { geo: [-120.24, 39.21] } }).
+ exec();
});
- it('runs validation on Mixed properties of embedded arrays during updates (gh-4441)', function(done) {
+ it('runs validation on Mixed properties of embedded arrays during updates (gh-4441)', async function() {
const A = new Schema({ str: {} });
let validateCalls = 0;
A.path('str').validate(function() {
@@ -2080,19 +1688,15 @@ describe('model: update:', function() {
B = db.model('Test', B);
- B.findOneAndUpdate(
+ await B.findOneAndUpdate(
{ foo: 'bar' },
{ $set: { a: [{ str: { somekey: 'someval' } }] } },
- { runValidators: true },
- function(err) {
- assert.ifError(err);
- assert.equal(validateCalls, 1);
- done();
- }
+ { runValidators: true }
);
+ assert.equal(validateCalls, 1);
});
- it('updating single nested doc property casts correctly (gh-4655)', function(done) {
+ it('updating single nested doc property casts correctly (gh-4655)', async function() {
const FileSchema = new Schema({});
const ProfileSchema = new Schema({
@@ -2113,48 +1717,14 @@ describe('model: update:', function() {
const User = db.model('User', UserSchema);
- User.create({ profiles: [] }, function(error, user) {
- assert.ifError(error);
- User.update({ _id: user._id }, { $set: { 'profiles.0.rules': {} } }).
- exec(function(error) {
- assert.ifError(error);
- User.findOne({ _id: user._id }).lean().exec(function(error, doc) {
- assert.ifError(error);
- assert.deepEqual(doc.profiles[0], { rules: {} });
- done();
- });
- });
- });
- });
+ const user = await User.create({ profiles: [] });
- it('with overwrite and upsert (gh-4749) (gh-5631)', function(done) {
- const schema = new Schema({
- name: String,
- meta: { age: { type: Number } }
- });
- const User = db.model('User', schema);
-
- const filter = { name: 'Bar' };
- const update = { name: 'Bar', meta: { age: 33 } };
- const options = { overwrite: true, upsert: true };
- const q2 = User.update(filter, update, options);
- assert.deepEqual(q2.getUpdate(), {
- __v: 0,
- meta: { age: 33 },
- name: 'Bar'
- });
-
- const q3 = User.findOneAndUpdate(filter, update, options);
- assert.deepEqual(q3.getUpdate(), {
- __v: 0,
- meta: { age: 33 },
- name: 'Bar'
- });
-
- done();
+ await User.updateOne({ _id: user._id }, { $set: { 'profiles.0.rules': {} } });
+ const doc = await User.findOne({ _id: user._id }).lean().exec();
+ assert.deepEqual(doc.profiles[0], { rules: {} });
});
- it('findOneAndUpdate with nested arrays (gh-5032)', function(done) {
+ it('findOneAndUpdate with nested arrays (gh-5032)', async function() {
const schema = Schema({
name: String,
inputs: [[String]] // Array of Arrays of Strings
@@ -2165,34 +1735,27 @@ describe('model: update:', function() {
const q = { name: 'Host Discovery' };
const u = { inputs: [['ipRange']] };
const o = { upsert: true };
- Activity.findOneAndUpdate(q, u, o).exec(function(error) {
- assert.ifError(error);
- done();
- });
+ await Activity.findOneAndUpdate(q, u, o).exec();
});
- it('findOneAndUpdate with timestamps (gh-5045)', function(done) {
+ it('findOneAndUpdate with timestamps (gh-5045)', async function() {
const schema = new Schema({
username: String,
isDeleted: Boolean
}, { timestamps: true });
const User = db.model('Test', schema);
- User.findOneAndUpdate(
+ await User.findOneAndUpdate(
{ username: 'test', isDeleted: false },
{ createdAt: '2017-03-06T14:08:59+00:00' },
- { new: true, upsert: true },
- function(error) {
- assert.ifError(error);
- User.update({ username: 'test' }, { createdAt: new Date() }).
- exec(function(error) {
- assert.ifError(error);
- done();
- });
- });
+ { new: true, upsert: true }
+ );
+
+ await User.updateOne({ username: 'test' }, { createdAt: new Date() }).
+ exec();
});
- it('doesnt double-call setters when updating an array (gh-5041)', function(done) {
+ it('doesnt double-call setters when updating an array (gh-5041)', async function() {
let called = 0;
const UserSchema = new Schema({
name: String,
@@ -2213,32 +1776,9 @@ describe('model: update:', function() {
const User = db.model('User', UserSchema);
- User.findOneAndUpdate({}, { foos: [{ foo: '13.57' }] }, function(error) {
- assert.ifError(error);
- assert.equal(called, 1);
- done();
- });
- });
-
- it('overwrite doc with update validators (gh-3556)', function(done) {
- const testSchema = new Schema({
- name: {
- type: String,
- required: true
- },
- otherName: String
- });
- const Test = db.model('Test', testSchema);
+ await User.findOneAndUpdate({}, { foos: [{ foo: '13.57' }] });
+ assert.equal(called, 1);
- const opts = { overwrite: true, runValidators: true };
- Test.update({}, { otherName: 'test' }, opts, function(error) {
- assert.ok(error);
- assert.ok(error.errors['name']);
- Test.update({}, { $set: { otherName: 'test' } }, opts, function(error) {
- assert.ifError(error);
- done();
- });
- });
});
it('does not fail if passing whole doc (gh-5088)', function(done) {
@@ -2275,7 +1815,7 @@ describe('model: update:', function() {
runValidators: false,
strict: false
};
- return Test.update({}, data, opts);
+ return Test.updateOne({}, data, opts);
}).
then(function() {
return Test.findOne();
@@ -2299,7 +1839,7 @@ describe('model: update:', function() {
return doc.save().
then(function(doc) {
- return Test.update({ _id: doc._id }, {
+ return Test.updateOne({ _id: doc._id }, {
$pullAll: { arr: [null] }
});
}).
@@ -2321,7 +1861,7 @@ describe('model: update:', function() {
Model.create({ colors: ['green'] }).
then(function() {
- return Model.update({}, { $set: { colors: 'red' } });
+ return Model.updateOne({}, { $set: { colors: 'red' } });
}).
then(function() {
return Model.collection.findOne();
@@ -2373,7 +1913,7 @@ describe('model: update:', function() {
assert.deepStrictEqual(updated.nested.xyz, [[0, 1], [200, 3], [4, 5]]);
});
- it('defaults with overwrite and no update validators (gh-5384)', function(done) {
+ it('defaults with overwrite and no update validators (gh-5384)', async function() {
const testSchema = new mongoose.Schema({
name: String,
something: { type: Number, default: 2 }
@@ -2385,17 +1925,13 @@ describe('model: update:', function() {
};
const update = { name: 'test' };
- TestModel.replaceOne({ name: 'a' }, update, options, function(error) {
- assert.ifError(error);
- TestModel.findOne({}, function(error, doc) {
- assert.ifError(error);
- assert.equal(doc.something, 2);
- done();
- });
- });
+ await TestModel.replaceOne({ name: 'a' }, update, options);
+ const doc = await TestModel.findOne({});
+ assert.equal(doc.something, 2);
+
});
- it('update validators with nested required (gh-5269)', function(done) {
+ it('update validators with nested required (gh-5269)', async function() {
const childSchema = new mongoose.Schema({
d1: {
type: String,
@@ -2412,16 +1948,15 @@ describe('model: update:', function() {
const Parent = db.model('Parent', parentSchema);
- Parent.update({}, { d: { d2: 'test' } }, { runValidators: true }, function(error) {
- assert.ok(error);
- assert.ok(error.errors['d.d1']);
- assert.ok(error.errors['d.d1'].message.indexOf('Path `d1` is required') !== -1,
- error.errors['d.d1'].message);
- done();
- });
+ const error = await Parent.updateOne({}, { d: { d2: 'test' } }, { runValidators: true }).then(() => null, err => err);
+ assert.ok(error);
+ assert.ok(error.errors['d.d1']);
+ assert.ok(error.errors['d.d1'].message.indexOf('Path `d1` is required') !== -1,
+ error.errors['d.d1'].message);
+
});
- it('$push with updateValidators and top-level doc (gh-5430)', function(done) {
+ it('$push with updateValidators and top-level doc (gh-5430)', async function() {
const notificationSchema = new mongoose.Schema({
message: String
});
@@ -2434,19 +1969,16 @@ describe('model: update:', function() {
const User = db.model('User', userSchema);
- User.update({}, {
+ await User.updateOne({}, {
$push: {
notifications: {
$each: [new Notification({ message: 'test' })]
}
}
- }, { multi: true, runValidators: true }).exec(function(error) {
- assert.ifError(error);
- done();
- });
+ }, { multi: true, runValidators: true });
});
- it('$pull with updateValidators (gh-5555)', function(done) {
+ it('$pull with updateValidators (gh-5555)', async function() {
const notificationSchema = new mongoose.Schema({
message: {
type: String,
@@ -2468,27 +2000,20 @@ describe('model: update:', function() {
}
}
};
- User.create({ notifications: [{ message: 'test' }] }, function(error, doc) {
- assert.ifError(error);
-
- User.update({}, update, opts).exec(function(error) {
- assert.ok(error);
- assert.ok(error.errors['notifications.message']);
-
- update.$pull.notifications.message = 'test';
- User.update({ _id: doc._id }, update, opts).exec(function(error) {
- assert.ifError(error);
- User.findById(doc._id, function(error, doc) {
- assert.ifError(error);
- assert.equal(doc.notifications.length, 0);
- done();
- });
- });
- });
- });
+ let doc = await User.create({ notifications: [{ message: 'test' }] });
+
+ const error = await User.updateOne({}, update, opts).exec().then(() => null, err => err);
+ assert.ok(error);
+ assert.ok(error.errors['notifications.message']);
+
+ update.$pull.notifications.message = 'test';
+ await User.updateOne({ _id: doc._id }, update, opts).exec();
+ doc = await User.findById(doc._id);
+ assert.equal(doc.notifications.length, 0);
+
});
- it('$pull with updateValidators and $in (gh-5744)', function(done) {
+ it('$pull with updateValidators and $in (gh-5744)', async function() {
const exampleSchema = mongoose.Schema({
subdocuments: [{
name: String
@@ -2499,23 +2024,18 @@ describe('model: update:', function() {
subdocuments: [{ name: 'First' }, { name: 'Second' }]
};
- ExampleModel.create(exampleDocument, function(error, doc) {
- assert.ifError(error);
- ExampleModel.update({ _id: doc._id }, {
- $pull: {
- subdocuments: {
- _id: { $in: [doc.subdocuments[0]._id] }
- }
+ let doc = await ExampleModel.create(exampleDocument);
+
+ await ExampleModel.updateOne({ _id: doc._id }, {
+ $pull: {
+ subdocuments: {
+ _id: { $in: [doc.subdocuments[0]._id] }
}
- }, { runValidators: true }, function(error) {
- assert.ifError(error);
- ExampleModel.findOne({ _id: doc._id }, function(error, doc) {
- assert.ifError(error);
- assert.equal(doc.subdocuments.length, 1);
- done();
- });
- });
- });
+ }
+ }, { runValidators: true });
+ doc = await ExampleModel.findOne({ _id: doc._id });
+ assert.equal(doc.subdocuments.length, 1);
+
});
it('$pull with updateValidators and required array (gh-6341)', function() {
@@ -2578,10 +2098,10 @@ describe('model: update:', function() {
{ program: 'B', money: mongoose.Types.Decimal128.fromString('7.8') }
]
};
- await Person.update({ name: 'Jack' }, newData);
+ await Person.updateOne({ name: 'Jack' }, newData);
});
- it('strict false in query (gh-5453)', function(done) {
+ it('strict false in query (gh-5453)', async function() {
const schema = new mongoose.Schema({
date: { type: Date, required: true }
}, { strict: true });
@@ -2590,9 +2110,7 @@ describe('model: update:', function() {
const q = { notInSchema: true };
const u = { $set: { smth: 1 } };
const o = { strict: false, upsert: true };
- Model.update(q, u, o).then(function() {
- done();
- }).catch(done);
+ await Model.updateOne(q, u, o);
});
it('replaceOne with buffer (gh-6124)', function() {
@@ -2615,22 +2133,20 @@ describe('model: update:', function() {
});
});
- it('returns error if passing array as conditions (gh-3677)', function(done) {
+ it('returns error if passing array as conditions (gh-3677)', async function() {
const schema = new mongoose.Schema({
name: String
});
const Model = db.model('Test', schema);
- Model.updateMany(['foo'], { name: 'bar' }, function(error) {
- assert.ok(error);
- assert.equal(error.name, 'ObjectParameterError');
- const expected = 'Parameter "filter" to updateMany() must be an object';
- assert.ok(error.message.indexOf(expected) !== -1, error.message);
- done();
- });
+ const error = await Model.updateMany(['foo'], { name: 'bar' }).then(() => null, err => err);
+ assert.equal(error.name, 'ObjectParameterError');
+ const expected = 'Parameter "filter" to updateMany() must be an object';
+ assert.ok(error.message.indexOf(expected) !== -1, error.message);
+
});
- it('upsert: 1 (gh-5839)', function(done) {
+ it('upsert: 1 (gh-5839)', async function() {
const schema = new mongoose.Schema({
name: String
});
@@ -2638,14 +2154,11 @@ describe('model: update:', function() {
const Model = db.model('Test', schema);
const opts = { upsert: 1 };
- Model.update({ name: 'Test' }, { name: 'Test2' }, opts, function(error) {
- assert.ifError(error);
- Model.findOne({}, function(error, doc) {
- assert.ifError(error);
- assert.equal(doc.name, 'Test2');
- done();
- });
- });
+ await Model.updateOne({ name: 'Test' }, { name: 'Test2' }, opts);
+
+ const doc = await Model.findOne({});
+ assert.equal(doc.name, 'Test2');
+
});
it('casting $addToSet without $each (gh-6086)', function() {
@@ -2713,7 +2226,7 @@ describe('model: update:', function() {
]
});
- await Site.update({ 'sections.type': 'header' }, {
+ await Site.updateOne({ 'sections.type': 'header' }, {
$set: { 'sections.$.title': 'Test' }
});
@@ -2721,7 +2234,7 @@ describe('model: update:', function() {
assert.equal(doc.sections[0].title, 'Test');
});
- it('update with nested id (gh-5640)', function(done) {
+ it('update with nested id (gh-5640)', async function() {
const testSchema = new mongoose.Schema({
_id: {
a: String,
@@ -2734,7 +2247,7 @@ describe('model: update:', function() {
const Test = db.model('Test', testSchema);
- const doc = {
+ let doc = {
_id: {
a: 'a',
b: 'b'
@@ -2742,18 +2255,14 @@ describe('model: update:', function() {
foo: 'bar'
};
- Test.create(doc, function(error, doc) {
- assert.ifError(error);
- doc.foo = 'baz';
- Test.update({ _id: doc._id }, doc, { upsert: true }, function(error) {
- assert.ifError(error);
- Test.findOne({ _id: doc._id }, function(error, doc) {
- assert.ifError(error);
- assert.equal(doc.foo, 'baz');
- done();
- });
- });
- });
+ doc = await Test.create(doc);
+ doc.foo = 'baz';
+ await Test.updateOne({ _id: doc._id }, doc, { upsert: true });
+
+ doc = await Test.findOne({ _id: doc._id });
+ assert.equal(doc.foo, 'baz');
+
+
});
it('$inc cast errors (gh-6770)', async function() {
@@ -2764,7 +2273,7 @@ describe('model: update:', function() {
let threw = false;
try {
- await Test.update({}, { $inc: { num: 'not a number' } });
+ await Test.updateOne({}, { $inc: { num: 'not a number' } });
} catch (error) {
threw = true;
assert.ok(error instanceof CastError);
@@ -2774,7 +2283,7 @@ describe('model: update:', function() {
threw = false;
try {
- await Test.update({}, { $inc: { num: null } });
+ await Test.updateOne({}, { $inc: { num: null } });
} catch (error) {
threw = true;
assert.ok(error instanceof CastError);
@@ -2796,7 +2305,7 @@ describe('model: update:', function() {
const Test = db.model('Test', schema);
// Shouldn't throw an error because `capitalGainsTax` is a virtual
- return Test.update({}, { total: 10000, capitalGainsTax: 1500 });
+ return Test.updateOne({}, { total: 10000, capitalGainsTax: 1500 });
});
it('cast error in update conditions (gh-5477)', async function() {
@@ -2809,7 +2318,7 @@ describe('model: update:', function() {
const u = { $set: { name: 'Test' } };
const o = { upsert: true };
- let error = await Model.update(q, u, o).then(() => null, err => err);
+ let error = await Model.updateOne(q, u, o).then(() => null, err => err);
assert.ok(error);
assert.ok(error.message.indexOf('notAField') !== -1, error.message);
assert.ok(error.message.indexOf('upsert') !== -1, error.message);
@@ -2820,7 +2329,7 @@ describe('model: update:', function() {
assert.ok(error.message.indexOf('upsert') !== -1, error.message);
});
- it('single embedded schema under document array (gh-4519)', function(done) {
+ it('single embedded schema under document array (gh-4519)', async function() {
const PermissionSchema = new mongoose.Schema({
read: { type: Boolean, required: true },
write: Boolean
@@ -2844,13 +2353,12 @@ describe('model: update:', function() {
runValidators: true
};
- Group.update({}, update, opts, function(error) {
- assert.ok(error);
- assert.ok(error.errors['users.0.permission.read'], Object.keys(error.errors));
- done();
- });
+ const error = await Group.updateOne({}, update, opts).then(() => null, err => err);
+ assert.ok(error);
+ assert.ok(error.errors['users.0.permission.read'], Object.keys(error.errors));
});
- it('casts objects to array when clobbering with $set (gh-6532)', function(done) {
+
+ it('casts objects to array when clobbering with $set (gh-6532)', function() {
const sub = new Schema({
x: String
});
@@ -2871,9 +2379,9 @@ describe('model: update:', function() {
const obj1 = { x: 'Y' };
const set = { $set: { arr: obj1 } };
- Test.create(test).
+ return Test.create(test).
then(function() {
- return Test.update(cond, set);
+ return Test.updateOne(cond, set);
}).
then(function() {
return Test.collection.findOne({});
@@ -2881,9 +2389,7 @@ describe('model: update:', function() {
then(function(found) {
assert.ok(Array.isArray(found.arr));
assert.strictEqual(found.arr[0].x, 'Y');
- done();
- }).
- catch(done);
+ });
});
});
});
@@ -3020,6 +2526,29 @@ describe('model: updateOne: ', function() {
assert.ok(doc.createdAt.valueOf() >= start);
});
+ it('overwriting immutable createdAt (gh-8619)', async function() {
+ const start = new Date().valueOf();
+ const schema = Schema({
+ createdAt: {
+ type: mongoose.Schema.Types.Date,
+ immutable: true
+ },
+ name: String
+ }, { timestamps: true });
+
+ const Model = db.model('Test', schema);
+
+ await Model.create({ name: 'gh-8619' });
+ let doc = await Model.collection.findOne({ name: 'gh-8619' });
+ assert.ok(doc.createdAt.valueOf() >= start);
+
+ const createdAt = new Date('2011-06-01');
+ assert.ok(createdAt.valueOf() < start.valueOf());
+ await Model.updateOne({ _id: doc._id }, { name: 'gh-8619 update', createdAt }, { overwriteImmutable: true, timestamps: false });
+ doc = await Model.collection.findOne({ name: 'gh-8619 update' });
+ assert.equal(doc.createdAt.valueOf(), createdAt.valueOf());
+ });
+
it('conditional immutable (gh-8001)', async function() {
const schema = Schema({
test: {
@@ -3480,6 +3009,140 @@ describe('model: updateOne: ', function() {
);
}
});
+ it('should throw when matchedCount === 0 and using orFail() on the query gh-11620', async function() {
+ const schema = new mongoose.Schema({
+ name: String
+ });
+
+ const Person = db.model('gh-11620', schema);
+
+ const doc = await Person.create({
+ name: 'Anakin'
+ });
+
+ const res = await Person.updateOne({ _id: doc._id }, { name: 'Darth Vader' }).orFail();
+ assert.equal(res.matchedCount, 1);
+ await assert.rejects(async() => {
+ await Person.updateOne({ name: 'Anakin' }, { name: 'The Chosen One' }).orFail();
+ }, { message: 'No document found for query "{ name: \'Anakin\' }" on model "gh-11620"' });
+ });
+ it('updateOne with top level key that starts with $ (gh-13786)', async function() {
+ const [major] = await start.mongodVersion();
+ // Top-level dollar key support was added in MongoDB 5
+ if (major < 5) {
+ return this.skip();
+ }
+
+ const schema = new mongoose.Schema({
+ $myKey: String
+ });
+
+ const Test = db.model('Test', schema);
+
+ const _id = new mongoose.Types.ObjectId();
+ await Test.updateOne({ _id }, { $myKey: 'gh13786' }, { upsert: true });
+ const doc = await Test.findById(_id);
+ assert.equal(doc.$myKey, 'gh13786');
+ });
+ it('works with update validators and single nested doc with numberic paths (gh-13977)', async function() {
+ const subdoc = new mongoose.Schema({
+ 1: { type: String, required: true, validate: () => true }
+ });
+ const schema = new mongoose.Schema({ subdoc });
+ const Test = db.model('Test', schema);
+
+ const _id = new mongoose.Types.ObjectId();
+ await Test.updateOne(
+ { _id },
+ { subdoc: { 1: 'foobar' } },
+ { upsert: true, runValidators: true }
+ );
+ const doc = await Test.findById(_id);
+ assert.equal(doc.subdoc['1'], 'foobar');
+ });
+ it('handles embedded discriminators with $pull when discriminator key set in filter (gh-14675)', async function() {
+ const LoginSchema = new Schema({}, { discriminatorKey: 'type', _id: false });
+ const UserSchema = new Schema({
+ name: String,
+ login: LoginSchema
+ });
+ UserSchema.path('login').discriminator('ssh-key', new Schema({
+ keys: {
+ type: [{
+ id: { type: String, required: true },
+ publicKey: { type: String, required: true }
+ }],
+ default: []
+ }
+ }, { _id: false }));
+ const User = db.model('Test', UserSchema);
+
+ const { _id } = await User.create({
+ login: {
+ type: 'ssh-key',
+ keys: [{ id: 'my-key', publicKey: 'test' }, { id: 'test2', publicKey: 'foo' }]
+ }
+ });
+ const doc = await User.findOneAndUpdate(
+ { _id, 'login.type': 'ssh-key' },
+ { $pull: { 'login.keys': { id: 'my-key' } } },
+ { new: true }
+ ).exec();
+ assert.equal(doc.login.keys.length, 1);
+ assert.equal(doc.login.keys[0].id, 'test2');
+ });
+
+ it('casts using overwritten discriminator key schema (gh-15051)', async function() {
+ const embedDiscriminatorSchema = new mongoose.Schema({
+ field1: String
+ });
+ const embedDiscriminatorSchema2 = new mongoose.Schema({
+ field2: String
+ });
+ const embedSchema = new mongoose.Schema({
+ field: String,
+ key: String
+ }, { discriminatorKey: 'key' });
+ embedSchema.discriminator('Type1', embedDiscriminatorSchema);
+ embedSchema.discriminator('Type2', embedDiscriminatorSchema2);
+
+ const testSchema = new mongoose.Schema({
+ testArray: [embedSchema]
+ });
+
+ const TestModel = db.model('Test', testSchema);
+ const test = new TestModel({
+ testArray: [{
+ key: 'Type1',
+ field: 'field',
+ field1: 'field1'
+ }]
+ });
+ await test.save();
+
+ const field2update = 'field2 update';
+ await TestModel.updateOne(
+ { _id: test._id },
+ {
+ $set: {
+ 'testArray.$[element].key': 'Type2',
+ 'testArray.$[element].field2': field2update
+ }
+ },
+ {
+ arrayFilters: [
+ {
+ 'element._id': test.testArray[0]._id
+ }
+ ],
+ overwriteDiscriminatorKey: true
+ }
+ );
+
+ const r2 = await TestModel.findById(test._id);
+ assert.equal(r2.testArray[0].key, 'Type2');
+ assert.equal(r2.testArray[0].field2, field2update);
+ });
});
async function delay(ms) {
diff --git a/test/model.validate.test.js b/test/model.validate.test.js
index 30891867ff3..cc95e69819f 100644
--- a/test/model.validate.test.js
+++ b/test/model.validate.test.js
@@ -57,7 +57,7 @@ describe('model: validate: ', function() {
assert.deepEqual(Object.keys(err.errors), ['comments.name']);
obj = { age: '42' };
- await Model.validate(obj, ['age']);
+ obj = await Model.validate(obj, ['age']);
assert.strictEqual(obj.age, 42);
});
@@ -106,7 +106,7 @@ describe('model: validate: ', function() {
const test = { docs: ['6132655f2cdb9d94eaebc09b'] };
- const err = await Test.validate(test);
+ const err = await Test.validate(test).then(() => null, err => err);
assert.ifError(err);
});
@@ -124,7 +124,7 @@ describe('model: validate: ', function() {
const User = mongoose.model('User', userSchema);
const user = new User({ name: 'test', nameRequired: false });
- const err = await User.validate(user).catch(err => err);
+ const err = await User.validate(user).then(() => null, err => err);
assert.ifError(err);
@@ -144,4 +144,66 @@ describe('model: validate: ', function() {
const err2 = await User.validate({ name: 'Sam' }).then(() => null, err => err);
assert.ok(err2 === null);
});
+ it('Model.validate(...) supports passing in an object, array or string (gh-10353)', async function() {
+ const testSchema = new Schema({
+ docs: {
+ type: String,
+ validate: () => false
+ },
+ name: {
+ type: String,
+ validate: () => false
+ },
+ obj: {
+ type: String,
+ validate: () => false
+ },
+ arr: {
+ type: String,
+ validate: () => false
+ }
+ });
+
+ const Test = mongoose.model('Test', testSchema);
+
+ let test = { obj: 1, docs: 'a doc' };
+ let pathsOrOptions = { pathsToSkip: ['obj'] };
+ await assert.rejects(async() => {
+ await Test.validate(test, pathsOrOptions);
+ }, { message: 'Validation failed: docs: Validator failed for path `docs` with value `a doc`' });
+ test = { arr: 1, docs: 'a doc' };
+ pathsOrOptions = ['arr'];
+ await assert.rejects(async() => {
+ await Test.validate(test, pathsOrOptions);
+ }, { message: 'Validation failed: arr: Validator failed for path `arr` with value `1`' });
+ test = { name: 1, docs: 'a doc' };
+ pathsOrOptions = 'name';
+ await assert.rejects(async() => {
+ await Test.validate(test, pathsOrOptions);
+ }, { message: 'Validation failed: name: Validator failed for path `name` with value `1`' });
+ });
+
+ it('runs validation on casted paths even if cast error happened', async function() {
+ const Model = mongoose.model('Test', new Schema({
+ invalid1: {
+ type: String,
+ validate: () => false
+ },
+ myNumber: {
+ type: Number,
+ required: true
+ },
+ invalid2: {
+ type: String,
+ validate: () => false
+ }
+ }));
+
+ const err = await Model.validate({ invalid1: 'foo', myNumber: 'not a number', invalid2: 'bar' }).
+ then(() => null, err => err);
+ assert.ok(err);
+ assert.deepEqual(Object.keys(err.errors).sort(), ['invalid1', 'invalid2', 'myNumber']);
+ assert.equal(err.errors['myNumber'].name, 'CastError');
+ assert.equal(err.errors['invalid1'].name, 'ValidatorError');
+ });
});
diff --git a/test/model.watch.test.js b/test/model.watch.test.js
index 84d41dc5b14..3e2ad733a24 100644
--- a/test/model.watch.test.js
+++ b/test/model.watch.test.js
@@ -37,18 +37,22 @@ describe('model: watch: ', function() {
const changeData = await changed;
assert.equal(changeData.operationType, 'insert');
assert.equal(changeData.fullDocument.name, 'Ned Stark');
+ await changeStream.close();
});
it('watch() close() prevents buffered watch op from running (gh-7022)', async function() {
const MyModel = db.model('Test', new Schema({}));
const changeStream = MyModel.watch();
- const ready = new global.Promise(resolve => {
+ const ready = new Promise(resolve => {
changeStream.once('data', () => {
resolve(true);
});
setTimeout(resolve, 500, false);
});
+ // Change stream may still emit "MongoAPIError: ChangeStream is closed" because change stream
+ // may still poll after close.
+ changeStream.on('error', () => {});
const close = changeStream.close();
await db.asPromise();
const readyCalled = await ready;
@@ -64,12 +68,16 @@ describe('model: watch: ', function() {
await MyModel.init();
const changeStream = MyModel.watch();
- const closed = new global.Promise(resolve => {
+ const closed = new Promise(resolve => {
changeStream.once('close', () => resolve(true));
});
await MyModel.create({ name: 'Hodor' });
+ // Change stream may still emit "MongoAPIError: ChangeStream is closed" because change stream
+ // may still poll after close.
+ changeStream.on('error', () => {});
+
await changeStream.close();
const closedData = await closed;
diff --git a/test/plugin.idGetter.test.js b/test/plugin.idGetter.test.js
index 0ed9d5192e8..761346a8272 100644
--- a/test/plugin.idGetter.test.js
+++ b/test/plugin.idGetter.test.js
@@ -26,45 +26,35 @@ describe('id virtual getter', function() {
afterEach(() => require('./util').clearTestData(db));
afterEach(() => require('./util').stopRemainingOps(db));
- it('should work as expected with an ObjectId', function(done) {
+ it('should work as expected with an ObjectId', async function() {
const schema = new Schema({});
const S = db.model('Test', schema);
- S.create({}, function(err, s) {
- assert.ifError(err);
+ const s = await S.create({});
- // Comparing with virtual getter
- assert.equal(s._id.toString(), s.id);
- done();
- });
+ // Comparing with virtual getter
+ assert.equal(s._id.toString(), s.id);
});
- it('should be turned off when `id` option is set to false', function(done) {
+ it('should be turned off when `id` option is set to false', async function() {
const schema = new Schema({}, { id: false });
const S = db.model('Test', schema);
- S.create({}, function(err, s) {
- assert.ifError(err);
+ const s = await S.create({});
- // Comparing with virtual getter
- assert.equal(s.id, undefined);
- done();
- });
+ // Comparing with virtual getter
+ assert.equal(s.id, undefined);
});
-
- it('should be turned off when the schema has a set `id` path', function(done) {
+ it('should be turned off when the schema has a set `id` path', async function() {
const schema = new Schema({
id: String
});
const S = db.model('Test', schema);
- S.create({ id: 'test' }, function(err, s) {
- assert.ifError(err);
+ const s = await S.create({ id: 'test' });
- // Comparing with expected value
- assert.equal(s.id, 'test');
- done();
- });
+ // Comparing with expected value
+ assert.equal(s.id, 'test');
});
});
diff --git a/test/query.cursor.test.js b/test/query.cursor.test.js
index a4c8ea92dd0..e7265be1d06 100644
--- a/test/query.cursor.test.js
+++ b/test/query.cursor.test.js
@@ -4,6 +4,7 @@
'use strict';
+const { once } = require('events');
const start = require('./common');
const assert = require('assert');
@@ -38,21 +39,6 @@ describe('QueryCursor', function() {
});
describe('#next()', function() {
- it('with callbacks', function(done) {
- const cursor = Model.find().sort({ name: 1 }).cursor();
- cursor.next(function(error, doc) {
- assert.ifError(error);
- assert.equal(doc.name, 'Axl');
- assert.equal(doc.test, 'test');
- cursor.next(function(error, doc) {
- assert.ifError(error);
- assert.equal(doc.name, 'Slash');
- assert.equal(doc.test, 'test');
- done();
- });
- });
- });
-
it('with promises', function(done) {
const cursor = Model.find().sort({ name: 1 }).cursor();
cursor.next().then(function(doc) {
@@ -66,20 +52,15 @@ describe('QueryCursor', function() {
});
});
- it('with limit (gh-4266)', function(done) {
+ it('with limit (gh-4266)', async function() {
const cursor = Model.find().limit(1).sort({ name: 1 }).cursor();
- cursor.next(function(error, doc) {
- assert.ifError(error);
- assert.equal(doc.name, 'Axl');
- cursor.next(function(error, doc) {
- assert.ifError(error);
- assert.ok(!doc);
- done();
- });
- });
+ const doc = await cursor.next();
+ assert.equal(doc.name, 'Axl');
+ const doc2 = await cursor.next();
+ assert.ok(!doc2);
});
- it('with projection', function(done) {
+ it('with projection', async function() {
const personSchema = new Schema({
name: String,
born: String
@@ -89,23 +70,16 @@ describe('QueryCursor', function() {
{ name: 'Axl Rose', born: 'William Bruce Rose' },
{ name: 'Slash', born: 'Saul Hudson' }
];
- Person.create(people, function(error) {
- assert.ifError(error);
- const cursor = Person.find({}, { _id: 0, name: 1 }).sort({ name: 1 }).cursor();
- cursor.next(function(error, doc) {
- assert.ifError(error);
- assert.equal(doc._id, undefined);
- assert.equal(doc.name, 'Axl Rose');
- assert.equal(doc.born, undefined);
- cursor.next(function(error, doc) {
- assert.ifError(error);
- assert.equal(doc._id, undefined);
- assert.equal(doc.name, 'Slash');
- assert.equal(doc.born, undefined);
- done();
- });
- });
- });
+ await Person.create(people);
+ const cursor = Person.find({}, { _id: 0, name: 1 }).sort({ name: 1 }).cursor();
+ const doc1 = await cursor.next();
+ assert.equal(doc1._id, undefined);
+ assert.equal(doc1.name, 'Axl Rose');
+ assert.equal(doc1.born, undefined);
+ const doc2 = await cursor.next();
+ assert.equal(doc2._id, undefined);
+ assert.equal(doc2.name, 'Slash');
+ assert.equal(doc2.born, undefined);
});
describe('with populate', function() {
@@ -213,26 +187,23 @@ describe('QueryCursor', function() {
});
});
- it('casting ObjectIds with where() (gh-4355)', function(done) {
- Model.findOne(function(error, doc) {
- assert.ifError(error);
- assert.ok(doc);
- const query = { _id: doc._id.toHexString() };
- Model.find().where(query).cursor().next(function(error, doc) {
- assert.ifError(error);
- assert.ok(doc);
- done();
- });
- });
+ it('casting ObjectIds with where() (gh-4355)', async function() {
+ let doc = await Model.findOne();
+ assert.ok(doc);
+ const query = { _id: doc._id.toHexString() };
+ doc = await Model.find().where(query).cursor().next();
+ assert.ok(doc);
});
- it('cast errors (gh-4355)', function(done) {
- Model.find().where({ _id: 'BadId' }).cursor().next(function(error) {
+ it('cast errors (gh-4355)', async function() {
+ try {
+ await Model.find().where({ _id: 'BadId' }).cursor().next();
+ assert.ok(false);
+ } catch (error) {
assert.ok(error);
assert.equal(error.name, 'CastError');
assert.equal(error.path, '_id');
- done();
- });
+ }
});
it('with pre-find hooks (gh-5096)', async function() {
@@ -331,24 +302,20 @@ describe('QueryCursor', function() {
});
});
- it('with #next', function(done) {
+ it('with #next', async function() {
const cursor = Model.find().sort({ name: 1 }).cursor()
.map(function(obj) {
obj.name += '_next';
return obj;
});
- cursor.next(function(error, doc) {
- assert.ifError(error);
- assert.equal(doc.name, 'Axl_next');
- assert.equal(doc.test, 'test');
- cursor.next(function(error, doc) {
- assert.ifError(error);
- assert.equal(doc.name, 'Slash_next');
- assert.equal(doc.test, 'test');
- done();
- });
- });
+ const doc = await cursor.next();
+ assert.equal(doc.name, 'Axl_next');
+ assert.equal(doc.test, 'test');
+
+ const doc2 = await cursor.next();
+ assert.equal(doc2.name, 'Slash_next');
+ assert.equal(doc2.test, 'test');
});
});
@@ -432,28 +399,26 @@ describe('QueryCursor', function() {
});
describe('#close()', function() {
- it('works (gh-4258)', function(done) {
+ it('works (gh-4258)', async function() {
const cursor = Model.find().sort({ name: 1 }).cursor();
- cursor.next(function(error, doc) {
- assert.ifError(error);
- assert.equal(doc.name, 'Axl');
- assert.equal(doc.test, 'test');
-
- let closed = false;
- cursor.on('close', function() {
- closed = true;
- });
+ const doc = await cursor.next();
+ assert.equal(doc.name, 'Axl');
+ assert.equal(doc.test, 'test');
- cursor.close(function(error) {
- assert.ifError(error);
- assert.ok(closed);
- cursor.next(function(error) {
- assert.ok(error);
- assert.equal(error.name, 'MongoCursorExhaustedError');
- done();
- });
- });
+ let closed = false;
+ cursor.on('close', function() {
+ closed = true;
});
+
+ await cursor.close();
+ assert.ok(closed);
+ try {
+ await cursor.next();
+ assert.ok(false);
+ } catch (error) {
+ assert.equal(error.name, 'MongooseError');
+ assert.ok(error.message.includes('closed cursor'), error.message);
+ }
});
});
@@ -474,7 +439,7 @@ describe('QueryCursor', function() {
assert.ok(!doc.$__);
});
- it('data before close (gh-4998)', function(done) {
+ it('data before close (gh-4998)', async function() {
const userSchema = new mongoose.Schema({
name: String
});
@@ -488,29 +453,29 @@ describe('QueryCursor', function() {
});
}
- User.insertMany(users, function(error) {
- assert.ifError(error);
+ await User.insertMany(users);
- const stream = User.find({}).cursor();
- const docs = [];
+ const stream = User.find({}).cursor();
+ const docs = [];
- stream.on('data', function(doc) {
- docs.push(doc);
- });
+ stream.on('data', function(doc) {
+ docs.push(doc);
+ });
- stream.on('close', function() {
- assert.equal(docs.length, 100);
- done();
- });
+ await new Promise(resolve => {
+ stream.on('close', resolve);
});
+ assert.equal(docs.length, 100);
});
- it('pulls schema-level readPreference (gh-8421)', function() {
+ it('pulls schema-level readPreference (gh-8421)', async function() {
const read = 'secondaryPreferred';
const User = db.model('User', Schema({ name: String }, { read }));
const cursor = User.find().cursor();
- assert.equal(cursor.options.readPreference.mode, read);
+ await new Promise(resolve => cursor.once('cursor', resolve));
+
+ assert.equal(cursor.options.readPreference, read);
});
it('eachAsync() with parallel > numDocs (gh-8422)', async function() {
@@ -844,7 +809,6 @@ describe('QueryCursor', function() {
const docs = await Example.find().sort('foo');
assert.deepStrictEqual(docs.map(d => d.foo), ['example1', 'example2']);
});
-
it('should allow middleware to run before applying _optionsForExec() gh-13417', async function() {
const testSchema = new Schema({
a: Number,
@@ -868,6 +832,123 @@ describe('QueryCursor', function() {
assert.equal(typeof cursorMiddleSelect[0].c, 'undefined');
assert.equal(typeof cursorMiddleSelect[1].c, 'undefined');
});
+
+ it('handles skipMiddlewareFunction() (gh-13411)', async function() {
+ const schema = new mongoose.Schema({ name: String });
+
+ schema.pre('find', async() => {
+ throw mongoose.skipMiddlewareFunction();
+ });
+
+ const Movie = db.model('Movie', schema);
+
+ await Movie.deleteMany({});
+ await Movie.create([
+ { name: 'Kickboxer' },
+ { name: 'Ip Man' },
+ { name: 'Enter the Dragon' }
+ ]);
+
+ const arr = [];
+ await Movie.find({}).cursor().eachAsync(doc => arr.push(doc.name));
+ assert.strictEqual(arr.length, 0);
+ });
+
+ it('supports including fields using plus path that have select: false in schema (gh-13773)', async function() {
+ const kittySchema = new mongoose.Schema({
+ name: String,
+ age: {
+ select: false,
+ type: Number
+ }
+ });
+
+ db.deleteModel(/Test/);
+ const Kitten = db.model('Test', kittySchema);
+ await Kitten.deleteMany({});
+ const silence = new Kitten({ name: 'Silence', age: 2 });
+ await silence.save();
+
+ const cursor = Kitten.find().select('+age').cursor();
+
+ const kittens = [];
+ for await (const kitten of cursor) {
+ kittens.push(kitten);
+ }
+ assert.strictEqual(kittens.length, 1);
+ assert.strictEqual(kittens[0].name, 'Silence');
+ assert.strictEqual(kittens[0].age, 2);
+ });
+
+ it('throws if calling skipMiddlewareFunction() with non-empty array (gh-13411)', async function() {
+ const schema = new mongoose.Schema({ name: String });
+
+ schema.pre('find', (next) => {
+ next(mongoose.skipMiddlewareFunction([{ name: 'bar' }]));
+ });
+
+ const Movie = db.model('Movie', schema);
+
+ await Movie.deleteMany({});
+ await Movie.create([
+ { name: 'Kickboxer' },
+ { name: 'Ip Man' },
+ { name: 'Enter the Dragon' }
+ ]);
+
+ const err = await Movie.find().cursor().
+ eachAsync(() => {}).
+ then(() => null, err => err);
+ assert.ok(err);
+ assert.ok(err.message.includes('skipMiddlewareFunction'), err.message);
+ });
+
+ it('returns the underlying Node driver cursor with getDriverCursor()', async function() {
+ const schema = new mongoose.Schema({ name: String });
+
+ const Movie = db.model('Movie', schema);
+
+ await Movie.deleteMany({});
+ await Movie.create([
+ { name: 'Kickboxer' },
+ { name: 'Ip Man' },
+ { name: 'Enter the Dragon' }
+ ]);
+
+ const cursor = await Movie.find({}).cursor();
+ assert.ok(!cursor.cursor);
+ const driverCursor = await cursor.getDriverCursor();
+ assert.ok(cursor.cursor);
+ assert.equal(driverCursor, cursor.cursor);
+ });
+
+ it('handles destroy() (gh-14966)', async function() {
+ db.deleteModel(/Test/);
+ const TestModel = db.model('Test', mongoose.Schema({ name: String }));
+
+ const stream = await TestModel.find().cursor();
+ await once(stream, 'cursor');
+ assert.ok(!stream.cursor.closed);
+
+ stream.destroy();
+
+ await once(stream.cursor, 'close');
+ assert.ok(stream.destroyed);
+ assert.ok(stream.cursor.closed);
+ });
+
+ it('handles destroy() before cursor is created (gh-14966)', async function() {
+ db.deleteModel(/Test/);
+ const TestModel = db.model('Test', mongoose.Schema({ name: String }));
+
+ const stream = await TestModel.find().cursor();
+ assert.ok(!stream.cursor);
+ stream.destroy();
+
+ await once(stream, 'cursor');
+ assert.ok(stream.destroyed);
+ assert.ok(stream.cursor.closed);
+ });
});
async function delay(ms) {
diff --git a/test/query.middleware.test.js b/test/query.middleware.test.js
index 153189ba8d1..48c889e98f2 100644
--- a/test/query.middleware.test.js
+++ b/test/query.middleware.test.js
@@ -22,40 +22,24 @@ describe('query middleware', function() {
await db.close();
});
- const initializeData = function(done) {
+ const initializeData = async function() {
Author = db.model('Person', schema);
Publisher = db.model('Publisher', publisherSchema, 'Publisher');
- Author.deleteMany({}, function(error) {
- if (error) {
- return done(error);
- }
-
- Publisher.deleteMany({}, function(error) {
- if (error) {
- return done(error);
- }
- Publisher.create({ name: 'Wiley' }, function(error, publisher) {
- if (error) {
- return done(error);
- }
-
- const doc = {
- title: 'Professional AngularJS',
- author: 'Val',
- publisher: publisher._id,
- options: 'bacon'
- };
-
- Author.create(doc, function(error) {
- done(error);
- });
- });
- });
- });
+ await Author.deleteMany({});
+ await Publisher.deleteMany({});
+
+ const publisher = await Publisher.create({ name: 'Wiley' });
+ const doc = {
+ title: 'Professional AngularJS',
+ author: 'Val',
+ publisher: publisher._id,
+ options: 'bacon'
+ };
+ await Author.create(doc);
};
- beforeEach(function(done) {
+ beforeEach(function() {
schema = new Schema({
title: String,
author: String,
@@ -66,32 +50,25 @@ describe('query middleware', function() {
publisherSchema = new Schema({
name: String
});
-
- done();
});
beforeEach(() => db.deleteModel(/.*/));
afterEach(() => require('./util').clearTestData(db));
afterEach(() => require('./util').stopRemainingOps(db));
- it('has a pre find hook', function(done) {
+ it('has a pre find hook', async function() {
let count = 0;
schema.pre('find', function(next) {
++count;
next();
});
- initializeData(function(error) {
- assert.ifError(error);
- Author.find({ x: 1 }, function(error) {
- assert.ifError(error);
- assert.equal(count, 1);
- done();
- });
- });
+ await initializeData();
+ await Author.find({ x: 1 });
+ assert.equal(count, 1);
});
- it('has post find hooks', function(done) {
+ it('has post find hooks', async function() {
let postCount = 0;
schema.post('find', function(results, next) {
assert.equal(results.length, 1);
@@ -101,18 +78,14 @@ describe('query middleware', function() {
next();
});
- initializeData(function(error) {
- assert.ifError(error);
- Author.find({ title: 'Professional AngularJS' }, function(error, docs) {
- assert.ifError(error);
- assert.equal(postCount, 1);
- assert.equal(docs.length, 1);
- done();
- });
- });
+ await initializeData();
+
+ const docs = await Author.find({ title: 'Professional AngularJS' });
+ assert.equal(postCount, 1);
+ assert.equal(docs.length, 1);
});
- it('works when using a chained query builder', function(done) {
+ it('works when using a chained query builder', async function() {
let count = 0;
schema.pre('find', function(next) {
++count;
@@ -127,18 +100,15 @@ describe('query middleware', function() {
next();
});
- initializeData(function() {
- Author.find({ title: 'Professional AngularJS' }).exec(function(error, docs) {
- assert.ifError(error);
- assert.equal(count, 1);
- assert.equal(postCount, 1);
- assert.equal(docs.length, 1);
- done();
- });
- });
+ await initializeData();
+
+ const docs = await Author.findOne({ title: 'Professional AngularJS' }).find();
+ assert.equal(count, 1);
+ assert.equal(postCount, 1);
+ assert.equal(docs.length, 1);
});
- it('has separate pre-findOne() and post-findOne() hooks', function(done) {
+ it('has separate pre-findOne() and post-findOne() hooks', async function() {
let count = 0;
schema.pre('findOne', function(next) {
++count;
@@ -152,18 +122,14 @@ describe('query middleware', function() {
next();
});
- initializeData(function() {
- Author.findOne({ title: 'Professional AngularJS' }).exec(function(error, doc) {
- assert.ifError(error);
- assert.equal(count, 1);
- assert.equal(postCount, 1);
- assert.equal(doc.author, 'Val');
- done();
- });
- });
+ await initializeData();
+ const doc = await Author.findOne({ title: 'Professional AngularJS' });
+ assert.equal(count, 1);
+ assert.equal(postCount, 1);
+ assert.equal(doc.author, 'Val');
});
- it('with regular expression (gh-6680)', function(done) {
+ it('with regular expression (gh-6680)', async function() {
let count = 0;
let postCount = 0;
schema.pre(/find/, function(next) {
@@ -176,92 +142,52 @@ describe('query middleware', function() {
next();
});
- initializeData(function() {
- Author.findOne({ title: 'Professional AngularJS' }).exec(function(error, doc) {
- assert.ifError(error);
- assert.equal(count, 1);
- assert.equal(postCount, 1);
- assert.equal(doc.author, 'Val');
-
- count = 0;
- postCount = 0;
- Author.find({ title: 'Professional AngularJS' }, function(error, docs) {
- assert.ifError(error);
- assert.equal(count, 1);
- assert.equal(postCount, 1);
- assert.equal(docs[0].author, 'Val');
-
- Author.updateOne({}, { title: 'Foo' }, function(error) {
- assert.ifError(error);
- assert.equal(count, 1);
- assert.equal(postCount, 1);
- done();
- });
- });
- });
- });
+ await initializeData();
+
+ const doc = await Author.findOne({ title: 'Professional AngularJS' });
+ assert.equal(count, 1);
+ assert.equal(postCount, 1);
+ assert.equal(doc.author, 'Val');
+
+ count = 0;
+ postCount = 0;
+
+ const docs = await Author.find({ title: 'Professional AngularJS' });
+ assert.equal(count, 1);
+ assert.equal(postCount, 1);
+ assert.equal(docs[0].author, 'Val');
+
+ await Author.updateOne({}, { title: 'Foo' });
+ assert.equal(count, 1);
+ assert.equal(postCount, 1);
});
- it('can populate in pre hook', function(done) {
+ it('can populate in pre hook', async function() {
schema.pre('findOne', function(next) {
this.populate('publisher');
next();
});
- initializeData(function() {
- Author.findOne({ title: 'Professional AngularJS' }).exec(function(error, doc) {
- assert.ifError(error);
- assert.equal(doc.author, 'Val');
- assert.equal(doc.publisher.name, 'Wiley');
- done();
- });
- });
- });
+ await initializeData();
- it('can populate in post hook', function(done) {
- schema.post('findOne', function(doc, next) {
- doc.populate('publisher', function(error) {
- next(error);
- });
- });
+ const doc = await Author.findOne({ title: 'Professional AngularJS' }).exec();
- initializeData(function() {
- Author.findOne({ title: 'Professional AngularJS' }).exec(function(error, doc) {
- assert.ifError(error);
- assert.equal(doc.author, 'Val');
- assert.equal(doc.publisher.name, 'Wiley');
- done();
- });
- });
+ assert.equal(doc.author, 'Val');
+ assert.equal(doc.publisher.name, 'Wiley');
});
- it.skip('has hooks for count()', function(done) {
- let preCount = 0;
- let postCount = 0;
-
- schema.pre('count', function() {
- ++preCount;
- });
-
- schema.post('count', function() {
- ++postCount;
+ it('can populate in post hook', async function() {
+ schema.post('findOne', function(doc, next) {
+ doc.populate('publisher').then(() => next(), err => next(err));
});
- initializeData(function(error) {
- assert.ifError(error);
- Author.
- find({ title: 'Professional AngularJS' }).
- count(function(error, count) {
- assert.ifError(error);
- assert.equal(1, count);
- assert.equal(1, preCount);
- assert.equal(1, postCount);
- done();
- });
- });
+ await initializeData();
+ const doc = await Author.findOne({ title: 'Professional AngularJS' });
+ assert.equal(doc.author, 'Val');
+ assert.equal(doc.publisher.name, 'Wiley');
});
- it('has hooks for countDocuments()', function(done) {
+ it('has hooks for countDocuments()', async function() {
let preCount = 0;
let postCount = 0;
@@ -273,21 +199,18 @@ describe('query middleware', function() {
++postCount;
});
- initializeData(function(error) {
- assert.ifError(error);
- Author.
- find({ title: 'Professional AngularJS' }).
- countDocuments(function(error, count) {
- assert.ifError(error);
- assert.equal(1, count);
- assert.equal(1, preCount);
- assert.equal(1, postCount);
- done();
- });
- });
+ await initializeData();
+
+ const count = await Author.
+ find({ title: 'Professional AngularJS' }).
+ countDocuments();
+
+ assert.equal(1, count);
+ assert.equal(1, preCount);
+ assert.equal(1, postCount);
});
- it('has hooks for estimatedDocumentCount()', function(done) {
+ it('has hooks for estimatedDocumentCount()', async function() {
let preCount = 0;
let postCount = 0;
@@ -299,21 +222,18 @@ describe('query middleware', function() {
++postCount;
});
- initializeData(function(error) {
- assert.ifError(error);
- Author.
- find({ title: 'Professional AngularJS' }).
- estimatedDocumentCount(function(error, count) {
- assert.ifError(error);
- assert.equal(1, count);
- assert.equal(1, preCount);
- assert.equal(1, postCount);
- done();
- });
- });
+ await initializeData();
+
+ const count = await Author.
+ find({ title: 'Professional AngularJS' }).
+ estimatedDocumentCount();
+
+ assert.equal(1, count);
+ assert.equal(1, preCount);
+ assert.equal(1, postCount);
});
- it('updateOne() (gh-3997)', function(done) {
+ it('updateOne() (gh-3997)', async function() {
let preCount = 0;
let postCount = 0;
@@ -325,24 +245,20 @@ describe('query middleware', function() {
++postCount;
});
- initializeData(function(error) {
- assert.ifError(error);
- Author.
- updateOne({}, { author: 'updatedOne' }).
- exec(function(error) {
- assert.ifError(error);
- assert.equal(preCount, 1);
- assert.equal(postCount, 1);
- Author.find({ author: 'updatedOne' }, function(error, res) {
- assert.ifError(error);
- assert.equal(res.length, 1);
- done();
- });
- });
- });
+ await initializeData();
+
+ await Author.
+ updateOne({}, { author: 'updatedOne' }).
+ exec();
+
+ assert.equal(preCount, 1);
+ assert.equal(postCount, 1);
+
+ const res = await Author.find({ author: 'updatedOne' });
+ assert.equal(res.length, 1);
});
- it('updateMany() (gh-3997)', function(done) {
+ it('updateMany() (gh-3997)', async function() {
let preCount = 0;
let postCount = 0;
@@ -354,27 +270,17 @@ describe('query middleware', function() {
++postCount;
});
- initializeData(function(error) {
- assert.ifError(error);
-
- Author.create({ author: 'test' }, function(error) {
- assert.ifError(error);
- Author.
- updateMany({}, { author: 'updatedMany' }).
- exec(function(error) {
- assert.ifError(error);
- assert.equal(preCount, 1);
- assert.equal(postCount, 1);
- Author.find({}, function(error, res) {
- assert.ifError(error);
- assert.ok(res.length > 1);
- res.forEach(function(doc) {
- assert.equal(doc.author, 'updatedMany');
- });
- done();
- });
- });
- });
+ await initializeData();
+ await Author.create({ author: 'test' });
+ await Author.updateMany({}, { author: 'updatedMany' });
+
+ assert.equal(preCount, 1);
+ assert.equal(postCount, 1);
+
+ const res = await Author.find({});
+ assert.ok(res.length > 1);
+ res.forEach(function(doc) {
+ assert.equal(doc.author, 'updatedMany');
});
});
@@ -476,7 +382,7 @@ describe('query middleware', function() {
assert.equal(err.message, 'woops');
});
- it('error handlers for validate (gh-4885)', function(done) {
+ it('error handlers for validate (gh-4885)', async function() {
const testSchema = new Schema({ title: { type: String, required: true } });
let called = 0;
@@ -487,14 +393,11 @@ describe('query middleware', function() {
const Test = db.model('Test', testSchema);
- Test.create({}, function(error) {
- assert.ok(error);
- assert.equal(called, 1);
- done();
- });
+ await Test.create({}).catch(() => {});
+ assert.equal(called, 1);
});
- it('error handlers with findOneAndUpdate and passRawResult (gh-4836)', function(done) {
+ it('error handlers with findOneAndUpdate and passRawResult (gh-4836)', async function() {
const schema = new Schema({ name: { type: String } });
let called = false;
@@ -507,16 +410,14 @@ describe('query middleware', function() {
const Person = db.model('Person', schema);
- Person.
+ await Person.
findOneAndUpdate({ name: 'name' }, {}, { upsert: true, passRawResult: true }).
- exec(function(error) {
- assert.ifError(error);
- assert.ok(!called);
- done();
- });
+ exec();
+
+ assert.ok(!called);
});
- it('error handlers with findOneAndUpdate error and passRawResult (gh-4836)', function(done) {
+ it('error handlers with findOneAndUpdate error and passRawResult (gh-4836)', async function() {
const schema = new Schema({ name: { type: String } });
let called = false;
@@ -529,16 +430,15 @@ describe('query middleware', function() {
const Person = db.model('Person', schema);
- Person.
+ const err = await Person.
findOneAndUpdate({}, { _id: 'test' }, { upsert: true, passRawResult: true }).
- exec(function(error) {
- assert.ok(error);
- assert.ok(called);
- done();
- });
+ exec().
+ then(() => null, err => err);
+ assert.ok(err);
+ assert.ok(called);
});
- it('error handlers with error from pre hook (gh-4927)', function(done) {
+ it('error handlers with error from pre hook (gh-4927)', async function() {
const schema = new Schema({});
let called = false;
@@ -558,14 +458,12 @@ describe('query middleware', function() {
const Test = db.model('Test', schema);
- Test.find().exec(function(error) {
- assert.equal(error.message, 'test2');
- assert.ok(!called);
- done();
- });
+ const error = await Test.find().exec().then(() => null, err => err);
+ assert.equal(error.message, 'test2');
+ assert.ok(!called);
});
- it('with clone() (gh-5153)', function(done) {
+ it('with clone() (gh-5153)', async function() {
const schema = new Schema({});
let calledPre = 0;
let calledPost = 0;
@@ -582,12 +480,9 @@ describe('query middleware', function() {
const Test = db.model('Test', schema.clone());
- Test.find().exec(function(error) {
- assert.ifError(error);
- assert.equal(calledPre, 1);
- assert.equal(calledPost, 1);
- done();
- });
+ await Test.find();
+ assert.equal(calledPre, 1);
+ assert.equal(calledPost, 1);
});
it('doesnt double call post(regexp) with updateOne (gh-7418)', function() {
diff --git a/test/query.test.js b/test/query.test.js
index 163bc1352f3..bca5f706cfd 100644
--- a/test/query.test.js
+++ b/test/query.test.js
@@ -52,38 +52,34 @@ describe('Query', function() {
afterEach(() => require('./util').stopRemainingOps(db));
describe('constructor', function() {
- it('should not corrupt options', function(done) {
+ it('should not corrupt options', function() {
const opts = {};
const query = new Query({}, opts);
assert.notEqual(opts, query._mongooseOptions);
- done();
});
});
describe('select', function() {
- it('(object)', function(done) {
+ it('(object)', function() {
const query = new Query({});
query.select({ a: 1, b: 1, c: 0 });
assert.deepEqual(query._fields, { a: 1, b: 1, c: 0 });
- done();
});
- it('(string)', function(done) {
+ it('(string)', function() {
const query = new Query({});
query.select(' a b -c ');
- assert.deepEqual(query._fields, { a: 1, b: 1, c: 0 });
- done();
+ assert.deepEqual(query._fields, { a: 1, b: 1, '-c': 0 });
});
- it('("a","b","c")', function(done) {
+ it('("a","b","c")', function() {
assert.throws(function() {
const query = new Query({});
query.select('a', 'b', 'c');
}, /Invalid select/);
- done();
});
- it('should not overwrite fields set in prior calls', function(done) {
+ it('should not overwrite fields set in prior calls', function() {
const query = new Query({});
query.select('a');
assert.deepEqual(query._fields, { a: 1 });
@@ -93,10 +89,9 @@ describe('Query', function() {
assert.deepEqual(query._fields, { a: 1, b: 1, c: 1 });
query.select('d');
assert.deepEqual(query._fields, { a: 1, b: 1, c: 1, d: 1 });
- done();
});
- it('should remove existing fields from inclusive projection', function(done) {
+ it('should remove existing fields from inclusive projection', function() {
const query = new Query({});
query.select({
a: 1,
@@ -108,10 +103,9 @@ describe('Query', function() {
'parent2.child2': 1
}).select({ b: 0, d: 1, 'c.child': 0, parent1: 0, 'parent2.child1': 0 });
assert.deepEqual(query._fields, { a: 1, c: 1, d: 1, 'parent2.child2': 1 });
- done();
});
- it('should remove existing fields from exclusive projection', function(done) {
+ it('should remove existing fields from exclusive projection', function() {
const query = new Query({});
query.select({
a: 0,
@@ -123,7 +117,6 @@ describe('Query', function() {
'parent2.child2': 0
}).select({ b: 1, d: 0, 'c.child': 1, parent1: 1, 'parent2.child1': 1 });
assert.deepEqual(query._fields, { a: 0, c: 0, d: 0, 'parent2.child2': 0 });
- done();
});
});
@@ -143,16 +136,15 @@ describe('Query', function() {
});
describe('where', function() {
- it('works', function(done) {
+ it('works', function() {
const query = new Query({});
query.where('name', 'guillermo');
assert.deepEqual(query._conditions, { name: 'guillermo' });
query.where('a');
query.equals('b');
assert.deepEqual(query._conditions, { name: 'guillermo', a: 'b' });
- done();
});
- it('throws if non-string or non-object path is passed', function(done) {
+ it('throws if non-string or non-object path is passed', function() {
const query = new Query({});
assert.throws(function() {
query.where(50);
@@ -160,318 +152,303 @@ describe('Query', function() {
assert.throws(function() {
query.where([]);
});
- done();
});
- it('does not throw when 0 args passed', function(done) {
+ it('does not throw when 0 args passed', function() {
const query = new Query({});
assert.doesNotThrow(function() {
query.where();
});
- done();
});
});
describe('equals', function() {
- it('works', function(done) {
+ it('works', function() {
const query = new Query({});
query.where('name').equals('guillermo');
assert.deepEqual(query._conditions, { name: 'guillermo' });
- done();
});
});
describe('gte', function() {
- it('with 2 args', function(done) {
+ it('with 2 args', function() {
const query = new Query({});
query.gte('age', 18);
assert.deepEqual(query._conditions, { age: { $gte: 18 } });
- done();
});
- it('with 1 arg', function(done) {
+ it('with 1 arg', function() {
const query = new Query({});
query.where('age').gte(18);
assert.deepEqual(query._conditions, { age: { $gte: 18 } });
- done();
});
});
describe('gt', function() {
- it('with 1 arg', function(done) {
+ it('with 1 arg', function() {
const query = new Query({});
query.where('age').gt(17);
assert.deepEqual(query._conditions, { age: { $gt: 17 } });
- done();
});
- it('with 2 args', function(done) {
+ it('with 2 args', function() {
const query = new Query({});
query.gt('age', 17);
assert.deepEqual(query._conditions, { age: { $gt: 17 } });
- done();
});
});
describe('lte', function() {
- it('with 1 arg', function(done) {
+ it('with 1 arg', function() {
const query = new Query({});
query.where('age').lte(65);
assert.deepEqual(query._conditions, { age: { $lte: 65 } });
- done();
});
- it('with 2 args', function(done) {
+ it('with 2 args', function() {
const query = new Query({});
query.lte('age', 65);
assert.deepEqual(query._conditions, { age: { $lte: 65 } });
- done();
});
});
describe('lt', function() {
- it('with 1 arg', function(done) {
+ it('with 1 arg', function() {
const query = new Query({});
query.where('age').lt(66);
assert.deepEqual(query._conditions, { age: { $lt: 66 } });
- done();
});
- it('with 2 args', function(done) {
+ it('with 2 args', function() {
const query = new Query({});
query.lt('age', 66);
assert.deepEqual(query._conditions, { age: { $lt: 66 } });
- done();
});
});
describe('combined', function() {
describe('lt and gt', function() {
- it('works', function(done) {
+ it('works', function() {
const query = new Query({});
query.where('age').lt(66).gt(17);
assert.deepEqual(query._conditions, { age: { $lt: 66, $gt: 17 } });
- done();
});
});
});
describe('tl on one path and gt on another', function() {
- it('works', function(done) {
+ it('works', function() {
const query = new Query({});
query
.where('age').lt(66)
.where('height').gt(5);
assert.deepEqual(query._conditions, { age: { $lt: 66 }, height: { $gt: 5 } });
- done();
});
});
describe('ne', function() {
- it('with 1 arg', function(done) {
+ it('with 1 arg', function() {
const query = new Query({});
query.where('age').ne(21);
assert.deepEqual(query._conditions, { age: { $ne: 21 } });
- done();
});
- it('with 2 args', function(done) {
+ it('with 2 args', function() {
const query = new Query({});
query.ne('age', 21);
assert.deepEqual(query._conditions, { age: { $ne: 21 } });
- done();
});
});
describe('in', function() {
- it('with 1 arg', function(done) {
+ it('with 1 arg', function() {
const query = new Query({});
query.where('age').in([21, 25, 30]);
assert.deepEqual(query._conditions, { age: { $in: [21, 25, 30] } });
- done();
+
});
- it('with 2 args', function(done) {
+ it('with 2 args', function() {
const query = new Query({});
query.in('age', [21, 25, 30]);
assert.deepEqual(query._conditions, { age: { $in: [21, 25, 30] } });
- done();
+
});
- it('where a non-array value no via where', function(done) {
+ it('where a non-array value no via where', function() {
const query = new Query({});
query.in('age', 21);
assert.deepEqual(query._conditions, { age: { $in: 21 } });
- done();
+
});
- it('where a non-array value via where', function(done) {
+ it('where a non-array value via where', function() {
const query = new Query({});
query.where('age').in(21);
assert.deepEqual(query._conditions, { age: { $in: 21 } });
- done();
+
});
});
describe('nin', function() {
- it('with 1 arg', function(done) {
+ it('with 1 arg', function() {
const query = new Query({});
query.where('age').nin([21, 25, 30]);
assert.deepEqual(query._conditions, { age: { $nin: [21, 25, 30] } });
- done();
+
});
- it('with 2 args', function(done) {
+ it('with 2 args', function() {
const query = new Query({});
query.nin('age', [21, 25, 30]);
assert.deepEqual(query._conditions, { age: { $nin: [21, 25, 30] } });
- done();
+
});
- it('with a non-array value not via where', function(done) {
+ it('with a non-array value not via where', function() {
const query = new Query({});
query.nin('age', 21);
assert.deepEqual(query._conditions, { age: { $nin: 21 } });
- done();
+
});
- it('with a non-array value via where', function(done) {
+ it('with a non-array value via where', function() {
const query = new Query({});
query.where('age').nin(21);
assert.deepEqual(query._conditions, { age: { $nin: 21 } });
- done();
+
});
});
describe('mod', function() {
- it('not via where, where [a, b] param', function(done) {
+ it('not via where, where [a, b] param', function() {
const query = new Query({});
query.mod('age', [5, 2]);
assert.deepEqual(query._conditions, { age: { $mod: [5, 2] } });
- done();
+
});
- it('not via where, where a and b params', function(done) {
+ it('not via where, where a and b params', function() {
const query = new Query({});
query.mod('age', 5, 2);
assert.deepEqual(query._conditions, { age: { $mod: [5, 2] } });
- done();
+
});
- it('via where, where [a, b] param', function(done) {
+ it('via where, where [a, b] param', function() {
const query = new Query({});
query.where('age').mod([5, 2]);
assert.deepEqual(query._conditions, { age: { $mod: [5, 2] } });
- done();
+
});
- it('via where, where a and b params', function(done) {
+ it('via where, where a and b params', function() {
const query = new Query({});
query.where('age').mod(5, 2);
assert.deepEqual(query._conditions, { age: { $mod: [5, 2] } });
- done();
+
});
});
describe('near', function() {
- it('via where, where { center :[lat, long]} param', function(done) {
+ it('via where, where { center :[lat, long]} param', function() {
const query = new Query({});
query.where('checkin').near({ center: [40, -72] });
assert.deepEqual(query._conditions, { checkin: { $near: [40, -72] } });
- done();
+
});
- it('via where, where [lat, long] param', function(done) {
+ it('via where, where [lat, long] param', function() {
const query = new Query({});
query.where('checkin').near([40, -72]);
assert.deepEqual(query._conditions, { checkin: { $near: [40, -72] } });
- done();
+
});
- it('via where, where lat and long params', function(done) {
+ it('via where, where lat and long params', function() {
const query = new Query({});
query.where('checkin').near(40, -72);
assert.deepEqual(query._conditions, { checkin: { $near: [40, -72] } });
- done();
+
});
- it('not via where, where [lat, long] param', function(done) {
+ it('not via where, where [lat, long] param', function() {
const query = new Query({});
query.near('checkin', [40, -72]);
assert.deepEqual(query._conditions, { checkin: { $near: [40, -72] } });
- done();
+
});
- it('not via where, where lat and long params', function(done) {
+ it('not via where, where lat and long params', function() {
const query = new Query({});
query.near('checkin', 40, -72);
assert.deepEqual(query._conditions, { checkin: { $near: [40, -72] } });
- done();
+
});
- it('via where, where GeoJSON param', function(done) {
+ it('via where, where GeoJSON param', function() {
const query = new Query({});
query.where('numbers').near({ center: { type: 'Point', coordinates: [40, -72] } });
assert.deepEqual(query._conditions, { numbers: { $near: { $geometry: { type: 'Point', coordinates: [40, -72] } } } });
assert.doesNotThrow(function() {
query.cast(db.model('Product', productSchema));
});
- done();
+
});
- it('with path, where GeoJSON param', function(done) {
+ it('with path, where GeoJSON param', function() {
const query = new Query({});
query.near('loc', { center: { type: 'Point', coordinates: [40, -72] } });
assert.deepEqual(query._conditions, { loc: { $near: { $geometry: { type: 'Point', coordinates: [40, -72] } } } });
- done();
+
});
});
describe('nearSphere', function() {
- it('via where, where [lat, long] param', function(done) {
+ it('via where, where [lat, long] param', function() {
const query = new Query({});
query.where('checkin').nearSphere([40, -72]);
assert.deepEqual(query._conditions, { checkin: { $nearSphere: [40, -72] } });
- done();
+
});
- it('via where, where lat and long params', function(done) {
+ it('via where, where lat and long params', function() {
const query = new Query({});
query.where('checkin').nearSphere(40, -72);
assert.deepEqual(query._conditions, { checkin: { $nearSphere: [40, -72] } });
- done();
+
});
- it('not via where, where [lat, long] param', function(done) {
+ it('not via where, where [lat, long] param', function() {
const query = new Query({});
query.nearSphere('checkin', [40, -72]);
assert.deepEqual(query._conditions, { checkin: { $nearSphere: [40, -72] } });
- done();
+
});
- it('not via where, where lat and long params', function(done) {
+ it('not via where, where lat and long params', function() {
const query = new Query({});
query.nearSphere('checkin', 40, -72);
assert.deepEqual(query._conditions, { checkin: { $nearSphere: [40, -72] } });
- done();
+
});
- it('via where, with object', function(done) {
+ it('via where, with object', function() {
const query = new Query({});
query.where('checkin').nearSphere({ center: [20, 23], maxDistance: 2 });
assert.deepEqual(query._conditions, { checkin: { $nearSphere: [20, 23], $maxDistance: 2 } });
- done();
+
});
- it('via where, where GeoJSON param', function(done) {
+ it('via where, where GeoJSON param', function() {
const query = new Query({});
query.where('numbers').nearSphere({ center: { type: 'Point', coordinates: [40, -72] } });
assert.deepEqual(query._conditions, { numbers: { $nearSphere: { $geometry: { type: 'Point', coordinates: [40, -72] } } } });
assert.doesNotThrow(function() {
query.cast(db.model('Product', productSchema));
});
- done();
+
});
- it('with path, with GeoJSON', function(done) {
+ it('with path, with GeoJSON', function() {
const query = new Query({});
query.nearSphere('numbers', { center: { type: 'Point', coordinates: [40, -72] } });
assert.deepEqual(query._conditions, { numbers: { $nearSphere: { $geometry: { type: 'Point', coordinates: [40, -72] } } } });
assert.doesNotThrow(function() {
query.cast(db.model('Product', productSchema));
});
- done();
+
});
});
describe('maxDistance', function() {
- it('via where', function(done) {
+ it('via where', function() {
const query = new Query({});
query.where('checkin').near([40, -72]).maxDistance(1);
assert.deepEqual(query._conditions, { checkin: { $near: [40, -72], $maxDistance: 1 } });
- done();
+
});
});
describe('within', function() {
describe('box', function() {
- it('via where', function(done) {
+ it('via where', function() {
const query = new Query({});
query.where('gps').within().box({ ll: [5, 25], ur: [10, 30] });
const match = { gps: { $within: { $box: [[5, 25], [10, 30]] } } };
@@ -480,9 +457,9 @@ describe('Query', function() {
delete match.gps.$within;
}
assert.deepEqual(query._conditions, match);
- done();
+
});
- it('via where, no object', function(done) {
+ it('via where, no object', function() {
const query = new Query({});
query.where('gps').within().box([5, 25], [10, 30]);
const match = { gps: { $within: { $box: [[5, 25], [10, 30]] } } };
@@ -491,12 +468,12 @@ describe('Query', function() {
delete match.gps.$within;
}
assert.deepEqual(query._conditions, match);
- done();
+
});
});
describe('center', function() {
- it('via where', function(done) {
+ it('via where', function() {
const query = new Query({});
query.where('gps').within().center({ center: [5, 25], radius: 5 });
const match = { gps: { $within: { $center: [[5, 25], 5] } } };
@@ -505,12 +482,12 @@ describe('Query', function() {
delete match.gps.$within;
}
assert.deepEqual(query._conditions, match);
- done();
+
});
});
describe('centerSphere', function() {
- it('via where', function(done) {
+ it('via where', function() {
const query = new Query({});
query.where('gps').within().centerSphere({ center: [5, 25], radius: 5 });
const match = { gps: { $within: { $centerSphere: [[5, 25], 5] } } };
@@ -519,12 +496,12 @@ describe('Query', function() {
delete match.gps.$within;
}
assert.deepEqual(query._conditions, match);
- done();
+
});
});
describe('polygon', function() {
- it('via where', function(done) {
+ it('via where', function() {
const query = new Query({});
query.where('gps').within().polygon({ a: { x: 10, y: 20 }, b: { x: 15, y: 25 }, c: { x: 20, y: 20 } });
const match = { gps: { $within: { $polygon: [{ a: { x: 10, y: 20 }, b: { x: 15, y: 25 }, c: { x: 20, y: 20 } }] } } };
@@ -533,62 +510,62 @@ describe('Query', function() {
delete match.gps.$within;
}
assert.deepEqual(query._conditions, match);
- done();
+
});
});
});
describe('exists', function() {
- it('0 args via where', function(done) {
+ it('0 args via where', function() {
const query = new Query({});
query.where('username').exists();
assert.deepEqual(query._conditions, { username: { $exists: true } });
- done();
+
});
- it('1 arg via where', function(done) {
+ it('1 arg via where', function() {
const query = new Query({});
query.where('username').exists(false);
assert.deepEqual(query._conditions, { username: { $exists: false } });
- done();
+
});
- it('where 1 argument not via where', function(done) {
+ it('where 1 argument not via where', function() {
const query = new Query({});
query.exists('username');
assert.deepEqual(query._conditions, { username: { $exists: true } });
- done();
+
});
- it('where 2 args not via where', function(done) {
+ it('where 2 args not via where', function() {
const query = new Query({});
query.exists('username', false);
assert.deepEqual(query._conditions, { username: { $exists: false } });
- done();
+
});
});
describe('all', function() {
- it('via where', function(done) {
+ it('via where', function() {
const query = new Query({});
query.where('pets').all(['dog', 'cat', 'ferret']);
assert.deepEqual(query._conditions, { pets: { $all: ['dog', 'cat', 'ferret'] } });
- done();
+
});
- it('not via where', function(done) {
+ it('not via where', function() {
const query = new Query({});
query.all('pets', ['dog', 'cat', 'ferret']);
assert.deepEqual(query._conditions, { pets: { $all: ['dog', 'cat', 'ferret'] } });
- done();
+
});
});
describe('find', function() {
- it('strict array equivalence condition v', function(done) {
+ it('strict array equivalence condition v', function() {
const query = new Query({});
query.find({ pets: ['dog', 'cat', 'ferret'] });
assert.deepEqual(query._conditions, { pets: ['dog', 'cat', 'ferret'] });
- done();
+
});
- it('with no args', function(done) {
+ it('with no args', function() {
let threw = false;
const q = new Query({});
@@ -599,149 +576,149 @@ describe('Query', function() {
}
assert.ok(!threw);
- done();
+
});
- it('works with overwriting previous object args (1176)', function(done) {
+ it('works with overwriting previous object args (1176)', function() {
const q = new Query({});
assert.doesNotThrow(function() {
q.find({ age: { $lt: 30 } });
q.find({ age: 20 }); // overwrite
});
assert.deepEqual({ age: 20 }, q._conditions);
- done();
+
});
});
describe('size', function() {
- it('via where', function(done) {
+ it('via where', function() {
const query = new Query({});
query.where('collection').size(5);
assert.deepEqual(query._conditions, { collection: { $size: 5 } });
- done();
+
});
- it('not via where', function(done) {
+ it('not via where', function() {
const query = new Query({});
query.size('collection', 5);
assert.deepEqual(query._conditions, { collection: { $size: 5 } });
- done();
+
});
});
describe('slice', function() {
- it('where and positive limit param', function(done) {
+ it('where and positive limit param', function() {
const query = new Query({});
query.where('collection').slice(5);
assert.deepEqual(query._fields, { collection: { $slice: 5 } });
- done();
+
});
- it('where just negative limit param', function(done) {
+ it('where just negative limit param', function() {
const query = new Query({});
query.where('collection').slice(-5);
assert.deepEqual(query._fields, { collection: { $slice: -5 } });
- done();
+
});
- it('where [skip, limit] param', function(done) {
+ it('where [skip, limit] param', function() {
const query = new Query({});
query.where('collection').slice([14, 10]); // Return the 15th through 25th
assert.deepEqual(query._fields, { collection: { $slice: [14, 10] } });
- done();
+
});
- it('where skip and limit params', function(done) {
+ it('where skip and limit params', function() {
const query = new Query({});
query.where('collection').slice(14, 10); // Return the 15th through 25th
assert.deepEqual(query._fields, { collection: { $slice: [14, 10] } });
- done();
+
});
- it('where just positive limit param', function(done) {
+ it('where just positive limit param', function() {
const query = new Query({});
query.where('collection').slice(5);
assert.deepEqual(query._fields, { collection: { $slice: 5 } });
- done();
+
});
- it('where just negative limit param', function(done) {
+ it('where just negative limit param', function() {
const query = new Query({});
query.where('collection').slice(-5);
assert.deepEqual(query._fields, { collection: { $slice: -5 } });
- done();
+
});
- it('where the [skip, limit] param', function(done) {
+ it('where the [skip, limit] param', function() {
const query = new Query({});
query.where('collection').slice([14, 10]); // Return the 15th through 25th
assert.deepEqual(query._fields, { collection: { $slice: [14, 10] } });
- done();
+
});
- it('where the skip and limit params', function(done) {
+ it('where the skip and limit params', function() {
const query = new Query({});
query.where('collection').slice(14, 10); // Return the 15th through 25th
assert.deepEqual(query._fields, { collection: { $slice: [14, 10] } });
- done();
+
});
- it('not via where, with just positive limit param', function(done) {
+ it('not via where, with just positive limit param', function() {
const query = new Query({});
query.slice('collection', 5);
assert.deepEqual(query._fields, { collection: { $slice: 5 } });
- done();
+
});
- it('not via where, where just negative limit param', function(done) {
+ it('not via where, where just negative limit param', function() {
const query = new Query({});
query.slice('collection', -5);
assert.deepEqual(query._fields, { collection: { $slice: -5 } });
- done();
+
});
- it('not via where, where [skip, limit] param', function(done) {
+ it('not via where, where [skip, limit] param', function() {
const query = new Query({});
query.slice('collection', [14, 10]); // Return the 15th through 25th
assert.deepEqual(query._fields, { collection: { $slice: [14, 10] } });
- done();
+
});
- it('not via where, where skip and limit params', function(done) {
+ it('not via where, where skip and limit params', function() {
const query = new Query({});
query.slice('collection', 14, 10); // Return the 15th through 25th
assert.deepEqual(query._fields, { collection: { $slice: [14, 10] } });
- done();
+
});
});
describe('elemMatch', function() {
describe('not via where', function() {
- it('works', function(done) {
+ it('works', function() {
const query = new Query({});
query.elemMatch('comments', { author: 'bnoguchi', votes: { $gte: 5 } });
assert.deepEqual(query._conditions, { comments: { $elemMatch: { author: 'bnoguchi', votes: { $gte: 5 } } } });
- done();
+
});
- it('where block notation', function(done) {
+ it('where block notation', function() {
const query = new Query({});
query.elemMatch('comments', function(elem) {
elem.where('author', 'bnoguchi');
elem.where('votes').gte(5);
});
assert.deepEqual(query._conditions, { comments: { $elemMatch: { author: 'bnoguchi', votes: { $gte: 5 } } } });
- done();
+
});
});
describe('via where', function() {
- it('works', function(done) {
+ it('works', function() {
const query = new Query({});
query.where('comments').elemMatch({ author: 'bnoguchi', votes: { $gte: 5 } });
assert.deepEqual(query._conditions, { comments: { $elemMatch: { author: 'bnoguchi', votes: { $gte: 5 } } } });
- done();
+
});
- it('where block notation', function(done) {
+ it('where block notation', function() {
const query = new Query({});
query.where('comments').elemMatch(function(elem) {
elem.where('author', 'bnoguchi');
elem.where('votes').gte(5);
});
assert.deepEqual(query._conditions, { comments: { $elemMatch: { author: 'bnoguchi', votes: { $gte: 5 } } } });
- done();
+
});
});
});
describe('$where', function() {
- it('function arg', function(done) {
+ it('function arg', function() {
const query = new Query({});
function filter() {
@@ -750,22 +727,22 @@ describe('Query', function() {
query.$where(filter);
assert.deepEqual(query._conditions, { $where: filter });
- done();
+
});
- it('string arg', function(done) {
+ it('string arg', function() {
const query = new Query({});
query.$where('this.lastName === this.firstName');
assert.deepEqual(query._conditions, { $where: 'this.lastName === this.firstName' });
- done();
+
});
});
describe('limit', function() {
- it('works', function(done) {
+ it('works', function() {
const query = new Query({});
query.limit(5);
assert.strictEqual(query.options.limit, 5);
- done();
+
});
it('with string limit (gh-11017)', function() {
@@ -778,16 +755,16 @@ describe('Query', function() {
});
describe('skip', function() {
- it('works', function(done) {
+ it('works', function() {
const query = new Query({});
query.skip(9);
assert.equal(query.options.skip, 9);
- done();
+
});
});
describe('sort', function() {
- it('works', function(done) {
+ it('works', function() {
let query = new Query({});
query.sort('a -c b');
assert.deepEqual(query.options.sort, { a: 1, c: -1, b: 1 });
@@ -795,12 +772,9 @@ describe('Query', function() {
query.sort({ a: 1, c: -1, b: 'asc', e: 'descending', f: 'ascending' });
assert.deepEqual(query.options.sort, { a: 1, c: -1, b: 1, e: -1, f: 1 });
- if (typeof global.Map !== 'undefined') {
- query = new Query({});
- query.sort(new global.Map().set('a', 1).set('b', 1));
- assert.equal(query.options.sort.get('a'), 1);
- assert.equal(query.options.sort.get('b'), 1);
- }
+ query = new Query({});
+ query.sort(new Map().set('a', 1).set('b', 1));
+ assert.deepStrictEqual(query.options.sort, { a: 1, b: 1 });
query = new Query({});
let e;
@@ -821,13 +795,13 @@ describe('Query', function() {
e = err;
}
assert.ok(e, 'uh oh. no error was thrown');
- assert.equal(e.message, 'sort() only takes 1 Argument');
- done();
+ assert.equal(e.message, 'sort() takes at most 2 arguments');
+
});
});
describe('or', function() {
- it('works', function(done) {
+ it('works', function() {
const query = new Query();
query.find({ $or: [{ x: 1 }, { x: 2 }] });
assert.equal(query._conditions.$or.length, 2);
@@ -838,12 +812,12 @@ describe('Query', function() {
assert.equal(query._conditions.$or.length, 5);
assert.equal(query._conditions.$or[3].z, 47);
assert.equal(query._conditions.$or[4].z, 'phew');
- done();
+
});
});
describe('and', function() {
- it('works', function(done) {
+ it('works', function() {
const query = new Query();
query.find({ $and: [{ x: 1 }, { y: 2 }] });
assert.equal(query._conditions.$and.length, 2);
@@ -857,12 +831,12 @@ describe('Query', function() {
assert.equal(query._conditions.$and[2].z, 'We\'re under attack');
assert.equal(query._conditions.$and[3].w, 47);
assert.equal(query._conditions.$and[4].a, 'phew');
- done();
+
});
});
describe('populate', function() {
- it('converts to PopulateOptions objects', function(done) {
+ it('converts to PopulateOptions objects', function() {
const q = new Query({});
const o = {
path: 'yellow.brick',
@@ -875,10 +849,10 @@ describe('Query', function() {
};
q.populate(o);
assert.deepEqual(o, q._mongooseOptions.populate['yellow.brick']);
- done();
+
});
- it('overwrites duplicate paths', function(done) {
+ it('overwrites duplicate paths', function() {
const q = new Query({});
let o = {
path: 'yellow.brick',
@@ -898,10 +872,10 @@ describe('Query', function() {
};
assert.equal(Object.keys(q._mongooseOptions.populate).length, 1);
assert.deepEqual(q._mongooseOptions.populate['yellow.brick'], o);
- done();
+
});
- it('accepts space delimited strings', function(done) {
+ it('accepts space delimited strings', function() {
const q = new Query({});
q.populate('yellow.brick dirt');
assert.equal(Object.keys(q._mongooseOptions.populate).length, 2);
@@ -915,18 +889,18 @@ describe('Query', function() {
_docs: {},
_childDocs: []
});
- done();
+
});
});
describe('casting', function() {
- it('to an array of mixed', function(done) {
+ it('to an array of mixed', function() {
const query = new Query({});
const Product = db.model('Product', productSchema);
const params = { _id: new DocumentObjectId(), tags: { $in: [4, 8, 15, 16] } };
query.cast(Product, params);
assert.deepEqual(params.tags.$in, [4, 8, 15, 16]);
- done();
+
});
it('doesn\'t wipe out $in (gh-6439)', async function() {
@@ -967,7 +941,7 @@ describe('Query', function() {
assert.strictEqual(found.props[0].name, 'abc');
});
- it('find $ne should not cast single value to array for schematype of Array', function(done) {
+ it('find $ne should not cast single value to array for schematype of Array', function() {
const query = new Query({});
const Product = db.model('Product', productSchema);
const Comment = db.model('Comment', commentSchema);
@@ -1008,10 +982,10 @@ describe('Query', function() {
assert.equal(params.strings.$ne[0], 'Hi there');
assert.ok(params.numbers.$ne instanceof Array);
assert.equal(params.numbers.$ne[0], 10000);
- done();
+
});
- it('subdocument array with $ne: null should not throw', function(done) {
+ it('subdocument array with $ne: null should not throw', function() {
const query = new Query({});
const Product = db.model('Product', productSchema);
@@ -1021,10 +995,10 @@ describe('Query', function() {
query.cast(Product, params);
assert.strictEqual(params.comments.$ne, null);
- done();
+
});
- it('find should not cast single value to array for schematype of Array', function(done) {
+ it('find should not cast single value to array for schematype of Array', function() {
const query = new Query({});
const Product = db.model('Product', productSchema);
const Comment = db.model('Comment', commentSchema);
@@ -1065,10 +1039,10 @@ describe('Query', function() {
assert.equal(params.strings[0], 'Hi there');
assert.ok(params.numbers instanceof Array);
assert.equal(params.numbers[0], 10000);
- done();
+
});
- it('an $elemMatch with $in works (gh-1100)', function(done) {
+ it('an $elemMatch with $in works (gh-1100)', function() {
const query = new Query({});
const Product = db.model('Product', productSchema);
const ids = [String(new DocumentObjectId()), String(new DocumentObjectId())];
@@ -1078,10 +1052,10 @@ describe('Query', function() {
assert.ok(params.ids.$elemMatch.$in[1] instanceof DocumentObjectId);
assert.deepEqual(params.ids.$elemMatch.$in[0].toString(), ids[0]);
assert.deepEqual(params.ids.$elemMatch.$in[1].toString(), ids[1]);
- done();
+
});
- it('inequality operators for an array', function(done) {
+ it('inequality operators for an array', function() {
const query = new Query({});
const Product = db.model('Product', productSchema);
const Comment = db.model('Comment', commentSchema);
@@ -1102,7 +1076,7 @@ describe('Query', function() {
assert.deepEqual(params.comments.$gt.toObject(), castedComment);
assert.equal(params.strings.$gt, 'Hi there');
assert.equal(params.numbers.$gt, 10000);
- done();
+
});
});
@@ -1114,32 +1088,26 @@ describe('Query', function() {
assert.equal(q.op, 'distinct');
});
+
+ it('using options parameter for distinct', function() {
+ const q = new Query({});
+ const options = { collation: { locale: 'en', strength: 2 } };
+
+ q.distinct('blah', {}, options);
+
+ assert.equal(q.op, 'distinct');
+ assert.deepEqual(q.options.collation, options.collation);
+ });
});
describe('findOne', function() {
- it('sets the op', function(done) {
+ it('sets the op', function() {
const Product = db.model('Product', productSchema);
const prod = new Product({});
const q = new Query(prod.collection, {}, Product).distinct();
- // use a timeout here because we have to wait for the connection to start
- // before any ops will get set
- setTimeout(function() {
- assert.equal(q.op, 'distinct');
- q.findOne();
- assert.equal(q.op, 'findOne');
- done();
- }, 50);
- });
-
- it('works as a promise', function(done) {
- const Product = db.model('Product', productSchema);
- const promise = Product.findOne();
-
- promise.then(function() {
- done();
- }, function(err) {
- assert.ifError(err);
- });
+ assert.equal(q.op, 'distinct');
+ q.findOne();
+ assert.equal(q.op, 'findOne');
});
});
@@ -1171,96 +1139,67 @@ describe('Query', function() {
});
});
- describe('remove', function() {
- it('handles cast errors async', function(done) {
+ describe('deleteMany', function() {
+ it('handles cast errors async', async function() {
const Product = db.model('Product', productSchema);
- assert.doesNotThrow(function() {
- Product.where({ numbers: [[[]]] }).deleteMany(function(err) {
- assert.ok(err);
- done();
- });
- });
+ const err = await Product.where({ numbers: [[[]]] }).deleteMany().then(() => null, err => err);
+ assert.ok(err);
+ assert.equal(err.name, 'CastError');
});
- it('supports a single conditions arg', function(done) {
+ it('supports a single conditions arg', async function() {
const Product = db.model('Product', productSchema);
- Product.create({ strings: ['remove-single-condition'] }).then(function() {
- const q = Product.where().deleteMany({ strings: 'remove-single-condition' });
- assert.ok(q instanceof mongoose.Query);
- done();
- }, done);
+ await Product.create({ strings: ['remove-single-condition'] });
+ const q = Product.where().deleteMany({ strings: 'remove-single-condition' });
+ assert.ok(q instanceof mongoose.Query);
});
- it('supports a single callback arg', function(done) {
+ it('supports a single callback arg', async function() {
const Product = db.model('Product', productSchema);
const val = 'remove-single-callback';
- Product.create({ strings: [val] }).then(function() {
- Product.where({ strings: val }).deleteMany(function(err) {
- assert.ifError(err);
- Product.findOne({ strings: val }, function(err, doc) {
- assert.ifError(err);
- assert.ok(!doc);
- done();
- });
- });
- }, done);
+ await Product.create({ strings: [val] });
+ await Product.where({ strings: val }).deleteMany();
+ const doc = await Product.findOne({ strings: val });
+ assert.ok(!doc);
});
- it('supports conditions and callback args', function(done) {
+ it('supports conditions and callback args', async function() {
const Product = db.model('Product', productSchema);
const val = 'remove-cond-and-callback';
- Product.create({ strings: [val] }).then(function() {
- Product.where().deleteMany({ strings: val }, function(err) {
- assert.ifError(err);
- Product.findOne({ strings: val }, function(err, doc) {
- assert.ifError(err);
- assert.ok(!doc);
- done();
- });
- });
- }, done);
+ await Product.create({ strings: [val] });
+ await Product.where().deleteMany({ strings: val });
+ const doc = await Product.findOne({ strings: val });
+ assert.ok(!doc);
});
});
describe('querying/updating with model instance containing embedded docs should work (#454)', function() {
- it('works', function(done) {
+ it('works', async function() {
const Product = db.model('Product', productSchema);
const proddoc = { comments: [{ text: 'hello' }] };
const prod2doc = { comments: [{ text: 'goodbye' }] };
const prod = new Product(proddoc);
- prod.save(function(err) {
- assert.ifError(err);
-
- Product.findOne({ _id: prod._id }, function(err, product) {
- assert.ifError(err);
- assert.equal(product.comments.length, 1);
- assert.equal(product.comments[0].text, 'hello');
-
- Product.updateOne({ _id: prod._id }, prod2doc, function(err) {
- assert.ifError(err);
-
- Product.collection.findOne({ _id: product._id }, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.comments.length, 1);
- // ensure hidden private props were not saved to db
- assert.ok(!doc.comments[0].hasOwnProperty('parentArry'));
- assert.equal(doc.comments[0].text, 'goodbye');
- done();
- });
- });
- });
- });
+ await prod.save();
+
+ const product = await Product.findOne({ _id: prod._id });
+ assert.equal(product.comments.length, 1);
+ assert.equal(product.comments[0].text, 'hello');
+ await Product.updateOne({ _id: prod._id }, prod2doc);
+ const doc = await Product.collection.findOne({ _id: product._id });
+
+ assert.ok(!doc.comments[0].hasOwnProperty('parentArry'));
+ assert.equal(doc.comments[0].text, 'goodbye');
});
});
describe('optionsForExec', function() {
- it('should retain key order', function(done) {
+ it('should retain key order', function() {
// this is important for query hints
const hint = { x: 1, y: 1, z: 1 };
const a = JSON.stringify({ hint: hint });
@@ -1270,10 +1209,9 @@ describe('Query', function() {
const options = q._optionsForExec();
assert.equal(JSON.stringify(options), a);
- done();
});
- it('applies schema-level writeConcern option', function(done) {
+ it('applies schema-level writeConcern option', function() {
const q = new Query();
q.j(true);
@@ -1291,10 +1229,9 @@ describe('Query', function() {
j: true
}
});
- done();
});
- it('session() (gh-6663)', function(done) {
+ it('session() (gh-6663)', function() {
const q = new Query();
const fakeSession = 'foo';
@@ -1304,41 +1241,14 @@ describe('Query', function() {
assert.deepEqual(options, {
session: fakeSession
});
- done();
});
});
// Advanced Query options
describe('options', function() {
- describe('maxscan', function() {
- it('works', function(done) {
- const query = new Query({});
- query.maxscan(100);
- assert.equal(query.options.maxScan, 100);
- done();
- });
- });
-
- describe('slaveOk', function() {
- it('works', function(done) {
- let query = new Query({});
- query.slaveOk();
- assert.equal(query.options.slaveOk, true);
-
- query = new Query({});
- query.slaveOk(true);
- assert.equal(query.options.slaveOk, true);
-
- query = new Query({});
- query.slaveOk(false);
- assert.equal(query.options.slaveOk, false);
- done();
- });
- });
-
describe('tailable', function() {
- it('works', function(done) {
+ it('works', function() {
let query = new Query({});
query.tailable();
assert.equal(query.options.tailable, true);
@@ -1350,29 +1260,26 @@ describe('Query', function() {
query = new Query({});
query.tailable(false);
assert.equal(query.options.tailable, false);
- done();
});
- it('supports passing the `awaitData` option', function(done) {
+ it('supports passing the `awaitData` option', function() {
const query = new Query({});
query.tailable({ awaitData: true });
assert.equal(query.options.tailable, true);
assert.equal(query.options.awaitData, true);
- done();
});
});
describe('comment', function() {
- it('works', function(done) {
+ it('works', function() {
const query = new Query();
assert.equal(typeof query.comment, 'function');
assert.equal(query.comment('Lowpass is more fun'), query);
assert.equal(query.options.comment, 'Lowpass is more fun');
- done();
});
});
describe('hint', function() {
- it('works', function(done) {
+ it('works', function() {
const query2 = new Query({});
query2.hint({ indexAttributeA: 1, indexAttributeB: -1 });
assert.deepEqual(query2.options.hint, { indexAttributeA: 1, indexAttributeB: -1 });
@@ -1380,129 +1287,71 @@ describe('Query', function() {
const query3 = new Query({});
query3.hint('indexAttributeA_1');
assert.deepEqual(query3.options.hint, 'indexAttributeA_1');
-
- done();
- });
- });
-
- describe('snapshot', function() {
- it('works', function(done) {
- const query = new Query({});
- query.snapshot(true);
- assert.equal(query.options.snapshot, true);
- done();
});
});
describe('batchSize', function() {
- it('works', function(done) {
+ it('works', function() {
const query = new Query({});
query.batchSize(10);
assert.equal(query.options.batchSize, 10);
- done();
});
});
describe('read', function() {
- const P = mongoose.mongo.ReadPreference;
-
describe('without tags', function() {
- it('works', function(done) {
+ it('works', function() {
const query = new Query({});
query.read('primary');
- assert.ok(query.options.readPreference instanceof P);
- assert.ok(query.options.readPreference.isValid());
- assert.equal(query.options.readPreference.mode, 'primary');
-
- query.read('p');
- assert.ok(query.options.readPreference instanceof P);
- assert.ok(query.options.readPreference.isValid());
assert.equal(query.options.readPreference.mode, 'primary');
query.read('primaryPreferred');
- assert.ok(query.options.readPreference instanceof P);
- assert.ok(query.options.readPreference.isValid());
- assert.equal(query.options.readPreference.mode, 'primaryPreferred');
-
- query.read('pp');
- assert.ok(query.options.readPreference instanceof P);
- assert.ok(query.options.readPreference.isValid());
assert.equal(query.options.readPreference.mode, 'primaryPreferred');
query.read('secondary');
- assert.ok(query.options.readPreference instanceof P);
- assert.ok(query.options.readPreference.isValid());
- assert.equal(query.options.readPreference.mode, 'secondary');
-
- query.read('s');
- assert.ok(query.options.readPreference instanceof P);
- assert.ok(query.options.readPreference.isValid());
assert.equal(query.options.readPreference.mode, 'secondary');
query.read('secondaryPreferred');
- assert.ok(query.options.readPreference instanceof P);
- assert.ok(query.options.readPreference.isValid());
- assert.equal(query.options.readPreference.mode, 'secondaryPreferred');
-
- query.read('sp');
- assert.ok(query.options.readPreference instanceof P);
- assert.ok(query.options.readPreference.isValid());
assert.equal(query.options.readPreference.mode, 'secondaryPreferred');
query.read('nearest');
- assert.ok(query.options.readPreference instanceof P);
- assert.ok(query.options.readPreference.isValid());
- assert.equal(query.options.readPreference.mode, 'nearest');
-
- query.read('n');
- assert.ok(query.options.readPreference instanceof P);
- assert.ok(query.options.readPreference.isValid());
assert.equal(query.options.readPreference.mode, 'nearest');
-
- done();
});
});
describe('with tags', function() {
- it('works', function(done) {
+ it('works', function() {
const query = new Query({});
const tags = [{ dc: 'sf', s: 1 }, { dc: 'jp', s: 2 }];
- query.read('pp', tags);
- assert.ok(query.options.readPreference instanceof P);
- assert.ok(query.options.readPreference.isValid());
+ query.read('primaryPreferred', tags);
assert.equal(query.options.readPreference.mode, 'primaryPreferred');
assert.ok(Array.isArray(query.options.readPreference.tags));
assert.equal(query.options.readPreference.tags[0].dc, 'sf');
assert.equal(query.options.readPreference.tags[0].s, 1);
assert.equal(query.options.readPreference.tags[1].dc, 'jp');
assert.equal(query.options.readPreference.tags[1].s, 2);
- done();
});
});
describe('inherits its models schema read option', function() {
let schema, M, called;
before(function() {
- schema = new Schema({}, { read: 'p' });
+ schema = new Schema({}, { read: 'primary' });
M = mongoose.model('schemaOptionReadPrefWithQuery', schema);
});
- it('if not set in query', function(done) {
+ it('if not set in query', function() {
const options = M.where()._optionsForExec(M);
- assert.ok(options.readPreference instanceof P);
- assert.equal(options.readPreference.mode, 'primary');
- done();
+ assert.equal(options.readPreference, 'primary');
});
- it('if set in query', function(done) {
- const options = M.where().read('s')._optionsForExec(M);
- assert.ok(options.readPreference instanceof P);
+ it('if set in query', function() {
+ const options = M.where().read('secondary')._optionsForExec(M);
assert.equal(options.readPreference.mode, 'secondary');
- done();
});
- it('and sends it though the driver', function(done) {
+ it('and sends it though the driver', async function() {
const options = { read: 'secondary', writeConcern: { w: 'majority' } };
const schema = new Schema({ name: String }, options);
const M = db.model('Test', schema);
@@ -1516,27 +1365,22 @@ describe('Query', function() {
const ret = getopts.call(this, model);
assert.ok(ret.readPreference);
- assert.equal(ret.readPreference.mode, 'secondary');
+ assert.equal(ret.readPreference, 'secondary');
assert.deepEqual({ w: 'majority' }, ret.writeConcern);
called = true;
return ret;
};
- q.exec(function(err) {
- if (err) {
- return done(err);
- }
- assert.ok(called);
- done();
- });
+ await q.exec();
+ assert.ok(called);
});
});
});
});
describe('setOptions', function() {
- it('works', function(done) {
+ it('works', function() {
const q = new Query();
q.setOptions({ thing: 'cat' });
q.setOptions({ populate: ['fans'] });
@@ -1546,7 +1390,7 @@ describe('Query', function() {
q.setOptions({ sort: '-blah' });
q.setOptions({ sort: { woot: -1 } });
q.setOptions({ hint: { index1: 1, index2: -1 } });
- q.setOptions({ read: ['s', [{ dc: 'eu' }]] });
+ q.setOptions({ read: ['secondary', [{ dc: 'eu' }]] });
assert.equal(q.options.thing, 'cat');
assert.deepEqual(q._mongooseOptions.populate.fans, { path: 'fans', _docs: {}, _childDocs: [] });
@@ -1560,27 +1404,13 @@ describe('Query', function() {
assert.equal(q.options.hint.index2, -1);
assert.equal(q.options.readPreference.mode, 'secondary');
assert.equal(q.options.readPreference.tags[0].dc, 'eu');
-
- const Product = db.model('Product', productSchema);
- Product.create(
- { numbers: [3, 4, 5] },
- { strings: 'hi there'.split(' '), w: 'majority' }, function(err, doc1, doc2) {
- assert.ifError(err);
- Product.find().setOptions({ limit: 1, sort: { _id: -1 }, read: 'n' }).exec(function(err, docs) {
- assert.ifError(err);
- assert.equal(docs.length, 1);
- assert.equal(docs[0].id, doc2.id);
- done();
- });
- });
});
- it('populate as array in options (gh-4446)', function(done) {
+ it('populate as array in options (gh-4446)', function() {
const q = new Query();
q.setOptions({ populate: [{ path: 'path1' }, { path: 'path2' }] });
assert.deepEqual(Object.keys(q._mongooseOptions.populate),
['path1', 'path2']);
- done();
});
});
@@ -1607,7 +1437,7 @@ describe('Query', function() {
}
});
- it('collation support (gh-4839)', function(done) {
+ it('collation support (gh-4839)', function() {
const schema = new Schema({
name: String
});
@@ -1615,7 +1445,7 @@ describe('Query', function() {
const MyModel = db.model('Test', schema);
const collation = { locale: 'en_US', strength: 1 };
- MyModel.create([{ name: 'a' }, { name: 'A' }]).
+ return MyModel.create([{ name: 'a' }, { name: 'A' }]).
then(function() {
return MyModel.find({ name: 'a' }).collation(collation);
}).
@@ -1636,13 +1466,10 @@ describe('Query', function() {
}).
then(function(docs) {
assert.equal(docs.length, 1);
- done();
- }).
- catch(done);
+ });
});
it('set on schema (gh-5295)', async function() {
-
await db.db.collection('tests').drop().catch(err => {
if (err.message === 'ns not found') {
return;
@@ -1665,30 +1492,19 @@ describe('Query', function() {
});
describe('gh-1950', function() {
- it.skip('ignores sort when passed to count', function(done) {
- const Product = db.model('Product', productSchema);
- Product.find().sort({ _id: 1 }).count({}).exec(function(error) {
- assert.ifError(error);
- done();
- });
- });
-
it('ignores sort when passed to countDocuments', function() {
const Product = db.model('Product', productSchema);
return Product.create({}).
then(() => Product.find().sort({ _id: 1 }).countDocuments({}).exec());
});
- it.skip('ignores count when passed to sort', function(done) {
+ it.skip('ignores count when passed to sort', function() {
const Product = db.model('Product', productSchema);
- Product.find().count({}).sort({ _id: 1 }).exec(function(error) {
- assert.ifError(error);
- done();
- });
+ return Product.find().count({}).sort({ _id: 1 }).exec();
});
});
- it('excludes _id when select false and inclusive mode (gh-3010)', function(done) {
+ it('excludes _id when select false and inclusive mode (gh-3010)', async function() {
const User = db.model('User', {
_id: {
select: false,
@@ -1698,19 +1514,14 @@ describe('Query', function() {
username: String
});
- User.create({ username: 'Val' }, function(error, user) {
- assert.ifError(error);
- User.find({ _id: user._id }).select('username').exec(function(error, users) {
- assert.ifError(error);
- assert.equal(users.length, 1);
- assert.ok(!users[0]._id);
- assert.equal(users[0].username, 'Val');
- done();
- });
- });
+ const user = await User.create({ username: 'Val' });
+ const users = await User.find({ _id: user._id }).select('username').exec();
+ assert.equal(users.length, 1);
+ assert.ok(!users[0]._id);
+ assert.equal(users[0].username, 'Val');
});
- it('doesnt reverse key order for update docs (gh-3215)', function(done) {
+ it('doesnt reverse key order for update docs (gh-3215)', function() {
const Test = db.model('Test', {
arr: [{ date: Date, value: Number }]
});
@@ -1726,52 +1537,24 @@ describe('Query', function() {
assert.deepEqual(Object.keys(q.getUpdate().$push.arr.$sort),
['value', 'date']);
- done();
});
- it('timestamps with $each (gh-4805)', function(done) {
+ it('timestamps with $each (gh-4805)', async function() {
const nestedSchema = new Schema({ value: Number }, { timestamps: true });
const Test = db.model('Test', new Schema({
arr: [nestedSchema]
}, { timestamps: true }));
- Test.updateOne({}, {
+ await Test.updateOne({}, {
$push: {
arr: {
$each: [{ value: 1 }]
}
}
- }).exec(function(error) {
- assert.ifError(error);
- done();
- });
- });
-
- it.skip('allows sort with count (gh-3914)', function(done) {
- const Post = db.model('BlogPost', {
- title: String
- });
-
- Post.count({}).sort({ title: 1 }).exec(function(error, count) {
- assert.ifError(error);
- assert.strictEqual(count, 0);
- done();
- });
- });
-
- it.skip('allows sort with select (gh-3914)', function(done) {
- const Post = db.model('BlogPost', {
- title: String
- });
-
- Post.count({}).select({ _id: 0 }).exec(function(error, count) {
- assert.ifError(error);
- assert.strictEqual(count, 0);
- done();
});
});
- it('handles nested $ (gh-3265)', function(done) {
+ it('handles nested $ (gh-3265)', function() {
const Post = db.model('BlogPost', {
title: String,
answers: [{
@@ -1790,10 +1573,9 @@ describe('Query', function() {
assert.deepEqual(q.getUpdate().$set['answers.$'].stats,
{ votes: 1, count: 3 });
- done();
});
- it('$geoWithin with single nested schemas (gh-4044)', function(done) {
+ it('$geoWithin with single nested schemas (gh-4044)', async function() {
const locationSchema = new Schema({
type: { type: String },
coordinates: []
@@ -1817,13 +1599,10 @@ describe('Query', function() {
}
}
};
- Model.find(query, function(error) {
- assert.ifError(error);
- done();
- });
+ await Model.find(query);
});
- it('setDefaultsOnInsert with empty update (gh-3825)', function(done) {
+ it('setDefaultsOnInsert with empty update (gh-3825)', async function() {
const schema = new mongoose.Schema({
test: { type: Number, default: 8472 },
name: String
@@ -1832,19 +1611,14 @@ describe('Query', function() {
const MyModel = db.model('Test', schema);
const opts = { upsert: true };
- MyModel.updateOne({}, {}, opts, function(error) {
- assert.ifError(error);
- MyModel.findOne({}, function(error, doc) {
- assert.ifError(error);
- assert.ok(doc);
- assert.strictEqual(doc.test, 8472);
- assert.ok(!doc.name);
- done();
- });
- });
+ await MyModel.updateOne({}, {}, opts);
+ const doc = await MyModel.findOne({});
+ assert.ok(doc);
+ assert.strictEqual(doc.test, 8472);
+ assert.ok(!doc.name);
});
- it('custom query methods (gh-3714)', function(done) {
+ it('custom query methods (gh-3714)', async function() {
const schema = new mongoose.Schema({
name: String
});
@@ -1854,33 +1628,25 @@ describe('Query', function() {
};
const MyModel = db.model('Test', schema);
-
- MyModel.create({ name: 'Val' }, function(error) {
- assert.ifError(error);
- MyModel.find().byName('Val').exec(function(error, docs) {
- assert.ifError(error);
- assert.equal(docs.length, 1);
- assert.equal(docs[0].name, 'Val');
- done();
- });
- });
+ await MyModel.create({ name: 'Val' });
+ const docs = await MyModel.find().byName('Val').exec();
+ assert.equal(docs.length, 1);
+ assert.equal(docs[0].name, 'Val');
});
- it('string as input (gh-4378)', function(done) {
+ it('string as input (gh-4378)', async function() {
const schema = new mongoose.Schema({
name: String
});
const MyModel = db.model('Test', schema);
- MyModel.findOne('', function(error) {
- assert.ok(error);
- assert.equal(error.name, 'ObjectParameterError');
- done();
- });
+ const error = await MyModel.findOne('').then(() => null, err => err);
+ assert.ok(error);
+ assert.equal(error.name, 'ObjectParameterError');
});
- it('handles geoWithin with $center and mongoose object (gh-4419)', function(done) {
+ it('handles geoWithin with $center and mongoose object (gh-4419)', async function() {
const areaSchema = new Schema({
name: String,
circle: Array
@@ -1905,36 +1671,30 @@ describe('Query', function() {
name: 'Tromso, Norway',
circle: [[18.89, 69.62], 10 / 3963.2]
});
- tromso.save(function(error) {
- assert.ifError(error);
-
- const airport = {
- name: 'Center',
- geometry: {
- type: 'Point',
- coordinates: [18.895, 69.67]
+ await tromso.save();
+
+ const airport = {
+ name: 'Center',
+ geometry: {
+ type: 'Point',
+ coordinates: [18.895, 69.67]
+ }
+ };
+ await Place.create(airport);
+
+ const q = {
+ geometry: {
+ $geoWithin: {
+ $centerSphere: tromso.circle
}
- };
- Place.create(airport, function(error) {
- assert.ifError(error);
- const q = {
- geometry: {
- $geoWithin: {
- $centerSphere: tromso.circle
- }
- }
- };
- Place.find(q).exec(function(error, docs) {
- assert.ifError(error);
- assert.equal(docs.length, 1);
- assert.equal(docs[0].name, 'Center');
- done();
- });
- });
- });
+ }
+ };
+ const docs = await Place.find(q).exec();
+ assert.equal(docs.length, 1);
+ assert.equal(docs[0].name, 'Center');
});
- it('$not with objects (gh-4495)', function(done) {
+ it('$not with objects (gh-4495)', function() {
const schema = new Schema({
createdAt: Date
});
@@ -1952,10 +1712,9 @@ describe('Query', function() {
assert.ok(q._conditions.createdAt.$not.$gte instanceof Date);
assert.ok(q._conditions.createdAt.$not.$lte instanceof Date);
- done();
});
- it('geoIntersects with mongoose doc as coords (gh-4408)', function(done) {
+ it('geoIntersects with mongoose doc as coords (gh-4408)', async function() {
const lineStringSchema = new Schema({
name: String,
geo: {
@@ -1978,41 +1737,32 @@ describe('Query', function() {
coordinates: [[27.528, 25.006], [14.063, 15.591]]
}
};
- LineString.create(ls, ls2, function(error, ls1) {
- assert.ifError(error);
- const query = {
- geo: {
- $geoIntersects: {
- $geometry: {
- type: 'LineString',
- coordinates: ls1.geo.coordinates
- }
+ const [ls1] = await LineString.create([ls, ls2]);
+ const query = {
+ geo: {
+ $geoIntersects: {
+ $geometry: {
+ type: 'LineString',
+ coordinates: ls1.geo.coordinates
}
}
- };
- LineString.find(query, function(error, results) {
- assert.ifError(error);
- assert.equal(results.length, 2);
- done();
- });
- });
+ }
+ };
+ const results = await LineString.find(query);
+ assert.equal(results.length, 2);
});
- it('string with $not (gh-4592)', function(done) {
+ it('string with $not (gh-4592)', async function() {
const TestSchema = new Schema({
test: String
});
const Test = db.model('Test', TestSchema);
- Test.findOne({ test: { $not: /test/ } }, function(error) {
- assert.ifError(error);
- done();
- });
+ await Test.findOne({ test: { $not: /test/ } });
});
it('does not cast undefined to null in mongoose (gh-6236)', async function() {
-
const TestSchema = new Schema({
test: String
});
@@ -2029,13 +1779,13 @@ describe('Query', function() {
assert.equal(res.length, 1);
});
- it('runs query setters with _id field (gh-5351)', function(done) {
+ it('runs query setters with _id field (gh-5351)', function() {
const testSchema = new Schema({
val: { type: String }
});
const Test = db.model('Test', testSchema);
- Test.create({ val: 'A string' }).
+ return Test.create({ val: 'A string' }).
then(function() {
return Test.findOne({});
}).
@@ -2049,9 +1799,7 @@ describe('Query', function() {
then(function(doc) {
assert.ok(doc);
assert.equal(doc.val, 'another string');
- }).
- then(done).
- catch(done);
+ });
});
it('runs setters if query field is an array (gh-6277)', async function() {
@@ -2077,20 +1825,17 @@ describe('Query', function() {
assert.deepEqual(setterCalled, [['test']]);
});
- it('$exists under $not (gh-4933)', function(done) {
+ it('$exists under $not (gh-4933)', async function() {
const TestSchema = new Schema({
test: String
});
const Test = db.model('Test', TestSchema);
- Test.findOne({ test: { $not: { $exists: true } } }, function(error) {
- assert.ifError(error);
- done();
- });
+ await Test.findOne({ test: { $not: { $exists: true } } });
});
- it('geojson underneath array (gh-5467)', function(done) {
+ it('geojson underneath array (gh-5467)', async function() {
const storySchema = new Schema({
name: String,
gallery: [{
@@ -2117,16 +1862,11 @@ describe('Query', function() {
}
}
};
- Story.once('index', function(error) {
- assert.ifError(error);
- Story.updateOne(q, { name: 'test' }, { upsert: true }, function(error) {
- assert.ifError(error);
- done();
- });
- });
+ await Story.init();
+ await Story.updateOne(q, { name: 'test' }, { upsert: true });
});
- it('slice respects schema projections (gh-5450)', function(done) {
+ it('slice respects schema projections (gh-5450)', async function() {
const gameSchema = Schema({
name: String,
developer: {
@@ -2137,16 +1877,11 @@ describe('Query', function() {
});
const Game = db.model('Test', gameSchema);
- Game.create({ name: 'Mass Effect', developer: 'BioWare', arr: [1, 2, 3] }, function(error) {
- assert.ifError(error);
- Game.findOne({ name: 'Mass Effect' }).slice({ arr: 1 }).exec(function(error, doc) {
- assert.ifError(error);
- assert.equal(doc.name, 'Mass Effect');
- assert.deepEqual(doc.toObject().arr, [1]);
- assert.ok(!doc.developer);
- done();
- });
- });
+ await Game.create({ name: 'Mass Effect', developer: 'BioWare', arr: [1, 2, 3] });
+ const doc = await Game.findOne({ name: 'Mass Effect' }).slice({ arr: 1 });
+ assert.equal(doc.name, 'Mass Effect');
+ assert.deepEqual(doc.toObject().arr, [1]);
+ assert.ok(!doc.developer);
});
it('overwrites when passing an object when path already set to primitive (gh-6097)', function() {
@@ -2159,7 +1894,7 @@ describe('Query', function() {
where({ status: { $ne: 'delayed' } });
});
- it('$exists for arrays and embedded docs (gh-4937)', function(done) {
+ it('$exists for arrays and embedded docs (gh-4937)', async function() {
const subSchema = new Schema({
name: String
});
@@ -2171,23 +1906,18 @@ describe('Query', function() {
const Test = db.model('Test', TestSchema);
const q = { test: { $exists: true }, sub: { $exists: false } };
- Test.findOne(q, function(error) {
- assert.ifError(error);
- done();
- });
+ await Test.findOne(q);
});
- it('report error in pre hook (gh-5520)', function(done) {
+ it('report error in pre hook (gh-5520)', async function() {
const TestSchema = new Schema({ name: String });
const ops = [
- 'count',
'find',
'findOne',
- 'findOneAndRemove',
+ 'findOneAndDelete',
'findOneAndUpdate',
'replaceOne',
- 'update',
'updateOne',
'updateMany'
];
@@ -2201,65 +1931,51 @@ describe('Query', function() {
const TestModel = db.model('Test', TestSchema);
- let numOps = ops.length;
-
- ops.forEach(function(op) {
- TestModel.find({}).updateOne({ name: 'test' })[op](function(error) {
- assert.ok(error);
- assert.equal(error.message, op + ' error');
- --numOps || done();
- });
- });
+ for (const op of ops) {
+ const q = TestModel.find({}).updateOne({ name: 'test' });
+ const error = await q[op]().then(() => null, err => err);
+ assert.ok(error);
+ assert.equal(error.message, op + ' error');
+ }
});
- it('cast error with custom error (gh-5520)', function(done) {
+ it('cast error with custom error (gh-5520)', function() {
const TestSchema = new Schema({ name: Number });
const TestModel = db.model('Test', TestSchema);
- TestModel.
+ return TestModel.
find({ name: 'not a number' }).
error(new Error('woops')).
- exec(function(error) {
+ exec().
+ then(() => assert.ok(false), error => {
assert.ok(error);
// CastError check happens **after** `.error()`
assert.equal(error.name, 'CastError');
- done();
});
});
- it('change deleteOne to updateOne for soft deletes using $isDeleted (gh-4428)', function(done) {
+ it('change deleteOne to updateOne for soft deletes using $isDeleted (gh-4428)', async function() {
const schema = new mongoose.Schema({
name: String,
isDeleted: Boolean
});
- schema.pre('remove', function(next) {
- const _this = this;
- this.constructor.updateOne({ isDeleted: true }, function(error) {
- // Force mongoose to consider this doc as deleted.
- _this.$isDeleted(true);
- next(error);
- });
+ schema.pre('deleteOne', { document: true, query: false }, async function() {
+ await this.constructor.updateOne({ isDeleted: true });
+ this.$isDeleted(true);
});
const M = db.model('Test', schema);
- M.create({ name: 'test' }, function(error, doc) {
- assert.ifError(error);
- doc.remove(function(error) {
- assert.ifError(error);
- M.findById(doc._id, function(error, doc) {
- assert.ifError(error);
- assert.ok(doc);
- assert.equal(doc.isDeleted, true);
- done();
- });
- });
- });
+ let doc = await M.create({ name: 'test' });
+ await doc.deleteOne();
+ doc = await M.findById(doc._id);
+ assert.ok(doc);
+ assert.equal(doc.isDeleted, true);
});
- it('child schema with select: false in multiple paths (gh-5603)', function(done) {
+ it('child schema with select: false in multiple paths (gh-5603)', async function() {
const ChildSchema = new mongoose.Schema({
field: {
type: String,
@@ -2276,18 +1992,13 @@ describe('Query', function() {
const ogParent = new Parent();
ogParent.child = { field: 'test' };
ogParent.child2 = { field: 'test' };
- ogParent.save(function(error) {
- assert.ifError(error);
- Parent.findById(ogParent._id).exec(function(error, doc) {
- assert.ifError(error);
- assert.ok(!doc.child.field);
- assert.ok(!doc.child2.field);
- done();
- });
- });
+ await ogParent.save();
+ const doc = await Parent.findById(ogParent._id);
+ assert.ok(!doc.child.field);
+ assert.ok(!doc.child2.field);
});
- it('errors in post init (gh-5592)', function(done) {
+ it('errors in post init (gh-5592)', async function() {
const TestSchema = new Schema();
let count = 0;
@@ -2302,79 +2013,24 @@ describe('Query', function() {
docs.push({});
}
- TestModel.create(docs, function(error) {
- assert.ifError(error);
- TestModel.find({}, function(error) {
- assert.ok(error);
- assert.equal(error.message, 'Failed! 0');
- assert.equal(count, 10);
- done();
- });
- });
+ await TestModel.create(docs);
+ const error = await TestModel.find({}).then(() => null, err => err);
+ assert.equal(error.message, 'Failed! 0');
+ assert.equal(count, 10);
});
- it('with non-object args (gh-1698)', function(done) {
+ it('with non-object args (gh-1698)', async function() {
const schema = new mongoose.Schema({
email: String
});
const M = db.model('Test', schema);
- M.find(42, function(error) {
- assert.ok(error);
- assert.equal(error.name, 'ObjectParameterError');
- done();
- });
- });
-
- describe('throw', function() {
- let listeners;
-
- beforeEach(function() {
- listeners = process.listeners('uncaughtException');
- process.removeAllListeners('uncaughtException');
- });
-
- afterEach(function() {
- process.on('uncaughtException', listeners[0]);
- });
-
- it('throw on sync exceptions in callbacks (gh-6178)', function(done) {
- // Deno doesn't support 'uncaughtException', so there's no way to test this in Deno
- // without starting a separate process.
- // See: https://stackoverflow.com/questions/64871554/deno-how-to-handle-exceptions
- if (typeof Deno !== 'undefined') {
- return this.skip();
- }
-
- const schema = new Schema({});
- const Test = db.model('Test', schema);
-
- process.once('uncaughtException', err => {
- assert.equal(err.message, 'Oops!');
- done();
- });
-
- Test.find({}, function() { throw new Error('Oops!'); });
- });
- });
-
- it.skip('set overwrite after update() (gh-4740)', async function() {
- const schema = new Schema({ name: String, age: Number });
- const User = db.model('User', schema);
-
-
- await User.create({ name: 'Bar', age: 29 });
-
- await User.where({ name: 'Bar' }).
- update({ name: 'Baz' }).
- setOptions({ overwrite: true });
-
- const doc = await User.findOne();
- assert.equal(doc.name, 'Baz');
- assert.ok(!doc.age);
+ const error = await M.find(42).then(() => null, err => err);
+ assert.ok(error);
+ assert.equal(error.name, 'ObjectParameterError');
});
- it('queries with BSON overflow (gh-5812)', function(done) {
+ it('queries with BSON overflow (gh-5812)', async function() {
this.timeout(10000);
const schema = new mongoose.Schema({
@@ -2388,43 +2044,16 @@ describe('Query', function() {
bigData[i] = 'test1234567890';
}
- model.find({ email: { $in: bigData } }).lean().
- then(function() {
- done(new Error('Expected an error'));
- }).
- catch(function(error) {
- assert.ok(error);
- assert.ok(error.message !== 'Expected error');
- done();
- });
+ const error = await model.find({ email: { $in: bigData } }).lean().then(() => null, err => err);
+ assert.ok(error);
});
- it('consistently return query when callback specified (gh-6271)', function(done) {
- const schema = new mongoose.Schema({
- n: Number
- });
+ it('explain() (gh-6625)', async function() {
+ const schema = new mongoose.Schema({ n: Number });
const Model = db.model('Test', schema);
- Model.create({ n: 0 }, (err, doc) => {
- assert.ifError(err);
-
- const updateQuery = Model.findOneAndUpdate({ _id: doc._id }, { $inc: { n: 1 } }, { new: true }, (err, doc) => {
- assert.ifError(err);
- assert.equal(doc.n, 1);
- done();
- });
- assert.ok(updateQuery instanceof Query);
- });
- });
-
- it('explain() (gh-6625)', async function() {
-
- const schema = new mongoose.Schema({ n: Number });
-
- const Model = db.model('Test', schema);
-
- await Model.create({ n: 42 });
+ await Model.create({ n: 42 });
let res = await Model.find().explain('queryPlanner');
assert.ok(res.queryPlanner);
@@ -2537,12 +2166,12 @@ describe('Query', function() {
it('cast embedded discriminators with $elemMatch discriminator key (gh-7449)', async function() {
const ListingLineSchema = new Schema({
sellerId: Number
- }, { strictQuery: false });
+ });
const OrderSchema = new Schema({
lines: [new Schema({
amount: Number
- }, { discriminatorKey: 'kind', strictQuery: false })]
+ }, { discriminatorKey: 'kind' })]
});
OrderSchema.path('lines').discriminator('listing', ListingLineSchema);
@@ -2652,30 +2281,19 @@ describe('Query', function() {
});
});
- it('falsy projection', function(done) {
- MyModel.findOne({ name: 'John' }, { lastName: false }).
- exec(function(error, person) {
- assert.ifError(error);
- assert.equal(person.salary, 25000);
- done();
- });
+ it('falsy projection', async function() {
+ const person = await MyModel.findOne({ name: 'John' }, { lastName: false });
+ assert.equal(person.salary, 25000);
});
- it('slice projection', function(done) {
- MyModel.findOne({ name: 'John' }, { dependents: { $slice: 1 } }).exec(function(error, person) {
- assert.ifError(error);
- assert.equal(person.salary, 25000);
- done();
- });
+ it('slice projection', async function() {
+ const person = await MyModel.findOne({ name: 'John' }, { dependents: { $slice: 1 } });
+ assert.equal(person.salary, 25000);
});
- it('empty projection', function(done) {
- MyModel.findOne({ name: 'John' }, {}).
- exec(function(error, person) {
- assert.ifError(error);
- assert.equal(person.salary, 25000);
- done();
- });
+ it('empty projection', async function() {
+ const person = await MyModel.findOne({ name: 'John' }, {}).exec();
+ assert.equal(person.salary, 25000);
});
});
@@ -2720,18 +2338,20 @@ describe('Query', function() {
});
});
- it('map (gh-7142)', async function() {
+ it('transform (gh-14236) (gh-7142)', async function() {
const Model = db.model('Test', new Schema({ name: String }));
-
+ let called = 0;
await Model.create({ name: 'test' });
const now = new Date();
const res = await Model.findOne().transform(res => {
res.loadedAt = now;
+ ++called;
return res;
});
assert.equal(res.loadedAt, now);
+ assert.strictEqual(called, 1);
});
describe('orFail (gh-6841)', function() {
@@ -2747,7 +2367,6 @@ describe('Query', function() {
});
it('find()', async function() {
-
let threw = false;
try {
await Model.find({ name: 'na' }).orFail(() => new Error('Oops!'));
@@ -2764,16 +2383,13 @@ describe('Query', function() {
});
it('findOne()', async function() {
-
- let threw = false;
try {
await Model.findOne({ name: 'na' }).orFail(() => new Error('Oops!'));
+ assert.ok(false);
} catch (error) {
assert.ok(error);
assert.equal(error.message, 'Oops!');
- threw = true;
}
- assert.ok(threw);
// Shouldn't throw
const res = await Model.findOne({ name: 'Test' }).orFail(new Error('Oops'));
@@ -2781,7 +2397,6 @@ describe('Query', function() {
});
it('deleteMany()', async function() {
-
let threw = false;
try {
await Model.deleteMany({ name: 'na' }).orFail(new Error('Oops!'));
@@ -2798,7 +2413,6 @@ describe('Query', function() {
});
it('deleteOne()', async function() {
-
let threw = false;
try {
await Model.deleteOne({ name: 'na' }).orFail(new Error('Oops!'));
@@ -2814,23 +2428,6 @@ describe('Query', function() {
assert.equal(res.deletedCount, 1);
});
- it('remove()', async function() {
-
- let threw = false;
- try {
- await Model.remove({ name: 'na' }).orFail(new Error('Oops!'));
- } catch (error) {
- assert.ok(error);
- assert.equal(error.message, 'Oops!');
- threw = true;
- }
- assert.ok(threw);
-
- // Shouldn't throw
- const res = await Model.remove({ name: 'Test' }).orFail(new Error('Oops'));
- assert.equal(res.deletedCount, 1);
- });
-
it('replaceOne()', async function() {
let threw = false;
try {
@@ -2847,26 +2444,7 @@ describe('Query', function() {
assert.equal(res.modifiedCount, 1);
});
- it('update()', async function() {
-
- let threw = false;
- try {
- await Model.update({ name: 'na' }, { name: 'foo' }).
- orFail(new Error('Oops!'));
- } catch (error) {
- assert.ok(error);
- assert.equal(error.message, 'Oops!');
- threw = true;
- }
- assert.ok(threw);
-
- // Shouldn't throw
- const res = await Model.update({}, { name: 'Test2' }).orFail(new Error('Oops'));
- assert.equal(res.modifiedCount, 1);
- });
-
it('updateMany()', async function() {
-
let threw = false;
try {
await Model.updateMany({ name: 'na' }, { name: 'foo' }).
@@ -2884,7 +2462,6 @@ describe('Query', function() {
});
it('updateOne()', async function() {
-
let threw = false;
try {
await Model.updateOne({ name: 'na' }, { name: 'foo' }).
@@ -2902,11 +2479,10 @@ describe('Query', function() {
});
it('findOneAndUpdate()', async function() {
-
let threw = false;
try {
- await Model.findOneAndUpdate({ name: 'na' }, { name: 'foo' }).
- orFail(new Error('Oops!'));
+ const q = Model.findOneAndUpdate({ name: 'na' }, { name: 'foo' }).orFail(new Error('Oops!'));
+ await q;
} catch (error) {
assert.ok(error);
assert.equal(error.message, 'Oops!');
@@ -2920,7 +2496,6 @@ describe('Query', function() {
});
it('findOneAndDelete()', async function() {
-
let threw = false;
try {
await Model.findOneAndDelete({ name: 'na' }).
@@ -2938,7 +2513,6 @@ describe('Query', function() {
});
it('executes before post hooks (gh-7280)', async function() {
-
const schema = new Schema({ name: String });
const docs = [];
schema.post('findOne', function(doc, next) {
@@ -2976,6 +2550,16 @@ describe('Query', function() {
assert.ok(err.message.indexOf('"Test"') !== -1, err.message);
assert.deepEqual(err.filter, { name: 'na' });
});
+
+ it('does not fire on CastError (gh-13165)', async function() {
+ try {
+ await Model.findOne({ _id: 'bad' }).orFail();
+ assert.ok(false);
+ } catch (error) {
+ assert.ok(error);
+ assert.equal(error.name, 'CastError');
+ }
+ });
});
describe('getPopulatedPaths', function() {
@@ -3278,7 +2862,7 @@ describe('Query', function() {
assert.ok(err.message.indexOf('strictQuery') !== -1, err.message);
});
- it('strictQuery inherits from strict (gh-10763) (gh-4136) (gh-7178)', async function() {
+ it('strictQuery does not inherit from strict (gh-11861)', async function() {
const modelSchema = new Schema({
field: Number,
nested: { path: String }
@@ -3288,8 +2872,17 @@ describe('Query', function() {
// `find()` on a top-level path not in the schema
const err = await Model.find({ notInschema: 1 }).then(() => null, e => e);
- assert.ok(err);
- assert.ok(err.message.indexOf('strictQuery') !== -1, err.message);
+ assert.ifError(err);
+ });
+
+ it('strictQuery is false by default (gh-11861)', async function() {
+ const modelSchema = new Schema({ field: Number }, {});
+ const Model = db.model('Test', modelSchema);
+
+ await Model.create({ field: 1 });
+ const docs = await Model.find({ nonexistingField: 1 });
+
+ assert.equal(docs.length, 0);
});
it('strictQuery = true (gh-6032)', async function() {
@@ -3371,30 +2964,6 @@ describe('Query', function() {
delete db.base.options.maxTimeMS;
});
- it('throws error with updateOne() and overwrite (gh-7475)', function() {
- const Model = db.model('Test', Schema({ name: String }));
-
- return Model.updateOne({}, { name: 'bar' }, { overwrite: true }).then(
- () => { throw new Error('Should have failed'); },
- err => assert.ok(err.message.indexOf('updateOne') !== -1)
- );
- });
-
- it('sets deletedCount on result of remove() (gh-7629)', async function() {
- const schema = new Schema({ name: String });
-
- const Model = db.model('Test', schema);
-
-
- await Model.create({ name: 'foo' });
-
- let res = await Model.remove({});
- assert.equal(res.deletedCount, 1);
-
- res = await Model.remove({});
- assert.strictEqual(res.deletedCount, 0);
- });
-
describe('merge()', function() {
it('copies populate() (gh-1790)', async function() {
const Car = db.model('Car', {
@@ -3523,19 +3092,16 @@ describe('Query', function() {
assert.equal(err.message, 'Query was already executed: Test.findOne({})');
assert.ok(err.originalStack);
- const cb = () => {};
- err = await Test.find(cb).then(() => null, err => err);
- assert.ok(err);
- assert.equal(err.name, 'MongooseError');
- assert.equal(err.message, 'Query was already executed: Test.find({})');
- assert.ok(err.originalStack);
-
err = await q.clone().then(() => null, err => err);
assert.ifError(err);
});
describe('stack traces', function() {
it('includes calling file for filter cast errors (gh-8691)', async function() {
+ if (typeof Deno !== 'undefined') {
+ // Deno doesn't have V8 async stack traces
+ return this.skip();
+ }
const toCheck = ['find', 'findOne', 'deleteOne'];
const Model = db.model('Test', Schema({}));
@@ -3893,6 +3459,47 @@ describe('Query', function() {
assert.deepEqual(q._fields, { email: 1 });
});
+ it('sanitizeProjection option with plus paths (gh-14333) (gh-10243)', async function() {
+ const MySchema = Schema({
+ name: String,
+ email: String,
+ password: { type: String, select: false }
+ });
+ const Test = db.model('Test', MySchema);
+
+ await Test.create({ name: 'test', password: 'secret' });
+
+ let q = Test.findOne().select('+password');
+ let doc = await q;
+ assert.deepEqual(q._fields, {});
+ assert.strictEqual(doc.password, 'secret');
+
+ q = Test.findOne().setOptions({ sanitizeProjection: true }).select('+password');
+ doc = await q;
+ assert.deepEqual(q._fields, { password: 0 });
+ assert.strictEqual(doc.password, undefined);
+
+ q = Test.find().select('+password').setOptions({ sanitizeProjection: true });
+ doc = await q;
+ assert.deepEqual(q._fields, { password: 0 });
+ assert.strictEqual(doc.password, undefined);
+
+ q = Test.find().select('name +password').setOptions({ sanitizeProjection: true });
+ doc = await q;
+ assert.deepEqual(q._fields, { name: 1 });
+ assert.strictEqual(doc.password, undefined);
+
+ q = Test.find().select('+name').setOptions({ sanitizeProjection: true });
+ doc = await q;
+ assert.deepEqual(q._fields, { password: 0 });
+ assert.strictEqual(doc.password, undefined);
+
+ q = Test.find().select('password').setOptions({ sanitizeProjection: true });
+ doc = await q;
+ assert.deepEqual(q._fields, { password: 0 });
+ assert.strictEqual(doc.password, undefined);
+ });
+
it('sanitizeFilter option (gh-3944)', function() {
const MySchema = Schema({ username: String, pwd: String });
const Test = db.model('Test', MySchema);
@@ -3913,6 +3520,21 @@ describe('Query', function() {
assert.ifError(q.error());
assert.deepEqual(q._conditions, { username: 'val', pwd: { $gt: null } });
});
+
+ it('sanitizeFilter disables implicit $in (gh-14657)', function() {
+ const schema = new mongoose.Schema({
+ name: {
+ type: String
+ }
+ });
+ const Test = db.model('Test', schema);
+
+ const q = Test.find({ name: ['foobar'] }).setOptions({ sanitizeFilter: true });
+ q._castConditions();
+ assert.ok(q.error());
+ assert.equal(q.error().name, 'CastError');
+ });
+
it('should not error when $not is used with $size (gh-10716)', async function() {
const barSchema = Schema({
bar: String
@@ -3972,7 +3594,7 @@ describe('Query', function() {
assert.equal(result[0].name, '@foo.com');
});
- it('should return query helper supplied in schema options query property instead of undefined', function(done) {
+ it('should return query helper supplied in schema options query property instead of undefined', async function() {
const Model = db.model('Test', new Schema({
userName: {
type: String,
@@ -3986,19 +3608,10 @@ describe('Query', function() {
}
}));
- Model.create({ userName: 'test' }, function(error) {
- if (error instanceof Error) {
- return done(error);
- }
- Model.find().byUserName('test').exec(function(error, docs) {
- if (error instanceof Error) {
- return done(error);
- }
- assert.equal(docs.length, 1);
- assert.equal(docs[0].userName, 'test');
- done();
- });
- });
+ await Model.create({ userName: 'test' });
+ const docs = await Model.find().byUserName('test').exec();
+ assert.equal(docs.length, 1);
+ assert.equal(docs[0].userName, 'test');
});
it('allows a transform option for lean on a query (gh-10423)', async function() {
@@ -4096,6 +3709,121 @@ describe('Query', function() {
});
+ it('translateAliases option (gh-7511)', async function() {
+ const testSchema = new Schema({
+ name: {
+ type: String,
+ alias: 'n'
+ },
+ age: {
+ type: Number
+ }
+ });
+ const Test = db.model('Test', testSchema);
+ await Test.create({ name: 'foo', age: 99 });
+
+ let res = await Test.findOne({ n: 'foo' }, { n: 1 }, { translateAliases: true });
+ assert.equal(res.name, 'foo');
+ assert.strictEqual(res.age, void 0);
+
+ res = await Test.find({ n: 'foo' }, { n: 1 }, { translateAliases: true });
+ assert.equal(res.length, 1);
+ assert.equal(res[0].name, 'foo');
+ assert.strictEqual(res[0].age, void 0);
+
+ res = await Test.countDocuments({ n: 'foo' }, { translateAliases: true });
+ assert.strictEqual(res, 1);
+
+ res = await Test.distinct('n').setOptions({ translateAliases: true });
+ assert.deepStrictEqual(res, ['foo']);
+
+ res = await Test.findOneAndUpdate(
+ { n: 'foo' },
+ { n: 'bar' },
+ { returnDocument: 'after', translateAliases: true }
+ );
+ assert.strictEqual(res.name, 'bar');
+
+ res = await Test.updateOne(
+ { n: 'bar' },
+ { $set: { age: 44 }, n: 'baz' },
+ { translateAliases: true }
+ );
+ assert.strictEqual(res.modifiedCount, 1);
+
+ res = await Test.updateOne(
+ { name: 'baz' },
+ { $set: { n: 'qux' } },
+ { translateAliases: true }
+ );
+ assert.strictEqual(res.modifiedCount, 1);
+
+ res = await Test.deleteMany({ n: 'qux' }, { translateAliases: true });
+ assert.deepStrictEqual(res.deletedCount, 1);
+ });
+
+ it('translateAliases throws error on conflicting properties (gh-7511)', async function() {
+ const testSchema = new Schema({
+ name: {
+ type: String,
+ alias: 'n'
+ },
+ age: {
+ type: Number
+ }
+ });
+ const Test = db.model('Test', testSchema);
+ await Test.create({ name: 'foo', age: 99 });
+
+ await assert.rejects(async() => {
+ await Test.findOne(
+ { name: 'foo', n: 'bar' },
+ null,
+ { translateAliases: true }
+ );
+ }, /Provided object has both field "n" and its alias "name"/);
+ });
+
+ it('translateAliases applies before casting (gh-14521) (gh-7511)', async function() {
+ const testSchema = new Schema({
+ name: {
+ type: String,
+ alias: 'n'
+ },
+ age: {
+ type: Number
+ }
+ });
+ const Test = db.model('Test', testSchema);
+
+ const doc = await Test.findOneAndUpdate(
+ { n: 14521 },
+ { age: 7511 },
+ { translateAliases: true, upsert: true, returnDocument: 'after' }
+ );
+
+ assert.strictEqual(doc.name, '14521');
+ assert.strictEqual(doc.age, 7511);
+ });
+
+ it('schema level translateAliases option (gh-7511)', async function() {
+ const testSchema = new Schema({
+ name: {
+ type: String,
+ alias: 'n'
+ },
+ age: {
+ type: Number
+ }
+ }, { translateAliases: true });
+ const Test = db.model('Test', testSchema);
+ await Test.deleteMany({});
+ await Test.create({ name: 'foo', age: 99 });
+
+ const res = await Test.findOne({ n: 'foo' });
+ assert.equal(res.name, 'foo');
+ });
+
describe('set()', function() {
it('overwrites top-level keys if setting to undefined (gh-12155)', function() {
const testSchema = new mongoose.Schema({
@@ -4371,8 +4099,86 @@ describe('Query', function() {
}
});
- assert.strictEqual(books.length, 1);
+ assert.strictEqual(books.length, 0);
+ });
+
+ it('merges $and, $or conditions (gh-12944)', function() {
+ const Test = db.model('Test', new Schema({ tags: [String] }));
+
+ let q = Test.find({ $and: [{ tags: 'a' }] });
+ q.find({ $and: [{ tags: 'b' }] });
+ q.find({ $and: [{ tags: 'c' }] });
+
+ assert.deepEqual(q.getFilter(), {
+ $and: [{ tags: 'a' }, { tags: 'b' }, { tags: 'c' }]
+ });
+
+ q = Test.find({ $or: [{ tags: 'a' }] });
+ q.find({ $or: [{ tags: 'b' }] });
+ assert.deepEqual(q.getFilter(), {
+ $or: [{ tags: 'a' }, { tags: 'b' }]
+ });
+ });
+
+ it('shallow clones $and, $or if merging with empty filter (gh-14567) (gh-12944)', function() {
+ const TestModel = db.model(
+ 'Test',
+ Schema({ name: String, age: Number, active: Boolean })
+ );
+
+ let originalQuery = { $and: [{ active: true }] };
+ let q = TestModel.countDocuments(originalQuery)
+ .and([{ age: { $gte: 18 } }]);
+ assert.deepStrictEqual(originalQuery, { $and: [{ active: true }] });
+ assert.deepStrictEqual(q.getFilter(), { $and: [{ active: true }, { age: { $gte: 18 } }] });
+
+ originalQuery = { $or: [{ active: true }] };
+ q = TestModel.countDocuments(originalQuery)
+ .or([{ age: { $gte: 18 } }]);
+ assert.deepStrictEqual(originalQuery, { $or: [{ active: true }] });
+ assert.deepStrictEqual(q.getFilter(), { $or: [{ active: true }, { age: { $gte: 18 } }] });
});
+
+ it('should avoid sending empty session to MongoDB server (gh-13052)', async function() {
+ const m = new mongoose.Mongoose();
+
+ let lastOptions = {};
+ m.set('debug', function(_coll, _method, ...args) {
+ lastOptions = args[args.length - 1];
+ });
+
+ const connDebug = m.createConnection(start.uri);
+
+ const schema = new Schema({ name: String });
+ const Test = connDebug.model('Test', schema);
+
+ await Test.create({ name: 'foo' });
+ assert.ok(!('session' in lastOptions));
+ });
+
+ it('should avoid sending empty projection to MongoDB server (gh-13065)', async function() {
+ const m = new mongoose.Mongoose();
+
+ let lastOptions = {};
+ m.set('debug', function(_coll, _method, ...args) {
+ lastOptions = args[args.length - 2];
+ });
+
+ const connDebug = m.createConnection(start.uri);
+
+ const schema = new Schema({ name: String });
+ const Test = connDebug.model('Test', schema);
+
+ await Test.findOne();
+ assert.ok(!('projection' in lastOptions));
+
+ await Test.find();
+ assert.ok(!('projection' in lastOptions));
+
+ await Test.findOneAndUpdate({}, { name: 'bar' });
+ assert.ok(!('projection' in lastOptions));
+ });
+
it('should provide a clearer error message when sorting with empty string', async function() {
const testSchema = new Schema({
name: { type: String }
@@ -4383,4 +4189,227 @@ describe('Query', function() {
await Error.find().sort('-');
}, { message: 'Invalid field "" passed to sort()' });
});
+
+ it('allows executing a find() with a subdocument with defaults disabled (gh-13512)', async function() {
+ const schema = mongoose.Schema({
+ title: String,
+ bookHolder: mongoose.Schema({
+ isReading: Boolean,
+ tags: [String]
+ })
+ });
+ const Test = db.model('Test', schema);
+
+ const BookHolder = schema.path('bookHolder').caster;
+
+ await Test.collection.insertOne({
+ title: 'test-defaults-disabled',
+ bookHolder: { isReading: true }
+ });
+
+ // Create a new BookHolder subdocument, skip applying defaults
+ // Otherwise, default `[]` for `tags` would cause this query to
+ // return no results.
+ const bookHolder = new BookHolder(
+ { isReading: true },
+ null,
+ null,
+ { defaults: false }
+ );
+ const doc = await Test.findOne({ bookHolder });
+ assert.ok(doc);
+ assert.equal(doc.title, 'test-defaults-disabled');
+ });
+ it('throws a readable error when executing Query instance without a model (gh-13570)', async function() {
+ const schema = new Schema({ name: String });
+ const M = db.model('Test', schema, 'Test');
+ await M.deleteMany({});
+ await M.create({ name: 'gh13570' });
+
+ const Q = new mongoose.Query();
+ await assert.rejects(
+ () => Q.collection('Test').find().lean(),
+ /Query must have an associated model before executing/
+ );
+ });
+
+ it('throws a readable error when executing Query instance without an op (gh-13570)', async function() {
+ const schema = new Schema({ name: String });
+ const M = db.model('Test', schema, 'Test');
+ await M.deleteMany({});
+ await M.create({ name: 'gh13570' });
+
+ const Q = new M.Query();
+ await assert.rejects(
+ () => Q.lean(),
+ /Query must have `op` before executing/
+ );
+ });
+
+ it('allows deselecting discriminator key (gh-13760) (gh-13679)', async function() {
+ const testSchema = new Schema({ name: String, age: Number });
+ const Test = db.model('Test', testSchema);
+ const Test2 = Test.discriminator('Test2', new Schema({ test: String }));
+
+ const { _id } = await Test2.create({ name: 'test1', test: 'test2' });
+ const { name, __t } = await Test.findById(_id).select({ __t: 0 });
+ assert.strictEqual(name, 'test1');
+ assert.strictEqual(__t, undefined);
+
+ let doc = await Test.findById(_id).select({ __t: 0, age: 0 });
+ assert.strictEqual(doc.name, 'test1');
+ assert.strictEqual(doc.__t, undefined);
+
+ doc = await Test.findById(_id).select(['-__t', '-age']);
+ assert.strictEqual(doc.name, 'test1');
+ assert.strictEqual(doc.__t, undefined);
+ });
+
+ it('does not apply sibling path defaults if using nested projection (gh-14115)', async function() {
+ const version = await start.mongodVersion();
+ if (version[0] < 5) {
+ return this.skip();
+ }
+
+ const userSchema = new mongoose.Schema({
+ name: String,
+ account: {
+ amount: Number,
+ owner: { type: String, default: () => 'OWNER' },
+ taxIds: [Number]
+ }
+ });
+ const User = db.model('User', userSchema);
+
+ const { _id } = await User.create({
+ name: 'test',
+ account: {
+ amount: 25,
+ owner: 'test',
+ taxIds: [42]
+ }
+ });
+
+ const doc = await User
+ .findOne({ _id }, { name: 1, account: { amount: 1 } })
+ .orFail();
+ assert.strictEqual(doc.name, 'test');
+ assert.strictEqual(doc.account.amount, 25);
+ assert.strictEqual(doc.account.owner, undefined);
+ assert.strictEqual(doc.account.taxIds, undefined);
+ });
+
+ it('allows overriding sort (gh-14365)', function() {
+ const testSchema = new mongoose.Schema({
+ name: String
+ });
+
+ const Test = db.model('Test', testSchema);
+
+ const q = Test.find().select('name').sort({ name: -1, _id: -1 });
+ assert.deepStrictEqual(q.getOptions().sort, { name: -1, _id: -1 });
+
+ q.sort({ name: 1 }, { override: true });
+ assert.deepStrictEqual(q.getOptions().sort, { name: 1 });
+
+ q.sort(null, { override: true });
+ assert.deepStrictEqual(q.getOptions().sort, {});
+
+ q.sort({ _id: 1 }, { override: true });
+ assert.deepStrictEqual(q.getOptions().sort, { _id: 1 });
+
+ q.sort({}, { override: true });
+ assert.deepStrictEqual(q.getOptions().sort, {});
+ });
+
+ it('avoids mutating user-provided query selectors (gh-14567)', async function() {
+ const TestModel = db.model(
+ 'Test',
+ Schema({ name: String, age: Number, active: Boolean })
+ );
+
+ await TestModel.create({ name: 'John', age: 21 });
+ await TestModel.create({ name: 'Bob', age: 35 });
+
+ const adultQuery = { age: { $gte: 18 } };
+
+ const docs = await TestModel.find(adultQuery).where('age').lte(25);
+ assert.equal(docs.length, 1);
+ assert.equal(docs[0].name, 'John');
+
+ assert.deepStrictEqual(adultQuery, { age: { $gte: 18 } });
+ });
+
+ it('avoids mutating $or, $and elements when casting (gh-14610)', async function() {
+ const personSchema = new mongoose.Schema({
+ name: String,
+ age: Number
+ });
+ const Person = db.model('Person', personSchema);
+
+ const filter = [{ name: 'Me', age: '20' }, { name: 'You', age: '50' }];
+ await Person.find({ $or: filter });
+ assert.deepStrictEqual(filter, [{ name: 'Me', age: '20' }, { name: 'You', age: '50' }]);
+
+ await Person.find({ $and: filter });
+ assert.deepStrictEqual(filter, [{ name: 'Me', age: '20' }, { name: 'You', age: '50' }]);
+ });
+
+ describe('schemaLevelProjections (gh-11474)', function() {
+ it('disables schema-level select: false', async function() {
+ const userSchema = new Schema({
+ email: { type: String, required: true },
+ passwordHash: { type: String, select: false, required: true }
+ });
+ const UserModel = db.model('User', userSchema);
+
+ const { _id } = await UserModel.create({ email: 'test', passwordHash: 'gh-11474' });
+
+ const doc = await UserModel.findById(_id).orFail().schemaLevelProjections(false);
+ assert.strictEqual(doc.email, 'test');
+ assert.strictEqual(doc.passwordHash, 'gh-11474');
+ });
+
+ it('disables schema-level select: true', async function() {
+ const userSchema = new Schema({
+ email: { type: String, required: true, select: true },
+ otherProp: String
+ });
+ const UserModel = db.model('User', userSchema);
+
+ const { _id } = await UserModel.create({ email: 'test', otherProp: 'gh-11474 select true' });
+
+ const doc = await UserModel.findById(_id).select('otherProp').orFail().schemaLevelProjections(false);
+ assert.strictEqual(doc.email, undefined);
+ assert.strictEqual(doc.otherProp, 'gh-11474 select true');
+ });
+
+ it('works via setOptions()', async function() {
+ const userSchema = new Schema({
+ email: { type: String, required: true },
+ passwordHash: { type: String, select: false, required: true }
+ });
+ const UserModel = db.model('User', userSchema);
+
+ const { _id } = await UserModel.create({ email: 'test', passwordHash: 'gh-11474' });
+
+ const doc = await UserModel.findById(_id).orFail().setOptions({ schemaLevelProjections: false });
+ assert.strictEqual(doc.email, 'test');
+ assert.strictEqual(doc.passwordHash, 'gh-11474');
+ });
+
+ it('disabled via truthy value', async function() {
+ const userSchema = new Schema({
+ email: { type: String, required: true },
+ passwordHash: { type: String, select: false, required: true }
+ });
+ const UserModel = db.model('User', userSchema);
+
+ const { _id } = await UserModel.create({ email: 'test', passwordHash: 'gh-11474' });
+
+ const doc = await UserModel.findById(_id).orFail().schemaLevelProjections(true);
+ assert.strictEqual(doc.email, 'test');
+ assert.strictEqual(doc.passwordHash, undefined);
+ });
+ });
});
diff --git a/test/query.toconstructor.test.js b/test/query.toconstructor.test.js
index b93c1afdecf..1b1cadb0d6c 100644
--- a/test/query.toconstructor.test.js
+++ b/test/query.toconstructor.test.js
@@ -40,15 +40,14 @@ describe('Query:', function() {
Product = db.model('Product', Product);
});
- it('creates a query', function(done) {
+ it('creates a query', function() {
const prodQ = Product.find({ title: /test/ }).toConstructor();
assert.ok(prodQ() instanceof Query);
- done();
});
- it('copies all the right values', function(done) {
- const prodQ = Product.update({ title: /test/ }, { title: 'blah' });
+ it('copies all the right values', function() {
+ const prodQ = Product.updateOne({ title: /test/ }, { title: 'blah' });
const prodC = prodQ.toConstructor();
@@ -61,49 +60,35 @@ describe('Query:', function() {
assert.deepEqual(prodQ.model, prodC().model);
assert.deepEqual(prodQ.mongooseCollection, prodC().mongooseCollection);
assert.deepEqual(prodQ._mongooseOptions, prodC()._mongooseOptions);
- done();
});
- it('gets expected results', function(done) {
- Product.create({ title: 'this is a test' }, function(err, p) {
- assert.ifError(err);
- const prodC = Product.find({ title: /test/ }).toConstructor();
-
- prodC().exec(function(err, results) {
- assert.ifError(err);
- assert.equal(results.length, 1);
- assert.equal(p.title, results[0].title);
- done();
- });
- });
+ it('gets expected results', async function() {
+ const p = await Product.create({ title: 'this is a test' });
+ const prodC = Product.find({ title: /test/ }).toConstructor();
+
+ const results = await prodC().exec();
+ assert.equal(results.length, 1);
+ assert.equal(p.title, results[0].title);
});
- it('can be re-used multiple times', function(done) {
- Product.create([{ title: 'moar thing' }, { title: 'second thing' }], function(err, prods) {
- assert.ifError(err);
- assert.equal(prods.length, 2);
- const prod = prods[0];
- const prodC = Product.find({ title: /thing/ }).toConstructor();
-
- prodC().exec(function(err, results) {
- assert.ifError(err);
-
- assert.equal(results.length, 2);
- prodC().find({ _id: prod.id }).exec(function(err, res) {
- assert.ifError(err);
- assert.equal(res.length, 1);
-
- prodC().exec(function(err, res) {
- assert.ifError(err);
- assert.equal(res.length, 2);
- done();
- });
- });
- });
- });
+ it('can be re-used multiple times', async function() {
+ const prods = await Product.create([{ title: 'moar thing' }, { title: 'second thing' }]);
+ assert.equal(prods.length, 2);
+ const prod = prods[0];
+ const prodC = Product.find({ title: /thing/ }).toConstructor();
+
+ const results = await prodC().exec();
+
+ assert.equal(results.length, 2);
+ let res = await prodC().find({ _id: prod.id }).exec();
+ assert.equal(res.length, 1);
+
+ res = await prodC().exec();
+ assert.equal(res.length, 2);
+
});
- it('options get merged properly', function(done) {
+ it('options get merged properly', function() {
let prodC = Product.find({ title: /blah/ }).setOptions({ sort: 'title', lean: true });
prodC = prodC.toConstructor();
@@ -113,10 +98,9 @@ describe('Query:', function() {
sort: { title: 1 },
limit: 3
});
- done();
});
- it('options get cloned (gh-3176)', function(done) {
+ it('options get cloned (gh-3176)', function() {
let prodC = Product.find({ title: /blah/ }).setOptions({ sort: 'title', lean: true });
prodC = prodC.toConstructor();
@@ -129,18 +113,16 @@ describe('Query:', function() {
const nq2 = prodC(null, { limit: 5 });
assert.deepEqual(nq._mongooseOptions, { lean: true, limit: 3 });
assert.deepEqual(nq2._mongooseOptions, { lean: true, limit: 5 });
-
- done();
});
- it('creates subclasses of mquery', function(done) {
+ it('creates subclasses of mquery', function() {
const opts = { w: 'majority', readPreference: 'p' };
const match = { title: 'test', count: { $gt: 101 } };
const select = { name: 1, count: 0 };
const update = { $set: { title: 'thing' } };
const path = 'title';
- const q = Product.update(match, update);
+ const q = Product.updateOne(match, update);
q.select(select);
q.where(path);
q.setOptions(opts);
@@ -156,10 +138,9 @@ describe('Query:', function() {
assert.deepEqual(update, m._update);
assert.equal(path, m._path);
assert.equal('find', m.op);
- done();
});
- it('with findOneAndUpdate (gh-4318)', function(done) {
+ it('with findOneAndUpdate (gh-4318)', async function() {
const Q = Product.where({ title: 'test' }).toConstructor();
const query = { 'tags.test': 1 };
@@ -167,10 +148,7 @@ describe('Query:', function() {
strings: ['123'],
numbers: [1, 2, 3]
};
- Q().findOneAndUpdate(query, update, function(error) {
- assert.ifError(error);
- done();
- });
+ await Q().findOneAndUpdate(query, update);
});
it('gets middleware from model (gh-6455)', async function() {
@@ -184,10 +162,11 @@ describe('Query:', function() {
});
const Test = db.model('Test', schema);
+ await Test.init();
+ await Test.deleteMany({});
const test = new Test({ name: 'Romero' });
const Q = Test.findOne({}).toConstructor();
-
await test.save();
const doc = await Q();
assert.strictEqual(doc.name, 'Romero');
@@ -201,7 +180,7 @@ describe('Query:', function() {
const query = Model.find().sort([['name', 1]]);
const Query = query.toConstructor();
const q = new Query();
- assert.deepEqual(q.options.sort, [['name', 1]]);
+ assert.deepEqual(q.options.sort, { name: 1 });
});
});
});
diff --git a/test/queryhelpers.test.js b/test/queryhelpers.test.js
new file mode 100644
index 00000000000..62c1b2e4afb
--- /dev/null
+++ b/test/queryhelpers.test.js
@@ -0,0 +1,57 @@
+'use strict';
+
+require('./common');
+
+const Schema = require('../lib/schema');
+const assert = require('assert');
+const queryhelpers = require('../lib/queryHelpers');
+
+describe('queryhelpers', function() {
+ describe('applyPaths', function() {
+ it('adds select: true paths unless excluded using minus path (gh-11694)', function() {
+ const schema = new Schema({
+ name: { type: String, select: true },
+ age: Number,
+ other: String
+ });
+
+ let fields = { age: 1 };
+ queryhelpers.applyPaths(fields, schema);
+ assert.deepStrictEqual(fields, { age: 1, name: 1 });
+
+ fields = { age: 1, name: 1 };
+ queryhelpers.applyPaths(fields, schema);
+ assert.deepStrictEqual(fields, { age: 1, name: 1 });
+
+ fields = {};
+ queryhelpers.applyPaths(fields, schema);
+ assert.deepStrictEqual(fields, {});
+
+ fields = { other: 0 };
+ queryhelpers.applyPaths(fields, schema);
+ assert.deepStrictEqual(fields, { other: 0 });
+
+ fields = { age: 1, '-name': 0 };
+ queryhelpers.applyPaths(fields, schema);
+ assert.deepStrictEqual(fields, { age: 1 });
+
+ fields = { age: 1, '-name': 1 };
+ queryhelpers.applyPaths(fields, schema);
+ assert.deepStrictEqual(fields, { age: 1 });
+ });
+
+ it('supports nested minus path (gh-11694)', function() {
+ const schema = new Schema({
+ nested: {
+ name: { type: String, select: true },
+ age: Number,
+ other: String
+ }
+ });
+
+ const fields = { nested: 1 };
+ queryhelpers.applyPaths(fields, schema);
+ assert.deepStrictEqual(fields, { nested: 1 });
+ });
+ });
+});
diff --git a/test/schema.alias.test.js b/test/schema.alias.test.js
index ca25511e2e5..39d919e21e9 100644
--- a/test/schema.alias.test.js
+++ b/test/schema.alias.test.js
@@ -26,7 +26,7 @@ describe('schema alias option', function() {
afterEach(() => require('./util').clearTestData(db));
afterEach(() => require('./util').stopRemainingOps(db));
- it('works with all basic schema types', function(done) {
+ it('works with all basic schema types', async function() {
const schema = new Schema({
string: { type: String, alias: 'StringAlias' },
number: { type: Number, alias: 'NumberAlias' },
@@ -39,7 +39,7 @@ describe('schema alias option', function() {
});
const S = db.model('Test', schema);
- S.create({
+ const s = await S.create({
string: 'hello',
number: 1,
date: new Date(),
@@ -48,23 +48,20 @@ describe('schema alias option', function() {
mixed: [1, [], 'three', { four: 5 }],
objectId: new mongoose.Types.ObjectId(),
array: ['a', 'b', 'c', 'd']
- }, function(err, s) {
- assert.ifError(err);
-
- // Comparing with aliases
- assert.equal(s.string, s.StringAlias);
- assert.equal(s.number, s.NumberAlias);
- assert.equal(s.date, s.DateAlias);
- assert.equal(s.buffer, s.BufferAlias);
- assert.equal(s.boolean, s.BooleanAlias);
- assert.equal(s.mixed, s.MixedAlias);
- assert.equal(s.objectId, s.ObjectIdAlias);
- assert.equal(s.array, s.ArrayAlias);
- done();
});
+
+ // Comparing with aliases
+ assert.equal(s.string, s.StringAlias);
+ assert.equal(s.number, s.NumberAlias);
+ assert.equal(s.date, s.DateAlias);
+ assert.equal(s.buffer, s.BufferAlias);
+ assert.equal(s.boolean, s.BooleanAlias);
+ assert.equal(s.mixed, s.MixedAlias);
+ assert.equal(s.objectId, s.ObjectIdAlias);
+ assert.equal(s.array, s.ArrayAlias);
});
- it('works with nested schema types', function(done) {
+ it('works with nested schema types', async function() {
const schema = new Schema({
nested: {
string: { type: String, alias: 'StringAlias' },
@@ -79,7 +76,7 @@ describe('schema alias option', function() {
});
const S = db.model('Test', schema);
- S.create({
+ const s = await S.create({
nested: {
string: 'hello',
number: 1,
@@ -90,20 +87,17 @@ describe('schema alias option', function() {
objectId: new mongoose.Types.ObjectId(),
array: ['a', 'b', 'c', 'd']
}
- }, function(err, s) {
- assert.ifError(err);
-
- // Comparing with aliases
- assert.equal(s.nested.string, s.StringAlias);
- assert.equal(s.nested.number, s.NumberAlias);
- assert.equal(s.nested.date, s.DateAlias);
- assert.equal(s.nested.buffer, s.BufferAlias);
- assert.equal(s.nested.boolean, s.BooleanAlias);
- assert.equal(s.nested.mixed, s.MixedAlias);
- assert.equal(s.nested.objectId, s.ObjectIdAlias);
- assert.equal(s.nested.array, s.ArrayAlias);
- done();
});
+
+ // Comparing with aliases
+ assert.equal(s.nested.string, s.StringAlias);
+ assert.equal(s.nested.number, s.NumberAlias);
+ assert.equal(s.nested.date, s.DateAlias);
+ assert.equal(s.nested.buffer, s.BufferAlias);
+ assert.equal(s.nested.boolean, s.BooleanAlias);
+ assert.equal(s.nested.mixed, s.MixedAlias);
+ assert.equal(s.nested.objectId, s.ObjectIdAlias);
+ assert.equal(s.nested.array, s.ArrayAlias);
});
it('throws when alias option is invalid', function() {
@@ -189,4 +183,33 @@ describe('schema alias option', function() {
assert.ok(schema.virtuals['name1']);
assert.ok(schema.virtuals['name2']);
});
+ it('supports passing the alias name for an index (gh-13276)', function() {
+ const testSchema = new Schema({
+ fullName: {
+ type: String,
+ alias: 'short'
+ }
+ });
+ testSchema.index({ short: 1 });
+ const indexes = testSchema.indexes();
+ assert.equal(indexes[0][0].fullName, 1);
+ });
+
+ it('should disable the id virtual entirely if there\'s a field with alias `id` gh-13650', async function() {
+
+ const testSchema = new Schema({
+ _id: {
+ type: String,
+ required: true,
+ set: (val) => val.replace(/\s+/g, ' '),
+ alias: 'id'
+ }
+ });
+ const Test = db.model('gh13650', testSchema);
+ const doc = new Test({
+ id: 'H-1'
+ });
+ await doc.save();
+ assert.ok(doc);
+ });
});
diff --git a/test/schema.onthefly.test.js b/test/schema.onthefly.test.js
index 6f07f432d9d..aa492dc6edb 100644
--- a/test/schema.onthefly.test.js
+++ b/test/schema.onthefly.test.js
@@ -28,16 +28,15 @@ describe('schema.onthefly', function() {
afterEach(() => require('./util').clearTestData(db));
afterEach(() => require('./util').stopRemainingOps(db));
- it('setting should cache the schema type and cast values appropriately', function(done) {
+ it('setting should cache the schema type and cast values appropriately', function() {
const Decorated = db.model('Test', DecoratedSchema);
const post = new Decorated();
post.set('adhoc', '9', Number);
assert.equal(post.get('adhoc').valueOf(), 9);
- done();
});
- it('should be local to the particular document', function(done) {
+ it('should be local to the particular document', function() {
const Decorated = db.model('Test', DecoratedSchema);
const postOne = new Decorated();
@@ -47,56 +46,49 @@ describe('schema.onthefly', function() {
const postTwo = new Decorated();
assert.notStrictEqual(postTwo.$__path('title'), undefined);
assert.strictEqual(undefined, postTwo.$__path('adhoc'));
- done();
});
- it('querying a document that had an on the fly schema should work', function(done) {
+ it('querying a document that had an on the fly schema should work', async function() {
const Decorated = db.model('Test', DecoratedSchema);
const post = new Decorated({ title: 'AD HOC' });
// Interpret adhoc as a Number
post.set('adhoc', '9', Number);
assert.equal(post.get('adhoc').valueOf(), 9);
- post.save(function(err) {
- assert.ifError(err);
- assert.strictEqual(null, err);
- Decorated.findById(post.id, function(err, found) {
- assert.strictEqual(null, err);
- assert.equal(found.get('adhoc'), 9);
- // Interpret adhoc as a String instead of a Number now
- assert.equal(found.get('adhoc', String), '9');
- assert.equal(found.get('adhoc'), '9');
-
- // set adhoc as an Object
- found.set('adhoc', '3', Object);
- assert.equal(typeof found.get('adhoc'), 'string');
- found.set('adhoc', 3, Object);
- assert.equal(typeof found.get('adhoc'), 'number');
-
- found.set('adhoc', ['hello'], Object);
- assert.ok(Array.isArray(found.get('adhoc')));
- found.set('adhoc', ['hello'], {});
- assert.ok(Array.isArray(found.get('adhoc')));
-
- found.set('adhoc', 3, String);
- assert.equal(typeof found.get('adhoc'), 'string');
- found.set('adhoc', 3, Object);
- assert.equal(typeof found.get('adhoc'), 'number');
- done();
- });
- });
+ await post.save();
+ const found = await Decorated.findById(post.id);
+ assert.equal(found.get('adhoc'), 9);
+ // Interpret adhoc as a String instead of a Number now
+ assert.equal(found.get('adhoc', String), '9');
+ assert.equal(found.get('adhoc'), '9');
+
+ // set adhoc as an Object
+ found.set('adhoc', '3', Object);
+ assert.equal(typeof found.get('adhoc'), 'string');
+ found.set('adhoc', 3, Object);
+ assert.equal(typeof found.get('adhoc'), 'number');
+
+ found.set('adhoc', ['hello'], Object);
+ assert.ok(Array.isArray(found.get('adhoc')));
+ found.set('adhoc', ['hello'], {});
+ assert.ok(Array.isArray(found.get('adhoc')));
+
+ found.set('adhoc', 3, String);
+ assert.equal(typeof found.get('adhoc'), 'string');
+ found.set('adhoc', 3, Object);
+ assert.equal(typeof found.get('adhoc'), 'number');
+
});
- it('on the fly Embedded Array schemas should cast properly', function(done) {
+ it('on the fly Embedded Array schemas should cast properly', function() {
const Decorated = db.model('Test', DecoratedSchema);
const post = new Decorated();
post.set('moderators', [{ name: 'alex trebek' }], [new Schema({ name: String })]);
assert.equal(post.get('moderators')[0].name, 'alex trebek');
- done();
});
- it('on the fly Embedded Array schemas should get from a fresh queried document properly', function(done) {
+ it('on the fly Embedded Array schemas should get from a fresh queried document properly', async function() {
const Decorated = db.model('Test', DecoratedSchema);
const post = new Decorated();
@@ -104,25 +96,21 @@ describe('schema.onthefly', function() {
post.set('moderators', [{ name: 'alex trebek', ranking: '1' }], [ModeratorSchema]);
assert.equal(post.get('moderators')[0].name, 'alex trebek');
- post.save(function(err) {
- assert.ifError(err);
- Decorated.findById(post.id, function(err, found) {
- assert.ifError(err);
- const rankingPreCast = found.get('moderators')[0].ranking;
- assert.equal(rankingPreCast, 1);
- assert.strictEqual(undefined, rankingPreCast.increment);
- let rankingPostCast = found.get('moderators', [ModeratorSchema])[0].ranking;
- assert.equal(rankingPostCast, 1);
-
- const NewModeratorSchema = new Schema({ name: String, ranking: String });
- rankingPostCast = found.get('moderators', [NewModeratorSchema])[0].ranking;
- assert.equal(rankingPostCast, 1);
- done();
- });
- });
+ await post.save();
+ const found = await Decorated.findById(post.id);
+ const rankingPreCast = found.get('moderators')[0].ranking;
+ assert.equal(rankingPreCast, 1);
+ assert.strictEqual(undefined, rankingPreCast.increment);
+ let rankingPostCast = found.get('moderators', [ModeratorSchema])[0].ranking;
+ assert.equal(rankingPostCast, 1);
+
+ const NewModeratorSchema = new Schema({ name: String, ranking: String });
+ rankingPostCast = found.get('moderators', [NewModeratorSchema])[0].ranking;
+ assert.equal(rankingPostCast, 1);
+
});
- it('casts on get() (gh-2360)', function(done) {
+ it('casts on get() (gh-2360)', function() {
const Decorated = db.model('Test', DecoratedSchema);
const d = new Decorated({ title: '1' });
@@ -133,7 +121,5 @@ describe('schema.onthefly', function() {
d.set('title', 1, Number);
assert.equal(typeof d.get('title'), 'number');
-
- done();
});
});
diff --git a/test/schema.select.test.js b/test/schema.select.test.js
index 0b9115416ef..e34af328327 100644
--- a/test/schema.select.test.js
+++ b/test/schema.select.test.js
@@ -25,49 +25,48 @@ describe('schema select option', function() {
afterEach(() => require('./util').clearTestData(db));
afterEach(() => require('./util').stopRemainingOps(db));
- it('excluding paths through schematype', function(done) {
+ it('excluding paths through schematype', async function() {
+ // data clearing is required for this test, because in deno some other test leaks a "_id: immutable" index
+ await db.dropDatabase();
+
const schema = new Schema({
thin: Boolean,
name: { type: String, select: false },
docs: [new Schema({ bool: Boolean, name: { type: String, select: false } })]
});
- const S = db.model('Test', schema);
- S.create({ thin: true, name: 'the excluded', docs: [{ bool: true, name: 'test' }] }, function(err, s) {
- assert.ifError(err);
- assert.equal(s.name, 'the excluded');
- assert.equal(s.docs[0].name, 'test');
-
- let pending = 5;
- const item = s;
-
- function cb(err, s) {
- pending--;
-
- if (Array.isArray(s)) {
- s = s[0];
- }
- assert.strictEqual(null, err);
- assert.equal(s.isSelected('name'), false);
- assert.equal(s.isSelected('docs.name'), false);
- assert.strictEqual(undefined, s.name);
- // we need to make sure this executes absolutely last.
- if (pending === 1) {
- S.findOneAndRemove({ _id: item._id }, cb);
- }
- if (pending === 0) {
- done();
- }
- }
-
- S.findById(s).select('-thin -docs.bool').exec(cb);
- S.find({ _id: s._id }).select('thin docs.bool').exec(cb);
- S.findById(s, cb);
- S.findOneAndUpdate({ _id: s._id }, { name: 'changed' }, cb);
+ const Test = db.model('Test', schema);
+ const doc = await Test.create({
+ thin: true,
+ name: 'the excluded',
+ docs: [{ bool: true, name: 'test' }]
});
+ assert.equal(doc.name, 'the excluded');
+ assert.equal(doc.docs[0].name, 'test');
+ const findByIdDoc = await Test.findById({ _id: doc._id }).select('-thin -docs.bool');
+ assert.equal(findByIdDoc.isSelected('name'), false);
+ assert.equal(findByIdDoc.isSelected('docs.name'), false);
+ assert.strictEqual(undefined, findByIdDoc.name);
+ const findDoc = await Test.find({ _id: doc._id }).select('thin docs.bool');
+ const singleFindDoc = findDoc[0];
+ assert.equal(singleFindDoc.isSelected('name'), false);
+ assert.equal(singleFindDoc.isSelected('docs.name'), false);
+ assert.strictEqual(undefined, singleFindDoc.name);
+ const findByIdDocAgain = await Test.findById({ _id: doc._id });
+ assert.equal(findByIdDocAgain.isSelected('name'), false);
+ assert.equal(findByIdDocAgain.isSelected('docs.name'), false);
+ assert.strictEqual(undefined, findByIdDocAgain.name);
+ const findUpdateDoc = await Test.findOneAndUpdate({ _id: doc._id }, { name: 'the excluded' });
+ assert.equal(findUpdateDoc.isSelected('name'), false);
+ assert.equal(findUpdateDoc.isSelected('docs.name'), false);
+ assert.strictEqual(undefined, findUpdateDoc.name);
+ const findAndDeleteDoc = await Test.findOneAndDelete({ _id: doc._id });
+ assert.equal(findAndDeleteDoc.isSelected('name'), false);
+ assert.equal(findAndDeleteDoc.isSelected('docs.name'), false);
+ assert.strictEqual(undefined, findAndDeleteDoc.name);
});
- it('including paths through schematype', function(done) {
+ it('including paths through schematype', async function() {
const schema = new Schema({
thin: Boolean,
name: { type: String, select: true },
@@ -75,40 +74,26 @@ describe('schema select option', function() {
});
const S = db.model('Test', schema);
- S.create({ thin: true, name: 'the included', docs: [{ bool: true, name: 'test' }] }, function(err, s) {
- assert.ifError(err);
- assert.equal(s.name, 'the included');
- assert.equal(s.docs[0].name, 'test');
-
- let pending = 4;
-
- function cb(err, s) {
- pending--;
-
- if (Array.isArray(s)) {
- s = s[0];
- }
- assert.strictEqual(null, err);
- assert.strictEqual(true, s.isSelected('name'));
- assert.strictEqual(true, s.isSelected('docs.name'));
- assert.equal(s.name, 'the included');
-
- if (pending === 0) {
- done();
- }
- }
-
- S.findById(s).select('-thin -docs.bool').exec(function(err, res) {
- cb(err, res);
- S.find({ _id: s._id }).select('thin docs.bool').exec(function(err, res) {
- cb(err, res);
- S.findOneAndUpdate({ _id: s._id }, { thin: false }, function(err, s) {
- cb(err, s);
- S.findOneAndRemove({ _id: s._id }, cb);
- });
- });
- });
- });
+ const doc = await S.create({ thin: true, name: 'the included', docs: [{ bool: true, name: 'test' }] });
+ assert.equal(doc.name, 'the included');
+ assert.equal(doc.docs[0].name, 'test');
+ const findByIdDoc = await S.findById({ _id: doc._id }).select('-thin -docs.bool');
+ assert.strictEqual(true, findByIdDoc.isSelected('name'));
+ assert.strictEqual(true, findByIdDoc.isSelected('docs.name'));
+ assert.equal(findByIdDoc.name, 'the included');
+ const findDoc = await S.find({ _id: doc._id });
+ const singleFindDoc = findDoc[0];
+ assert.strictEqual(true, singleFindDoc.isSelected('name'));
+ assert.strictEqual(true, singleFindDoc.isSelected('docs.name'));
+ assert.equal(singleFindDoc.name, 'the included');
+ const findOneAndUpdateDoc = await S.findOneAndUpdate({ _id: doc._id }, { thin: false });
+ assert.strictEqual(true, findOneAndUpdateDoc.isSelected('name'));
+ assert.strictEqual(true, findOneAndUpdateDoc.isSelected('docs.name'));
+ assert.equal(findOneAndUpdateDoc.name, 'the included');
+ const findOneAndDeleteDoc = await S.findOneAndDelete({ _id: doc._id });
+ assert.strictEqual(true, findOneAndDeleteDoc.isSelected('name'));
+ assert.strictEqual(true, findOneAndDeleteDoc.isSelected('docs.name'));
+ assert.equal(findOneAndDeleteDoc.name, 'the included');
});
describe('overriding schematype select options', function() {
@@ -132,144 +117,112 @@ describe('schema select option', function() {
describe('works', function() {
describe('for inclusions', function() {
- let s;
- beforeEach(function(done) {
- S.create({ thin: true, name: 'the included', docs: [{ name: 'test', bool: true }] }, function(err, s_) {
- assert.ifError(err);
- s = s_;
- assert.equal(s.name, 'the included');
- assert.equal(s.docs[0].name, 'test');
- done();
- });
+ let inclusionDoc;
+ beforeEach(async function() {
+ inclusionDoc = await S.create({ thin: true, name: 'the included', docs: [{ name: 'test', bool: true }] });
+ assert.equal(inclusionDoc.name, 'the included');
+ assert.equal(inclusionDoc.docs[0].name, 'test');
});
- it('with find', function(done) {
- S.find({ _id: s._id }).select('thin name docs.bool docs.name').exec(function(err, s) {
- assert.ifError(err);
- assert.ok(s && s.length > 0, 'no document found');
- s = s[0];
- assert.strictEqual(true, s.isSelected('name'));
- assert.strictEqual(true, s.isSelected('thin'));
- assert.strictEqual(true, s.isSelected('docs.name'));
- assert.strictEqual(true, s.isSelected('docs.bool'));
- assert.equal(s.name, 'the included');
- assert.equal(s.docs[0].name, 'test');
- assert.ok(s.thin);
- assert.ok(s.docs[0].bool);
- done();
- });
+ it('with find', async function() {
+ const findDoc = await S.find({ _id: inclusionDoc._id }).select('thin name docs.bool docs.name');
+ assert.ok(findDoc && findDoc.length > 0, 'no document found');
+ const singleFindDoc = findDoc[0];
+ assert.strictEqual(true, singleFindDoc.isSelected('name'));
+ assert.strictEqual(true, singleFindDoc.isSelected('thin'));
+ assert.strictEqual(true, singleFindDoc.isSelected('docs.name'));
+ assert.strictEqual(true, singleFindDoc.isSelected('docs.bool'));
+ assert.strictEqual(singleFindDoc.name, 'the included');
+ assert.strictEqual(singleFindDoc.docs[0].name, 'test');
+ assert.ok(singleFindDoc.thin);
+ assert.ok(singleFindDoc.docs[0].bool);
});
- it('for findById', function(done) {
- S.findById(s).select('-name -docs.name').exec(function(err, s) {
- assert.strictEqual(null, err);
- assert.equal(s.isSelected('name'), false);
- assert.equal(s.isSelected('thin'), true);
- assert.equal(s.isSelected('docs.name'), false);
- assert.equal(s.isSelected('docs.bool'), true);
- assert.ok(s.isSelected('docs'));
- assert.strictEqual(undefined, s.name);
- assert.strictEqual(undefined, s.docs[0].name);
- assert.equal(s.thin, true);
- assert.equal(s.docs[0].bool, true);
- done();
- });
+ it('for findById', async function() {
+ const findByIdDoc = await S.findById({ _id: inclusionDoc._id }).select('-name -docs.name');
+ assert.equal(findByIdDoc.isSelected('name'), false);
+ assert.equal(findByIdDoc.isSelected('thin'), true);
+ assert.equal(findByIdDoc.isSelected('docs.name'), false);
+ assert.equal(findByIdDoc.isSelected('docs.bool'), true);
+ assert.ok(findByIdDoc.isSelected('docs'));
+ assert.strictEqual(undefined, findByIdDoc.name);
+ assert.strictEqual(undefined, findByIdDoc.docs[0].name);
+ assert.equal(findByIdDoc.thin, true);
+ assert.equal(findByIdDoc.docs[0].bool, true);
});
- it('with findOneAndUpdate', function(done) {
- S.findOneAndUpdate({ _id: s._id }, { name: 'changed' }, { new: true }).select('thin name docs.bool docs.name').exec(function(err, s) {
- assert.ifError(err);
- assert.strictEqual(true, s.isSelected('name'));
- assert.strictEqual(true, s.isSelected('thin'));
- assert.strictEqual(true, s.isSelected('docs.name'));
- assert.strictEqual(true, s.isSelected('docs.bool'));
- assert.equal(s.name, 'changed');
- assert.equal(s.docs[0].name, 'test');
- assert.ok(s.thin);
- assert.ok(s.docs[0].bool);
- done();
- });
+ it('with findOneAndUpdate', async function() {
+ const findOneAndUpdateDoc = await S.findOneAndUpdate({ _id: inclusionDoc._id }, { name: 'changed' }, { new: true }).select('thin name docs.bool docs.name');
+ assert.strictEqual(true, findOneAndUpdateDoc.isSelected('name'));
+ assert.strictEqual(true, findOneAndUpdateDoc.isSelected('thin'));
+ assert.strictEqual(true, findOneAndUpdateDoc.isSelected('docs.name'));
+ assert.strictEqual(true, findOneAndUpdateDoc.isSelected('docs.bool'));
+ assert.equal(findOneAndUpdateDoc.name, 'changed');
+ assert.equal(findOneAndUpdateDoc.docs[0].name, 'test');
+ assert.ok(findOneAndUpdateDoc.thin);
+ assert.ok(findOneAndUpdateDoc.docs[0].bool);
});
- it('for findByIdAndUpdate', function(done) {
- S.findByIdAndUpdate(s, { thin: false }, { new: true }).select('-name -docs.name').exec(function(err, s) {
- assert.strictEqual(null, err);
- assert.equal(s.isSelected('name'), false);
- assert.equal(s.isSelected('thin'), true);
- assert.equal(s.isSelected('docs.name'), false);
- assert.equal(s.isSelected('docs.bool'), true);
- assert.strictEqual(undefined, s.name);
- assert.strictEqual(undefined, s.docs[0].name);
- assert.equal(s.thin, false);
- assert.equal(s.docs[0].bool, true);
- done();
- });
+ it('for findByIdAndUpdate', async function() {
+ const findByIdAndUpdateDoc = await S.findByIdAndUpdate({ _id: inclusionDoc._id }, { thin: false }, { new: true }).select('-name -docs.name');
+ assert.equal(findByIdAndUpdateDoc.isSelected('name'), false);
+ assert.equal(findByIdAndUpdateDoc.isSelected('thin'), true);
+ assert.equal(findByIdAndUpdateDoc.isSelected('docs.name'), false);
+ assert.equal(findByIdAndUpdateDoc.isSelected('docs.bool'), true);
+ assert.strictEqual(undefined, findByIdAndUpdateDoc.name);
+ assert.strictEqual(undefined, findByIdAndUpdateDoc.docs[0].name);
+ assert.equal(findByIdAndUpdateDoc.thin, false);
+ assert.equal(findByIdAndUpdateDoc.docs[0].bool, true);
});
});
describe('for exclusions', function() {
- let e;
- beforeEach(function(done) {
- E.create({ thin: true, name: 'the excluded', docs: [{ name: 'test', bool: true }] }, function(err, e_) {
- e = e_;
- assert.ifError(err);
- assert.equal(e.name, 'the excluded');
- assert.equal(e.docs[0].name, 'test');
- done();
- });
+ let exclusionDoc;
+ beforeEach(async function() {
+ exclusionDoc = await E.create({ thin: true, name: 'the excluded', docs: [{ name: 'test', bool: true }] });
+ assert.equal(exclusionDoc.name, 'the excluded');
+ assert.equal(exclusionDoc.docs[0].name, 'test');
});
- it('with find', function(done) {
- E.find({ _id: e._id }).select('thin name docs.name docs.bool').exec(function(err, e) {
- e = e[0];
- assert.strictEqual(null, err);
- assert.equal(e.isSelected('name'), true);
- assert.equal(e.isSelected('thin'), true);
- assert.equal(e.isSelected('docs.name'), true);
- assert.equal(e.isSelected('docs.bool'), true);
- assert.equal(e.name, 'the excluded');
- assert.equal(e.docs[0].name, 'test');
- assert.ok(e.thin);
- assert.ok(e.docs[0].bool);
- done();
- });
+ it('with find', async function() {
+ const findDoc = await E.find({ _id: exclusionDoc._id }).select('thin name docs.name docs.bool');
+ const singleFindDoc = findDoc[0];
+ assert.equal(singleFindDoc.isSelected('name'), true);
+ assert.equal(singleFindDoc.isSelected('thin'), true);
+ assert.equal(singleFindDoc.isSelected('docs.name'), true);
+ assert.equal(singleFindDoc.isSelected('docs.bool'), true);
+ assert.equal(singleFindDoc.name, 'the excluded');
+ assert.equal(singleFindDoc.docs[0].name, 'test');
+ assert.ok(singleFindDoc.thin);
+ assert.ok(singleFindDoc.docs[0].bool);
});
- it('with findById', function(done) {
- E.findById(e).select('-name -docs.name').exec(function(err, e) {
- assert.strictEqual(null, err);
- assert.equal(e.isSelected('name'), false);
- assert.equal(e.isSelected('thin'), true);
- assert.equal(e.isSelected('docs.name'), false);
- assert.equal(e.isSelected('docs.bool'), true);
- assert.strictEqual(undefined, e.name);
- assert.strictEqual(undefined, e.docs[0].name);
- assert.strictEqual(true, e.thin);
- assert.strictEqual(true, e.docs[0].bool);
- done();
- });
+ it('with findById', async function() {
+ const findByIdDoc = await E.findById({ _id: exclusionDoc._id }).select('-name -docs.name');
+ assert.equal(findByIdDoc.isSelected('name'), false);
+ assert.equal(findByIdDoc.isSelected('thin'), true);
+ assert.equal(findByIdDoc.isSelected('docs.name'), false);
+ assert.equal(findByIdDoc.isSelected('docs.bool'), true);
+ assert.strictEqual(undefined, findByIdDoc.name);
+ assert.strictEqual(undefined, findByIdDoc.docs[0].name);
+ assert.strictEqual(true, findByIdDoc.thin);
+ assert.strictEqual(true, findByIdDoc.docs[0].bool);
});
- it('with findOneAndUpdate', function(done) {
- E.findOneAndUpdate({ _id: e._id }, { name: 'changed' }, { new: true }).select('thin name docs.name docs.bool').exec(function(err, e) {
- assert.strictEqual(null, err);
- assert.equal(e.isSelected('name'), true);
- assert.equal(e.isSelected('thin'), true);
- assert.equal(e.isSelected('docs.name'), true);
- assert.equal(e.isSelected('docs.bool'), true);
- assert.equal(e.name, 'changed');
- assert.equal(e.docs[0].name, 'test');
- assert.ok(e.thin);
- assert.ok(e.docs[0].bool);
- done();
- });
+ it('with findOneAndUpdate', async function() {
+ const findOneAndUpdateDoc = await E.findOneAndUpdate({ _id: exclusionDoc._id }, { name: 'changed' }, { new: true }).select('thin name docs.name docs.bool');
+ assert.equal(findOneAndUpdateDoc.isSelected('name'), true);
+ assert.equal(findOneAndUpdateDoc.isSelected('thin'), true);
+ assert.equal(findOneAndUpdateDoc.isSelected('docs.name'), true);
+ assert.equal(findOneAndUpdateDoc.isSelected('docs.bool'), true);
+ assert.equal(findOneAndUpdateDoc.name, 'changed');
+ assert.equal(findOneAndUpdateDoc.docs[0].name, 'test');
+ assert.ok(findOneAndUpdateDoc.thin);
+ assert.ok(findOneAndUpdateDoc.docs[0].bool);
});
- it('with findOneAndRemove', function(done) {
- E.findOneAndRemove({ _id: e._id }).select('-name -docs.name').exec(function(err, e) {
- assert.strictEqual(null, err);
- assert.equal(e.isSelected('name'), false);
- assert.equal(e.isSelected('thin'), true);
- assert.equal(e.isSelected('docs.name'), false);
- assert.equal(e.isSelected('docs.bool'), true);
- assert.strictEqual(undefined, e.name);
- assert.strictEqual(undefined, e.docs[0].name);
- assert.strictEqual(true, e.thin);
- assert.strictEqual(true, e.docs[0].bool);
- done();
- });
+ it('with findOneAndDelete', async function() {
+ const findOneAndDeleteDoc = await E.findOneAndDelete({ _id: exclusionDoc._id }).select('-name -docs.name');
+ assert.equal(findOneAndDeleteDoc.isSelected('name'), false);
+ assert.equal(findOneAndDeleteDoc.isSelected('thin'), true);
+ assert.equal(findOneAndDeleteDoc.isSelected('docs.name'), false);
+ assert.equal(findOneAndDeleteDoc.isSelected('docs.bool'), true);
+ assert.strictEqual(undefined, findOneAndDeleteDoc.name);
+ assert.strictEqual(undefined, findOneAndDeleteDoc.docs[0].name);
+ assert.strictEqual(true, findOneAndDeleteDoc.thin);
+ assert.strictEqual(true, findOneAndDeleteDoc.docs[0].bool);
});
});
});
@@ -296,7 +249,7 @@ describe('schema select option', function() {
done();
});
- it('with nested (gh-7945)', function() {
+ it('with nested (gh-7945)', async function() {
const child = new Schema({
name1: { type: String, select: false },
name2: { type: String, select: true }
@@ -315,11 +268,9 @@ describe('schema select option', function() {
assert.equal(query._fields['parent.docs.name2'], undefined);
assert.equal(query._fields['parent.docs'], 0);
- return M.create({ parent: { docs: [{ name1: 'foo', name2: 'bar' }] } }).
- then(() => query).
- then(doc => {
- assert.ok(!doc.parent.docs);
- });
+ await M.create({ parent: { docs: [{ name1: 'foo', name2: 'bar' }] } });
+ const docQuery = await M.findOne();
+ assert.ok(!docQuery.parent.docs); // the returned document doesn't have a parent property, don't know if thats a problem since before the refactor it also didn't have it.
});
});
@@ -345,7 +296,7 @@ describe('schema select option', function() {
});
describe('forcing inclusion of a deselected schema path', function() {
- it('works', function(done) {
+ it('works', async function() {
const excluded = new Schema({
thin: Boolean,
name: { type: String, select: false },
@@ -353,74 +304,70 @@ describe('schema select option', function() {
});
const M = db.model('Test', excluded);
- M.create({ thin: false, name: '1 meter', docs: [{ name: 'test', bool: false }] }, function(err, d) {
- assert.ifError(err);
-
- M.findById(d)
- .select('+name +docs.name')
- .exec(function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.thin, false);
- assert.equal(doc.name, '1 meter');
- assert.equal(doc.docs[0].bool, false);
- assert.equal(doc.docs[0].name, 'test');
- assert.equal(d.id, doc.id);
-
- M.findById(d)
- .select('+name -thin +docs.name -docs.bool')
- .exec(function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.thin, undefined);
- assert.equal(doc.name, '1 meter');
- assert.equal(doc.docs[0].bool, undefined);
- assert.equal(doc.docs[0].name, 'test');
- assert.equal(d.id, doc.id);
-
- M.findById(d)
- .select('-thin +name -docs.bool +docs.name')
- .exec(function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.thin, undefined);
- assert.equal(doc.name, '1 meter');
- assert.equal(doc.docs[0].bool, undefined);
- assert.equal(doc.docs[0].name, 'test');
- assert.equal(d.id, doc.id);
-
- M.findById(d)
- .select('-thin -docs.bool')
- .exec(function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.thin, undefined);
- assert.equal(doc.name, undefined);
- assert.equal(doc.docs[0].bool, undefined);
- assert.equal(doc.docs[0].name, undefined);
- assert.equal(d.id, doc.id);
- done();
- });
- });
- });
- });
+ const d = await M.create({
+ thin: false,
+ name: '1 meter',
+ docs: [{ name: 'test', bool: false }]
+ });
+ let doc = await M.findById(d)
+ .select('+name +docs.name')
+ .exec();
+ assert.equal(doc.thin, false);
+ assert.equal(doc.name, '1 meter');
+ assert.equal(doc.docs[0].bool, false);
+ assert.equal(doc.docs[0].name, 'test');
+ assert.equal(d.id, doc.id);
+
+ doc = await M.findById(d)
+ .select('+name -thin +docs.name -docs.bool')
+ .exec();
+ assert.equal(doc.thin, undefined);
+ assert.equal(doc.name, '1 meter');
+ assert.equal(doc.docs[0].bool, undefined);
+ assert.equal(doc.docs[0].name, 'test');
+ assert.equal(d.id, doc.id);
+
+ doc = await M.findById(d)
+ .select('-thin +name -docs.bool +docs.name')
+ .exec();
+ assert.equal(doc.thin, undefined);
+ assert.equal(doc.name, '1 meter');
+ assert.equal(doc.docs[0].bool, undefined);
+ assert.equal(doc.docs[0].name, 'test');
+ assert.equal(d.id, doc.id);
+
+ doc = await M.findById(d)
+ .select('-thin -docs.bool')
+ .exec();
+ assert.equal(doc.thin, undefined);
+ assert.equal(doc.name, undefined);
+ assert.equal(doc.docs[0].bool, undefined);
+ assert.equal(doc.docs[0].name, undefined);
+ assert.equal(d.id, doc.id);
+ });
+
+ it('works if only one plus path and only one deselected field', async function() {
+ const MySchema = Schema({
+ name: String,
+ email: String,
+ password: { type: String, select: false }
});
+ const Test = db.model('Test', MySchema);
+ const { _id } = await Test.create({ name: 'test', password: 'secret' });
+
+ const doc = await Test.findById(_id).select('+password');
+ assert.strictEqual(doc.password, 'secret');
});
- it('works with query.slice (gh-1370)', function(done) {
+ it('works with query.slice (gh-1370)', async function() {
const M = db.model('Test', new Schema({ many: { type: [String], select: false } }));
- M.create({ many: ['1', '2', '3', '4', '5'] }, function(err) {
- if (err) {
- return done(err);
- }
+ await M.create({ many: ['1', '2', '3', '4', '5'] });
- const query = M.findOne().select('+many').where('many').slice(2);
+ const query = M.findOne().select('+many').where('many').slice(2);
- query.exec(function(err, doc) {
- if (err) {
- return done(err);
- }
- assert.equal(doc.many.length, 2);
- done();
- });
- });
+ const doc = await query.exec();
+ assert.equal(doc.many.length, 2);
});
it('ignores if path does not have select in schema (gh-6785)', async function() {
@@ -459,7 +406,7 @@ describe('schema select option', function() {
});
});
- it('conflicting schematype path selection should not error', function(done) {
+ it('conflicting schematype path selection should not error', async function() {
const schema = new Schema({
thin: Boolean,
name: { type: String, select: true },
@@ -467,55 +414,79 @@ describe('schema select option', function() {
});
const S = db.model('Test', schema);
- S.create({ thin: true, name: 'bing', conflict: 'crosby' }, function(err, s) {
- assert.strictEqual(null, err);
- assert.equal(s.name, 'bing');
- assert.equal(s.conflict, 'crosby');
+ let s = await S.create({ thin: true, name: 'bing', conflict: 'crosby' });
+ assert.equal(s.name, 'bing');
+ assert.equal(s.conflict, 'crosby');
- let pending = 2;
+ s = await S.findById(s).exec();
+ assert.equal(s.name, 'bing');
+ assert.equal(s.conflict, undefined);
- function cb(err, s) {
- if (!--pending) {
- done();
- }
- if (Array.isArray(s)) {
- s = s[0];
- }
- assert.ifError(err);
- assert.equal(s.name, 'bing');
- assert.equal(s.conflict, undefined);
- }
-
- S.findById(s).exec(cb);
- S.find({ _id: s._id }).exec(cb);
- });
+ s = await S.find({ _id: s._id }).exec();
+ assert.equal(s[0].name, 'bing');
+ assert.equal(s[0].conflict, undefined);
});
- it('selecting _id works with excluded schematype path', function(done) {
+ it('selecting _id works with excluded schematype path', function() {
const schema = new Schema({
name: { type: String, select: false }
});
const M = db.model('Test', schema);
- M.find().select('_id').exec(function(err) {
- assert.ifError(err, err && err.stack);
- done();
- });
+ return M.find().select('_id').exec();
});
- it('selecting _id works with excluded schematype path on sub doc', function(done) {
+ it('selecting _id works with excluded schematype path on sub doc', async function() {
const schema = new Schema({
docs: [new Schema({ name: { type: String, select: false } })]
});
const M = db.model('Test', schema);
- M.find().select('_id').exec(function(err) {
- assert.ifError(err, err && err.stack);
- done();
- });
+ await M.find().select('_id').exec();
});
- it('all inclusive/exclusive combos work', function(done) {
+ it('inclusive/exclusive combos should work', async function() {
+ const coll = 'Test';
+
+ const schema = new Schema({
+ name: { type: String },
+ age: Number
+ }, { collection: coll });
+ const M = db.model('Test1', schema);
+
+ const doc = await M.create({ name: 'ssd', age: 0 });
+ let d = await M.findOne().select('-_id name');
+ assert.equal(d.id, undefined);
+ assert.equal(d.name, 'ssd');
+ assert.equal(d.age, undefined);
+ d = await M.findOne().select('-_id -name');
+ assert.equal(d.id, undefined);
+ assert.equal(d.name, undefined);
+ assert.equal(d.age, 0);
+ d = await M.findOne().select('_id name');
+ assert.equal(d.id, doc.id);
+ assert.equal(d.name, 'ssd');
+ assert.equal(d.age, undefined);
+ try {
+ d = await M.findOne().select('age -name');
+ } catch (error) {
+ assert.ok(error);
+ }
+ try {
+ d = await M.findOne().select('-age name');
+ } catch (error) {
+ assert.ok(error);
+ }
+ d = await M.findOne().select('-age -name');
+ assert.equal(d.id, doc.id);
+ assert.equal(d.name, undefined);
+ assert.equal(d.age, undefined);
+ d = await M.findOne().select('age name');
+ assert.equal(d.id, doc.id);
+ assert.equal(d.name, 'ssd');
+ assert.equal(d.age, 0);
+ });
+ it('when select is false in the schema definition, all inclusive/exclusive combos should work', async function() {
const coll = 'Test';
const schema = new Schema({
@@ -528,78 +499,84 @@ describe('schema select option', function() {
name: { type: String, select: false },
age: Number
}, { collection: coll });
- const S = db.model('Test2', schema1);
+ const SelectFalse = db.model('Test2', schema1);
+ const doc = await M.create({ name: 'ssd', age: 0 });
+ let d = await SelectFalse.findOne().select('-_id name');
+ assert.equal(d.id, undefined);
+ assert.equal(d.name, 'ssd');
+ assert.equal(d.age, undefined);
+ d = await SelectFalse.findOne().select('-_id -name');
+ assert.equal(d.id, undefined);
+ assert.equal(d.name, undefined);
+ assert.equal(d.age, 0);
+ d = await SelectFalse.findOne().select('_id name');
+ assert.equal(d.id, doc.id);
+ assert.equal(d.name, 'ssd');
+ assert.equal(d.age, undefined);
+ try {
+ d = await SelectFalse.findOne().select('age -name');
+ } catch (error) {
+ assert.ok(error);
+ }
+ try {
+ d = await SelectFalse.findOne().select('-age name');
+ } catch (error) {
+ assert.ok(error);
+ }
+ d = await SelectFalse.findOne().select('-age -name');
+ assert.equal(d.id, doc.id);
+ assert.equal(d.name, undefined);
+ assert.equal(d.age, undefined);
+ d = await SelectFalse.findOne().select('age name');
+ assert.equal(d.id, doc.id);
+ assert.equal(d.name, 'ssd');
+ assert.equal(d.age, 0);
+
+ });
+ it('when select is set to true in the schema definition, all inclusive/exclusive combos should work', async function() {
+ const coll = 'Test';
+
+ const schema = new Schema({
+ name: { type: String },
+ age: Number
+ }, { collection: coll });
+ const M = db.model('Test1', schema);
const schema2 = new Schema({
name: { type: String, select: true },
age: Number
}, { collection: coll });
- const T = db.model('Test3', schema2);
-
- function useId(M, id, cb) {
- M.findOne().select('-_id name').exec(function(err, d) {
- // mongo special case for exclude _id + include path
- assert.ifError(err);
- assert.equal(d.id, undefined);
- assert.equal(d.name, 'ssd');
- assert.equal(d.age, undefined);
- M.findOne().select('-_id -name').exec(function(err, d) {
- assert.ifError(err);
- assert.equal(d.id, undefined);
- assert.equal(d.name, undefined);
- assert.equal(d.age, 0);
- M.findOne().select('_id name').exec(function(err, d) {
- assert.ifError(err);
- assert.equal(d.id, id);
- assert.equal(d.name, 'ssd');
- assert.equal(d.age, undefined);
- cb();
- });
- });
- });
- }
-
- function nonId(M, id, cb) {
- M.findOne().select('age -name').exec(function(err, d) {
- assert.ok(err);
- assert.ok(!d);
- M.findOne().select('-age name').exec(function(err, d) {
- assert.ok(err);
- assert.ok(!d);
- M.findOne().select('-age -name').exec(function(err, d) {
- assert.ifError(err);
- assert.equal(d.id, id);
- assert.equal(d.name, undefined);
- assert.equal(d.age, undefined);
- M.findOne().select('age name').exec(function(err, d) {
- assert.ifError(err);
- assert.equal(d.id, id);
- assert.equal(d.name, 'ssd');
- assert.equal(d.age, 0);
- cb();
- });
- });
- });
- });
+ const SelectTrue = db.model('Test3', schema2);
+
+ const doc = await M.create({ name: 'ssd', age: 0 });
+ let d = await SelectTrue.findOne().select('-_id name');
+ assert.equal(d.id, undefined);
+ assert.equal(d.name, 'ssd');
+ assert.equal(d.age, undefined);
+ d = await SelectTrue.findOne().select('-_id -name');
+ assert.equal(d.id, undefined);
+ assert.equal(d.name, undefined);
+ assert.equal(d.age, 0);
+ d = await SelectTrue.findOne().select('_id name');
+ assert.equal(d.id, doc.id);
+ assert.equal(d.name, 'ssd');
+ assert.equal(d.age, undefined);
+ d = await SelectTrue.findOne().select('age -name');
+ assert.equal(d.age, 0);
+ assert.equal(d.name, undefined);
+ try {
+ d = await SelectTrue.findOne().select('-age name');
+ } catch (error) {
+ assert.ok(error);
}
-
- M.create({ name: 'ssd', age: 0 }, function(err, d) {
- assert.ifError(err);
- const id = d.id;
- useId(M, id, function() {
- nonId(M, id, function() {
- useId(S, id, function() {
- nonId(S, id, function() {
- useId(T, id, function() {
- nonId(T, id, function() {
- done();
- });
- });
- });
- });
- });
- });
- });
+ d = await SelectTrue.findOne().select('-age -name');
+ assert.equal(d.id, doc.id);
+ assert.equal(d.name, undefined);
+ assert.equal(d.age, undefined);
+ d = await SelectTrue.findOne().select('age name');
+ assert.equal(d.id, doc.id);
+ assert.equal(d.name, 'ssd');
+ assert.equal(d.age, 0);
});
it('does not set defaults for nested objects (gh-4707)', function(done) {
@@ -631,7 +608,7 @@ describe('schema select option', function() {
catch(done);
});
- it('does not create nested objects if not included (gh-4669)', function(done) {
+ it('does not create nested objects if not included (gh-4669)', async function() {
const schema = new Schema({
field1: String,
field2: String,
@@ -653,18 +630,13 @@ describe('schema select option', function() {
field4: { f1: 'e' },
field5: 'f'
};
- M.create(obj, function(error, doc) {
- assert.ifError(error);
- M.findOne({ _id: doc._id }, 'field1 field2', function(error, doc) {
- assert.ifError(error);
- assert.ok(!doc.toObject({ minimize: false }).field3);
- assert.ok(!doc.toObject({ minimize: false }).field4);
- done();
- });
- });
+ let doc = await M.create(obj);
+ doc = await M.findOne({ _id: doc._id }, 'field1 field2');
+ assert.ok(!doc.toObject({ minimize: false }).field3);
+ assert.ok(!doc.toObject({ minimize: false }).field4);
});
- it('initializes nested defaults with selected objects (gh-2629)', function(done) {
+ it('initializes nested defaults with selected objects (gh-2629)', async function() {
const NestedSchema = new mongoose.Schema({
nested: {
name: { type: String, default: 'val' }
@@ -673,15 +645,23 @@ describe('schema select option', function() {
const Model = db.model('Test', NestedSchema);
- const doc = new Model();
+ let doc = new Model();
doc.nested.name = undefined;
- doc.save(function(error) {
- assert.ifError(error);
- Model.findOne({}, { nested: 1 }, function(error, doc) {
- assert.ifError(error);
- assert.equal(doc.nested.name, 'val');
- done();
- });
+ await doc.save();
+ doc = await Model.findOne({}, { nested: 1 });
+ assert.equal(doc.nested.name, 'val');
+ });
+
+ it('should allow deselecting a field on a query even if the definition has select set to true (gh-11694)', async function() {
+ const testSchema = new mongoose.Schema({
+ name: String,
+ age: { type: String, select: true }
});
+
+ const Test = db.model('Test', testSchema);
+ const doc = await Test.create({ name: 'Test', age: '42' });
+ const result = await Test.findOne({ _id: doc._id }).select('name -age');
+ assert.equal(result.name, 'Test');
+ assert.equal(result.age, undefined);
});
});
diff --git a/test/schema.test.js b/test/schema.test.js
index 52c5128160d..b8f4daf21a3 100644
--- a/test/schema.test.js
+++ b/test/schema.test.js
@@ -16,10 +16,10 @@ const SchemaTypes = Schema.Types;
const ObjectId = SchemaTypes.ObjectId;
const Mixed = SchemaTypes.Mixed;
const DocumentObjectId = mongoose.Types.ObjectId;
-const ReadPref = mongoose.mongo.ReadPreference;
const vm = require('vm');
const idGetter = require('../lib/helpers/schema/idGetter');
const applyPlugins = require('../lib/helpers/schema/applyPlugins');
+const utils = require('../lib/utils');
/**
* Test Document constructor.
@@ -73,10 +73,11 @@ describe('schema', function() {
},
b: { $type: String }
}, { typeKey: '$type' });
+ db.deleteModel(/Test/);
NestedModel = db.model('Test', NestedSchema);
});
- it('don\'t disappear', function(done) {
+ it('don\'t disappear', async function() {
const n = new NestedModel({
a: {
b: {
@@ -86,39 +87,33 @@ describe('schema', function() {
}, b: 'foobar'
});
- n.save(function(err) {
- assert.ifError(err);
- NestedModel.findOne({ _id: n._id }, function(err, nm) {
- assert.ifError(err);
+ await n.save();
+ const nm = await NestedModel.findOne({ _id: n._id });
+
+ // make sure no field has disappeared
+ assert.ok(nm.a);
+ assert.ok(nm.a.b);
+ assert.ok(nm.a.b.c);
+ assert.ok(nm.a.b.d);
+ assert.equal(nm.a.b.c, n.a.b.c);
+ assert.equal(nm.a.b.d, n.a.b.d);
- // make sure no field has disappeared
- assert.ok(nm.a);
- assert.ok(nm.a.b);
- assert.ok(nm.a.b.c);
- assert.ok(nm.a.b.d);
- assert.equal(nm.a.b.c, n.a.b.c);
- assert.equal(nm.a.b.d, n.a.b.d);
- done();
- });
- });
});
});
- it('can be created without the "new" keyword', function(done) {
+ it('can be created without the "new" keyword', function() {
const schema = new Schema({ name: String });
assert.ok(schema instanceof Schema);
- done();
});
- it('does expose a property for duck-typing instanceof', function(done) {
+ it('does expose a property for duck-typing instanceof', function() {
const schema = new Schema({ name: String });
assert.ok(schema.instanceOfSchema);
- done();
});
- it('supports different schematypes', function(done) {
+ it('supports different schematypes', function() {
const Checkin = new Schema({
date: Date,
location: {
@@ -199,10 +194,10 @@ describe('schema', function() {
assert.ok(Ferret1.path('obj') instanceof SchemaTypes.Mixed);
assert.ok(Ferret1.path('buf') instanceof SchemaTypes.Buffer);
assert.ok(Ferret1.path('Buf') instanceof SchemaTypes.Buffer);
- done();
+
});
- it('supports dot notation for path accessors', function(done) {
+ it('supports dot notation for path accessors', function() {
const Racoon = new Schema({
name: { type: String, enum: ['Edwald', 'Tobi'] },
age: Number
@@ -226,10 +221,10 @@ describe('schema', function() {
assert.ok(Person.path('location.state') instanceof SchemaTypes.String);
assert.strictEqual(Person.path('location.unexistent'), undefined);
- done();
+
});
- it('allows paths nested > 2 levels', function(done) {
+ it('allows paths nested > 2 levels', function() {
const Nested = new Schema({
first: {
second: {
@@ -238,10 +233,10 @@ describe('schema', function() {
}
});
assert.ok(Nested.path('first.second.third') instanceof SchemaTypes.String);
- done();
+
});
- it('default definition', function(done) {
+ it('default definition', function() {
const Test = new Schema({
simple: { $type: String, default: 'a' },
array: { $type: Array, default: [1, 2, 3, 4, 5] },
@@ -271,10 +266,10 @@ describe('schema', function() {
assert.ok(Test.path('arrayFn').getDefault(new TestDocument()).isMongooseArray);
assert.ok(Test.path('arrayX').getDefault(new TestDocument()).isMongooseArray);
assert.equal(Test.path('arrayX').getDefault(new TestDocument())[0], 9);
- done();
+
});
- it('Mixed defaults can be empty arrays', function(done) {
+ it('Mixed defaults can be empty arrays', function() {
const Test = new Schema({
mixed1: { type: Mixed, default: [] },
mixed2: { type: Mixed, default: Array }
@@ -284,11 +279,11 @@ describe('schema', function() {
assert.equal(Test.path('mixed1').getDefault().length, 0);
assert.ok(Test.path('mixed2').getDefault() instanceof Array);
assert.equal(Test.path('mixed2').getDefault().length, 0);
- done();
+
});
describe('casting', function() {
- it('number', function(done) {
+ it('number', function() {
const Tobi = new Schema({
age: Number
});
@@ -299,11 +294,11 @@ describe('schema', function() {
assert.equal(typeof Tobi.path('age').cast(0), 'number');
assert.equal((+Tobi.path('age').cast(0)), 0);
- done();
+
});
describe('string', function() {
- it('works', function(done) {
+ it('works', function() {
const Tobi = new Schema({
nickname: String
});
@@ -322,11 +317,11 @@ describe('schema', function() {
// test any object that implements toString
assert.equal(typeof Tobi.path('nickname').cast(new Test()), 'string');
assert.equal(Tobi.path('nickname').cast(new Test()), 'woot');
- done();
+
});
});
- it('date', function(done) {
+ it('date', function() {
const Loki = new Schema({
birth_date: { type: Date }
});
@@ -336,10 +331,10 @@ describe('schema', function() {
assert.ok(Loki.path('birth_date').cast(new Date()) instanceof Date);
assert.ok(Loki.path('birth_date').cast('') === null);
assert.ok(Loki.path('birth_date').cast(null) === null);
- done();
+
});
- it('objectid', function(done) {
+ it('objectid', function() {
const Loki = new Schema({
owner: { type: ObjectId }
});
@@ -354,10 +349,10 @@ describe('schema', function() {
assert.ok(Loki.path('owner').cast(doc) instanceof DocumentObjectId);
assert.equal(Loki.path('owner').cast(doc).toString(), id);
- done();
+
});
- it('array', function(done) {
+ it('array', function() {
const Loki = new Schema({
oids: [ObjectId],
dates: [Date],
@@ -420,10 +415,10 @@ describe('schema', function() {
assert.ok(Loki.path('buffers.$') instanceof SchemaTypes.Buffer);
assert.ok(Loki.path('mixed.$') instanceof SchemaTypes.Mixed);
- done();
+
});
- it('array of arrays', function(done) {
+ it('array of arrays', function() {
const test = new Schema({
nums: [[Number]],
strings: [{ type: [String] }]
@@ -450,10 +445,10 @@ describe('schema', function() {
assert.equal(strs.length, 1);
assert.deepEqual(strs[0].toObject(), ['test']);
- done();
+
});
- it('boolean', function(done) {
+ it('boolean', function() {
const Animal = new Schema({
isFerret: { type: Boolean, required: true }
});
@@ -469,11 +464,11 @@ describe('schema', function() {
assert.equal(Animal.path('isFerret').cast(1), true);
assert.equal(Animal.path('isFerret').cast('1'), true);
assert.equal(Animal.path('isFerret').cast('true'), true);
- done();
+
});
});
- it('methods declaration', function(done) {
+ it('methods declaration', function() {
const a = new Schema();
a.method('test', function() {
});
@@ -484,10 +479,10 @@ describe('schema', function() {
}
});
assert.equal(Object.keys(a.methods).length, 3);
- done();
+
});
- it('static declaration', function(done) {
+ it('static declaration', function() {
const a = new Schema();
a.static('test', function() {
});
@@ -501,11 +496,11 @@ describe('schema', function() {
});
assert.equal(Object.keys(a.statics).length, 4);
- done();
+
});
describe('setters', function() {
- it('work', function(done) {
+ it('work', function() {
function lowercase(v) {
return v.toLowerCase();
}
@@ -523,10 +518,10 @@ describe('schema', function() {
assert.equal(Tobi.path('name').applySetters('WOOT'), 'wootwoot');
assert.equal(Tobi.path('name').setters.length, 2);
- done();
+
});
- it('order', function(done) {
+ it('order', function() {
function extract(v) {
return (v && v._id)
? v._id
@@ -544,10 +539,10 @@ describe('schema', function() {
assert.equal(Tobi.path('name').applySetters(sid, { a: 'b' }).toString(), sid);
assert.equal(Tobi.path('name').applySetters(_id, { a: 'b' }).toString(), sid);
assert.equal(Tobi.path('name').applySetters(id, { a: 'b' }).toString(), sid);
- done();
+
});
- it('scope', function(done) {
+ it('scope', function() {
function lowercase(v, cur, self) {
assert.equal(this.a, 'b');
assert.equal(self.path, 'name');
@@ -559,10 +554,10 @@ describe('schema', function() {
});
assert.equal(Tobi.path('name').applySetters('WHAT', { a: 'b' }), 'what');
- done();
+
});
- it('casting', function(done) {
+ it('casting', function() {
function last(v) {
assert.equal(typeof v, 'number');
assert.equal(v, 0);
@@ -579,11 +574,11 @@ describe('schema', function() {
Tobi.path('name').set(first);
assert.equal(Tobi.path('name').applySetters('woot'), 'last');
- done();
+
});
describe('array', function() {
- it('object setters will be applied for each object in array', function(done) {
+ it('object setters will be applied for each object in array', function() {
const Tobi = new Schema({
names: [{ type: String, lowercase: true, trim: true }]
});
@@ -591,50 +586,50 @@ describe('schema', function() {
assert.equal(typeof Tobi.path('names').applySetters([' whaT', 'WoOt '])[1], 'string');
assert.equal(Tobi.path('names').applySetters([' whaT', 'WoOt '])[0], 'what');
assert.equal(Tobi.path('names').applySetters([' whaT', 'WoOt '])[1], 'woot');
- done();
+
});
});
describe('string', function() {
- it('lowercase', function(done) {
+ it('lowercase', function() {
const Tobi = new Schema({
name: { type: String, lowercase: true }
});
assert.equal(Tobi.path('name').applySetters('WHAT'), 'what');
assert.equal(Tobi.path('name').applySetters(1977), '1977');
- done();
+
});
- it('uppercase', function(done) {
+ it('uppercase', function() {
const Tobi = new Schema({
name: { type: String, uppercase: true }
});
assert.equal(Tobi.path('name').applySetters('what'), 'WHAT');
assert.equal(Tobi.path('name').applySetters(1977), '1977');
- done();
+
});
- it('trim', function(done) {
+ it('trim', function() {
const Tobi = new Schema({
name: { type: String, uppercase: true, trim: true }
});
assert.equal(Tobi.path('name').applySetters(' what '), 'WHAT');
assert.equal(Tobi.path('name').applySetters(1977), '1977');
- done();
+
});
});
- it('applying when none have been defined', function(done) {
+ it('applying when none have been defined', function() {
const Tobi = new Schema({
name: String
});
assert.equal(Tobi.path('name').applySetters('woot'), 'woot');
- done();
+
});
- it('assignment of non-functions throw', function(done) {
+ it('assignment of non-functions throw', function() {
const schema = new Schema({ fun: String });
let g;
@@ -646,12 +641,12 @@ describe('schema', function() {
assert.ok(g);
assert.equal(g.message, 'A setter must be a function.');
- done();
+
});
});
describe('getters', function() {
- it('work', function(done) {
+ it('work', function() {
function woot(v) {
return v + ' woot';
}
@@ -662,9 +657,9 @@ describe('schema', function() {
assert.equal(Tobi.path('name').getters.length, 1);
assert.equal(Tobi.path('name').applyGetters('test'), 'test woot');
- done();
+
});
- it('order', function(done) {
+ it('order', function() {
function format(v) {
return v
? '$' + v
@@ -676,9 +671,9 @@ describe('schema', function() {
});
assert.equal(Tobi.path('name').applyGetters(30, { a: 'b' }), '$30');
- done();
+
});
- it('scope', function(done) {
+ it('scope', function() {
function woot(v, self) {
assert.equal(this.a, 'b');
assert.equal(self.path, 'name');
@@ -690,9 +685,9 @@ describe('schema', function() {
});
assert.equal(Tobi.path('name').applyGetters('YEP', { a: 'b' }), 'yep');
- done();
+
});
- it('casting', function(done) {
+ it('casting', function() {
function last(v) {
assert.equal(typeof v, 'number');
assert.equal(v, 0);
@@ -709,17 +704,17 @@ describe('schema', function() {
Tobi.path('name').get(last);
assert.equal(Tobi.path('name').applyGetters('woot'), 'last');
- done();
+
});
- it('applying when none have been defined', function(done) {
+ it('applying when none have been defined', function() {
const Tobi = new Schema({
name: String
});
assert.equal(Tobi.path('name').applyGetters('woot'), 'woot');
- done();
+
});
- it('assignment of non-functions throw', function(done) {
+ it('assignment of non-functions throw', function() {
const schema = new Schema({ fun: String });
let g;
@@ -731,9 +726,9 @@ describe('schema', function() {
assert.ok(g);
assert.equal(g.message, 'A getter must be a function.');
- done();
+
});
- it('auto _id', function(done) {
+ it('auto _id', function() {
let schema = new Schema({
name: String
});
@@ -754,13 +749,13 @@ describe('schema', function() {
schema.set('_id', true);
assert.ok(schema.path('_id') instanceof Schema.ObjectId);
- done();
+
});
});
describe('indexes', function() {
describe('definition', function() {
- it('basic', function(done) {
+ it('basic', function() {
const Tobi = new Schema({
name: { type: String, index: true }
});
@@ -827,9 +822,9 @@ describe('schema', function() {
assert.equal(T.path('name')._index, false);
assert.equal(T.indexes().length, 0);
- done();
+
});
- it('compound', function(done) {
+ it('compound', function() {
const Tobi = new Schema({
name: { type: String, index: true },
last: { type: Number, sparse: true },
@@ -847,7 +842,7 @@ describe('schema', function() {
[{ firstname: 1, nope: 1 }, { unique: true, background: false }]
]);
- done();
+
});
it('compound based on name (gh-6499)', function() {
@@ -863,7 +858,22 @@ describe('schema', function() {
assert.deepEqual(indexes[1][0], { prop2: 1 });
});
- it('with single nested doc (gh-6113)', function(done) {
+ it('using "ascending" and "descending" for order (gh-13725)', function() {
+ const testSchema = new Schema({
+ prop1: { type: String, index: 'ascending' },
+ prop2: { type: Number, index: 'descending' },
+ prop3: { type: String }
+ });
+ testSchema.index({ prop3: 'desc' }, { unique: true });
+
+ const indexes = testSchema.indexes();
+ assert.equal(indexes.length, 3);
+ assert.deepEqual(indexes[0][0], { prop1: 1 });
+ assert.deepEqual(indexes[1][0], { prop2: -1 });
+ assert.deepEqual(indexes[2][0], { prop3: -1 });
+ });
+
+ it('with single nested doc (gh-6113)', function() {
const pointSchema = new Schema({
type: {
type: String,
@@ -880,8 +890,6 @@ describe('schema', function() {
assert.deepEqual(schema.indexes(), [
[{ point: '2dsphere' }, { background: true }]
]);
-
- done();
});
it('with embedded discriminator (gh-6485)', function() {
@@ -954,7 +962,7 @@ describe('schema', function() {
assert.equal(Tobi.options._id, true);
});
- it('setting', function(done) {
+ it('setting', function() {
let Tobi = new Schema({}, { collection: 'users' });
Tobi.set('a', 'b');
@@ -968,7 +976,6 @@ describe('schema', function() {
const tags = [{ x: 1 }];
Tobi.set('read', 'n');
- assert.ok(Tobi.options.read instanceof ReadPref);
assert.equal(Tobi.options.read.mode, 'nearest');
Tobi.set('read', 'n', tags);
@@ -984,133 +991,101 @@ describe('schema', function() {
assert.equal(Tobi.options.read.tags[0].x, 1);
Tobi = new Schema({}, { read: 'p' });
- assert.ok(Tobi.options.read instanceof ReadPref);
- assert.equal(Tobi.options.read.mode, 'primary');
+ assert.equal(Tobi.options.read, 'primary');
Tobi = new Schema({}, { read: ['s', tags] });
- assert.ok(Tobi.options.read instanceof ReadPref);
assert.equal(Tobi.options.read.mode, 'secondary');
assert.ok(Array.isArray(Tobi.options.read.tags));
assert.equal(Tobi.options.read.tags.length, 1);
assert.equal(Tobi.options.read.tags[0].x, 1);
Tobi = new Schema({}, { read: 'primary' });
- assert.ok(Tobi.options.read instanceof ReadPref);
- assert.equal(Tobi.options.read.mode, 'primary');
+ assert.equal(Tobi.options.read, 'primary');
Tobi = new Schema({}, { read: ['secondary', tags] });
- assert.ok(Tobi.options.read instanceof ReadPref);
assert.equal(Tobi.options.read.mode, 'secondary');
assert.ok(Array.isArray(Tobi.options.read.tags));
assert.equal(Tobi.options.read.tags.length, 1);
assert.equal(Tobi.options.read.tags[0].x, 1);
Tobi = new Schema({}, { read: 's' });
- assert.ok(Tobi.options.read instanceof ReadPref);
- assert.equal(Tobi.options.read.mode, 'secondary');
+ assert.equal(Tobi.options.read, 'secondary');
Tobi = new Schema({}, { read: ['s', tags] });
- assert.ok(Tobi.options.read instanceof ReadPref);
assert.equal(Tobi.options.read.mode, 'secondary');
assert.ok(Array.isArray(Tobi.options.read.tags));
assert.equal(Tobi.options.read.tags.length, 1);
assert.equal(Tobi.options.read.tags[0].x, 1);
Tobi = new Schema({}, { read: 'secondary' });
- assert.ok(Tobi.options.read instanceof ReadPref);
- assert.equal(Tobi.options.read.mode, 'secondary');
+ assert.equal(Tobi.options.read, 'secondary');
Tobi = new Schema({}, { read: ['secondary', tags] });
- assert.ok(Tobi.options.read instanceof ReadPref);
assert.equal(Tobi.options.read.mode, 'secondary');
assert.ok(Array.isArray(Tobi.options.read.tags));
assert.equal(Tobi.options.read.tags.length, 1);
assert.equal(Tobi.options.read.tags[0].x, 1);
Tobi = new Schema({}, { read: 'pp' });
- assert.ok(Tobi.options.read instanceof ReadPref);
- assert.ok(Tobi.options.read.isValid());
- assert.equal(Tobi.options.read.mode, 'primaryPreferred');
+ assert.equal(Tobi.options.read, 'primaryPreferred');
Tobi = new Schema({}, { read: ['pp', tags] });
- assert.ok(Tobi.options.read instanceof ReadPref);
- assert.ok(Tobi.options.read.isValid());
assert.equal(Tobi.options.read.mode, 'primaryPreferred');
assert.ok(Array.isArray(Tobi.options.read.tags));
assert.equal(Tobi.options.read.tags.length, 1);
assert.equal(Tobi.options.read.tags[0].x, 1);
Tobi = new Schema({}, { read: 'primaryPreferred' });
- assert.ok(Tobi.options.read instanceof ReadPref);
- assert.ok(Tobi.options.read.isValid());
- assert.equal(Tobi.options.read.mode, 'primaryPreferred');
+ assert.equal(Tobi.options.read, 'primaryPreferred');
Tobi = new Schema({}, { read: ['primaryPreferred', tags] });
- assert.ok(Tobi.options.read instanceof ReadPref);
- assert.ok(Tobi.options.read.isValid());
assert.equal(Tobi.options.read.mode, 'primaryPreferred');
assert.ok(Array.isArray(Tobi.options.read.tags));
assert.equal(Tobi.options.read.tags.length, 1);
assert.equal(Tobi.options.read.tags[0].x, 1);
Tobi = new Schema({}, { read: 'sp' });
- assert.ok(Tobi.options.read instanceof ReadPref);
- assert.ok(Tobi.options.read.isValid());
- assert.equal(Tobi.options.read.mode, 'secondaryPreferred');
+ assert.equal(Tobi.options.read, 'secondaryPreferred');
Tobi = new Schema({}, { read: ['sp', tags] });
- assert.ok(Tobi.options.read instanceof ReadPref);
- assert.ok(Tobi.options.read.isValid());
assert.equal(Tobi.options.read.mode, 'secondaryPreferred');
assert.ok(Array.isArray(Tobi.options.read.tags));
assert.equal(Tobi.options.read.tags.length, 1);
assert.equal(Tobi.options.read.tags[0].x, 1);
Tobi = new Schema({}, { read: 'secondaryPreferred' });
- assert.ok(Tobi.options.read instanceof ReadPref);
- assert.ok(Tobi.options.read.isValid());
- assert.equal(Tobi.options.read.mode, 'secondaryPreferred');
+ assert.equal(Tobi.options.read, 'secondaryPreferred');
Tobi = new Schema({}, { read: ['secondaryPreferred', tags] });
- assert.ok(Tobi.options.read instanceof ReadPref);
- assert.ok(Tobi.options.read.isValid());
assert.equal(Tobi.options.read.mode, 'secondaryPreferred');
assert.ok(Array.isArray(Tobi.options.read.tags));
assert.equal(Tobi.options.read.tags.length, 1);
assert.equal(Tobi.options.read.tags[0].x, 1);
Tobi = new Schema({}, { read: 'n' });
- assert.ok(Tobi.options.read instanceof ReadPref);
- assert.ok(Tobi.options.read.isValid());
- assert.equal(Tobi.options.read.mode, 'nearest');
+ assert.equal(Tobi.options.read, 'nearest');
Tobi = new Schema({}, { read: ['n', tags] });
- assert.ok(Tobi.options.read instanceof ReadPref);
- assert.ok(Tobi.options.read.isValid());
assert.equal(Tobi.options.read.mode, 'nearest');
assert.ok(Array.isArray(Tobi.options.read.tags));
assert.equal(Tobi.options.read.tags.length, 1);
assert.equal(Tobi.options.read.tags[0].x, 1);
Tobi = new Schema({}, { read: 'nearest' });
- assert.ok(Tobi.options.read instanceof ReadPref);
- assert.ok(Tobi.options.read.isValid());
- assert.equal(Tobi.options.read.mode, 'nearest');
+ assert.equal(Tobi.options.read, 'nearest');
Tobi = new Schema({}, { read: ['nearest', tags] });
- assert.ok(Tobi.options.read instanceof ReadPref);
- assert.ok(Tobi.options.read.isValid());
assert.equal(Tobi.options.read.mode, 'nearest');
assert.ok(Array.isArray(Tobi.options.read.tags));
assert.equal(Tobi.options.read.tags.length, 1);
assert.equal(Tobi.options.read.tags[0].x, 1);
- done();
+
});
});
describe('virtuals', function() {
- it('works', function(done) {
+ it('works', function() {
const Contact = new Schema({
firstName: String,
lastName: String
@@ -1128,25 +1103,25 @@ describe('schema', function() {
});
assert.ok(Contact.virtualpath('fullName') instanceof VirtualType);
- done();
+
});
describe('id', function() {
- it('default creation of id can be overridden (gh-298)', function(done) {
+ it('default creation of id can be overridden (gh-298)', function() {
assert.doesNotThrow(function() {
new Schema({ id: String });
});
- done();
+
});
- it('disabling', function(done) {
+ it('disabling', function() {
const schema = new Schema({ name: String });
assert.strictEqual(undefined, schema.virtuals.id);
- done();
+
});
});
describe('getter', function() {
- it('scope', function(done) {
+ it('scope', function() {
const Tobi = new Schema();
Tobi.virtual('name').get(function(v, self) {
@@ -1156,12 +1131,12 @@ describe('schema', function() {
});
assert.equal(Tobi.virtualpath('name').applyGetters('YEP', { a: 'b' }), 'yep');
- done();
+
});
});
describe('setter', function() {
- it('scope', function(done) {
+ it('scope', function() {
const Tobi = new Schema();
Tobi.virtual('name').set(function(v, self) {
@@ -1171,13 +1146,13 @@ describe('schema', function() {
});
assert.equal(Tobi.virtualpath('name').applySetters('YEP', { a: 'b' }), 'yep');
- done();
+
});
});
});
describe('other contexts', function() {
- it('work', function(done) {
+ it('work', function() {
if (typeof Deno !== 'undefined') {
// Deno throws "Not implemented: Script.prototype.runInNewContext"
return this.skip();
@@ -1205,12 +1180,12 @@ describe('schema', function() {
assert.ok(Ferret.path('date') instanceof SchemaTypes.Date);
assert.ok(Ferret.path('num') instanceof SchemaTypes.Number);
assert.ok(Ferret.path('bool') instanceof SchemaTypes.Boolean);
- done();
+
});
});
describe('#add()', function() {
- it('does not pollute existing paths', function(done) {
+ it('does not pollute existing paths', function() {
let o = { name: String };
let s = new Schema(o);
@@ -1235,7 +1210,7 @@ describe('schema', function() {
}, /Cannot set nested path/);
assert.equal(o.name, 'string');
- done();
+
});
it('returns the schema instance', function() {
@@ -1251,7 +1226,7 @@ describe('schema', function() {
assert.strictEqual(ret, schemaB);
});
- it('merging nested objects (gh-662)', function(done) {
+ it('merging nested objects (gh-662)', async function() {
const MergedSchema = new Schema({
a: {
foo: String
@@ -1278,28 +1253,22 @@ describe('schema', function() {
}
});
- merged.save(function(err) {
- assert.ifError(err);
- Merged.findById(merged.id, function(err, found) {
- assert.ifError(err);
- assert.equal(found.a.foo, 'baz');
- assert.equal(found.a.b.bar, 'qux');
- done();
- });
- });
+ await merged.save();
+ const found = await Merged.findById(merged.id);
+ assert.equal(found.a.foo, 'baz');
+ assert.equal(found.a.b.bar, 'qux');
});
- it('prefix (gh-1730)', function(done) {
+ it('prefix (gh-1730)', function() {
const s = new Schema({});
s.add({ n: Number }, 'prefix.');
assert.equal(s.pathType('prefix.n'), 'real');
assert.equal(s.pathType('prefix'), 'nested');
- done();
});
- it('adds another schema (gh-6897)', function(done) {
+ it('adds another schema (gh-6897)', function() {
const s = new Schema({ name: String });
const s2 = new Schema({ age: Number });
@@ -1314,8 +1283,6 @@ describe('schema', function() {
assert.ok(s.paths.age);
assert.strictEqual(s.statics.foo, s2.statics.foo);
assert.ok(s.s.hooks._pres.get('save'));
-
- done();
});
it('overwrites existing paths (gh-10203)', function() {
@@ -1344,7 +1311,7 @@ describe('schema', function() {
});
});
- it('debugging msgs', function(done) {
+ it('debugging msgs', function() {
let err;
try {
new Schema({ name: { first: null } });
@@ -1358,11 +1325,10 @@ describe('schema', function() {
err = e;
}
assert.ok(err.message.indexOf('Invalid value for schema path `age`') !== -1, err.message);
- done();
});
describe('construction', function() {
- it('array of object literal missing a type is interpreted as DocumentArray', function(done) {
+ it('array of object literal missing a type is interpreted as DocumentArray', function() {
const goose = new mongoose.Mongoose();
const s = new Schema({
arr: [
@@ -1374,10 +1340,10 @@ describe('schema', function() {
const m = new M({ arr: [{ something: 'wicked this way comes' }] });
assert.equal(m.arr[0].something, 'wicked this way comes');
assert.ok(m.arr[0]._id);
- done();
+
});
- it('array of object literal with type.type is interpreted as DocumentArray', function(done) {
+ it('array of object literal with type.type is interpreted as DocumentArray', function() {
const goose = new mongoose.Mongoose();
const s = new Schema({
arr: [
@@ -1389,10 +1355,10 @@ describe('schema', function() {
const m = new M({ arr: [{ type: 'works' }] });
assert.equal(m.arr[0].type, 'works');
assert.ok(m.arr[0]._id);
- done();
+
});
- it('does not alter original argument (gh-1364)', function(done) {
+ it('does not alter original argument (gh-1364)', function() {
const schema = {
ids: [{ type: Schema.ObjectId, ref: 'something' }],
a: { type: Array },
@@ -1411,10 +1377,10 @@ describe('schema', function() {
assert.deepEqual({ type: 'Boolean' }, schema.d);
assert.deepEqual([{ a: String, b: [{ type: { type: Buffer }, x: Number }] }], schema.e);
- done();
+
});
- it('properly gets value of plain objects when dealing with refs (gh-1606)', function(done) {
+ it('properly gets value of plain objects when dealing with refs (gh-1606)', async function() {
const el = new Schema({ title: String });
const so = new Schema({
title: String,
@@ -1426,29 +1392,56 @@ describe('schema', function() {
const ele = new Element({ title: 'thing' });
- ele.save(function(err) {
- assert.ifError(err);
- const s = new Some({ obj: ele.toObject() });
- s.save(function(err) {
- assert.ifError(err);
- Some.findOne({ _id: s.id }, function(err, ss) {
- assert.ifError(err);
- assert.equal(ss.obj, ele.id);
- done();
- });
- });
- });
+ await ele.save();
+ const s = new Some({ obj: ele.toObject() });
+ await s.save();
+ const ss = await Some.findOne({ _id: s.id });
+ assert.equal(ss.obj, ele.id);
});
- it('array of of schemas and objects (gh-7218)', function(done) {
+ it('array of of schemas and objects (gh-7218)', function() {
const baseSchema = new Schema({ created: Date }, { id: true });
const s = new Schema([baseSchema, { name: String }], { id: false });
assert.ok(s.path('created'));
assert.ok(s.path('name'));
assert.ok(!s.options.id);
+ });
+
+ it('copies options from array of schemas', function() {
+ const baseSchema = new Schema({ created: Date }, { id: true, toJSON: { virtuals: true } });
+ const s = new Schema([baseSchema, { name: String }]);
+
+ assert.ok(s.path('created'));
+ assert.ok(s.path('name'));
+ assert.ok(s.options.id);
+ assert.deepEqual(s.options.toJSON, { virtuals: true });
+ assert.strictEqual(s.options.toObject, undefined);
- done();
+ s.add(new Schema({}, { toObject: { getters: true } }));
+ assert.ok(s.path('created'));
+ assert.ok(s.path('name'));
+ assert.ok(s.options.id);
+ assert.deepEqual(s.options.toJSON, { virtuals: true });
+ assert.deepEqual(s.options.toObject, { getters: true });
+ });
+
+ it('propagates typeKey down to implicitly created single nested schemas (gh-13154)', function() {
+ const TestSchema = {
+ action: {
+ $type: {
+ type: {
+ $type: String,
+ required: true
+ }
+ },
+ required: true
+ }
+ };
+ const s = new Schema(TestSchema, { typeKey: '$type' });
+ assert.equal(s.path('action').constructor.name, 'SchemaSubdocument');
+ assert.ok(s.path('action').schema.$implicitlyCreated);
+ assert.equal(s.path('action.type').constructor.name, 'SchemaString');
});
});
@@ -1474,7 +1467,7 @@ describe('schema', function() {
assert.ok(lastWarnMessage.includes(`\`${reservedProperty}\` is a reserved schema pathname`), lastWarnMessage);
});
- it(`\`${reservedProperty}\` when used as a schema path doesn't log a warning if \`supressReservedKeysWarning\` is true`, async() => {
+ it(`\`${reservedProperty}\` when used as a schema path doesn't log a warning if \`suppressReservedKeysWarning\` is true`, async() => {
// Arrange
const emitWarningStub = sinon.stub(process, 'emitWarning').returns();
@@ -1482,7 +1475,7 @@ describe('schema', function() {
// Act
new Schema(
{ [reservedProperty]: String },
- { supressReservedKeysWarning: true }
+ { suppressReservedKeysWarning: true }
);
const lastWarnMessage = emitWarningStub.args[0] && emitWarningStub.args[0][0];
@@ -1521,22 +1514,6 @@ describe('schema', function() {
new M({ setMaxListeners: 'works' });
});
});
-
- it('permit _scope to be used (gh-1184)', function(done) {
- const child = new Schema({ _scope: Schema.ObjectId });
- const C = db.model('Test', child);
- const c = new C();
- c.save(function(err) {
- assert.ifError(err);
- try {
- c._scope;
- } catch (e) {
- err = e;
- }
- assert.ifError(err);
- done();
- });
- });
});
describe('pathType()', function() {
@@ -1553,31 +1530,31 @@ describe('schema', function() {
});
describe('when called on an explicit real path', function() {
- it('returns "real"', function(done) {
+ it('returns "real"', function() {
assert.equal(schema.pathType('n'), 'real');
assert.equal(schema.pathType('nest.thing.nests'), 'real');
assert.equal(schema.pathType('docs'), 'real');
assert.equal(schema.pathType('docs.0.x'), 'real');
assert.equal(schema.pathType('docs.0.x.3.y'), 'real');
assert.equal(schema.pathType('mixed'), 'real');
- done();
+
});
});
describe('when called on a virtual', function() {
- it('returns virtual', function(done) {
+ it('returns virtual', function() {
assert.equal(schema.pathType('myVirtual'), 'virtual');
- done();
+
});
});
describe('when called on nested structure', function() {
- it('returns nested', function(done) {
+ it('returns nested', function() {
assert.equal(schema.pathType('nest'), 'nested');
assert.equal(schema.pathType('nest.thing'), 'nested');
- done();
+
});
});
describe('when called on undefined path', function() {
- it('returns adHocOrUndefined', function(done) {
+ it('returns adHocOrUndefined', function() {
assert.equal(schema.pathType('mixed.what'), 'adhocOrUndefined');
assert.equal(schema.pathType('mixed.4'), 'adhocOrUndefined');
assert.equal(schema.pathType('mixed.4.thing'), 'adhocOrUndefined');
@@ -1593,7 +1570,7 @@ describe('schema', function() {
assert.equal(schema.pathType('nest.thing.nests.9'), 'adhocOrUndefined');
assert.equal(schema.pathType('nest.thing.nests.9a'), 'adhocOrUndefined');
assert.equal(schema.pathType('nest.thing.nests.a'), 'adhocOrUndefined');
- done();
+
});
});
@@ -1609,7 +1586,7 @@ describe('schema', function() {
});
});
- it('required() with doc arrays (gh-3199)', function(done) {
+ it('required() with doc arrays (gh-3199)', function() {
const schema = new Schema({
test: [{ x: String }]
});
@@ -1619,10 +1596,10 @@ describe('schema', function() {
const m = new M({ test: [{}] });
assert.equal(m.validateSync().errors['test.0.x'].kind, 'required');
- done();
+
});
- it('custom typeKey in doc arrays (gh-3560)', function(done) {
+ it('custom typeKey in doc arrays (gh-3560)', function() {
const schema = new Schema({
test: [{
name: { $type: String }
@@ -1635,10 +1612,10 @@ describe('schema', function() {
assert.ifError(m.validateSync());
assert.equal(m.test[0].name, 'Val');
- done();
+
});
- it('required for single nested schemas (gh-3562)', function(done) {
+ it('required for single nested schemas (gh-3562)', function() {
const personSchema = new Schema({
name: { type: String, required: true }
});
@@ -1656,10 +1633,10 @@ describe('schema', function() {
band.guitarist = { name: 'Slash' };
assert.ifError(band.validateSync());
- done();
+
});
- it('booleans cause cast error for date (gh-3935)', function(done) {
+ it('booleans cause cast error for date (gh-3935)', function() {
const testSchema = new Schema({
test: Date
});
@@ -1670,10 +1647,10 @@ describe('schema', function() {
assert.ok(test.validateSync());
assert.equal(test.validateSync().errors.test.name, 'CastError');
- done();
+
});
- it('trim: false works with strings (gh-4042)', function(done) {
+ it('trim: false works with strings (gh-4042)', function() {
const testSchema = new Schema({
test: { type: String, trim: false }
});
@@ -1681,10 +1658,10 @@ describe('schema', function() {
const Test = mongoose.model('gh4042', testSchema);
const test = new Test({ test: ' test ' });
assert.equal(test.test, ' test ');
- done();
+
});
- it('arrays with typeKey (gh-4548)', function(done) {
+ it('arrays with typeKey (gh-4548)', function() {
const testSchema = new Schema({
test: [{ $type: String }]
}, { typeKey: '$type' });
@@ -1694,10 +1671,10 @@ describe('schema', function() {
const Test = mongoose.model('gh4548', testSchema);
const test = new Test({ test: [123] });
assert.strictEqual(test.test[0], '123');
- done();
+
});
- it('arrays of mixed arrays (gh-5416)', function(done) {
+ it('arrays of mixed arrays (gh-5416)', function() {
const testSchema = new Schema({
test: [Array]
});
@@ -1706,7 +1683,7 @@ describe('schema', function() {
assert.equal(testSchema.paths.test.casterConstructor,
mongoose.Schema.Types.Array);
- done();
+
});
describe('remove()', function() {
@@ -1729,45 +1706,45 @@ describe('schema', function() {
assert.ok(ret instanceof Schema);
});
- it('removes a single path', function(done) {
+ it('removes a single path', function() {
assert.ok(this.schema.paths.a);
this.schema.remove('a');
assert.strictEqual(this.schema.path('a'), undefined);
assert.strictEqual(this.schema.paths.a, void 0);
- done();
+
});
- it('removes a nested path', function(done) {
+ it('removes a nested path', function() {
this.schema.remove('b.c.d');
assert.strictEqual(this.schema.path('b'), undefined);
assert.strictEqual(this.schema.path('b.c'), undefined);
assert.strictEqual(this.schema.path('b.c.d'), undefined);
- done();
+
});
- it('removes all children of a nested path (gh-2398)', function(done) {
+ it('removes all children of a nested path (gh-2398)', function() {
this.schema.remove('b');
assert.strictEqual(this.schema.nested['b'], undefined);
assert.strictEqual(this.schema.nested['b.c'], undefined);
assert.strictEqual(this.schema.path('b.c.d'), undefined);
- done();
+
});
- it('removes an array of paths', function(done) {
+ it('removes an array of paths', function() {
this.schema.remove(['e', 'f', 'g']);
assert.strictEqual(this.schema.path('e'), undefined);
assert.strictEqual(this.schema.path('f'), undefined);
assert.strictEqual(this.schema.path('g'), undefined);
- done();
+
});
- it('works properly with virtuals (gh-2398)', function(done) {
+ it('works properly with virtuals (gh-2398)', function() {
this.schema.remove('a');
this.schema.virtual('a').get(function() { return 42; });
const Test = mongoose.model('gh2398', this.schema);
const t = new Test();
assert.equal(t.a, 42);
- done();
+
});
it('methods named toString (gh-4551)', function() {
@@ -1779,15 +1756,15 @@ describe('schema', function() {
});
});
- it('handles default value = 0 (gh-4620)', function(done) {
+ it('handles default value = 0 (gh-4620)', function() {
const schema = new Schema({
tags: { type: [Number], default: 0 }
});
assert.deepEqual(schema.path('tags').getDefault().toObject(), [0]);
- done();
+
});
- it('type: childSchema (gh-5521)', function(done) {
+ it('type: childSchema (gh-5521)', function() {
const childSchema = new mongoose.Schema({
name: String
}, { _id: false });
@@ -1800,10 +1777,10 @@ describe('schema', function() {
const doc = new Model({ children: [{ name: 'test' }] });
assert.deepEqual(doc.toObject().children, [{ name: 'test' }]);
- done();
+
});
- it('Decimal128 type (gh-4759)', function(done) {
+ it('Decimal128 type (gh-4759)', function() {
const Decimal128 = mongoose.Schema.Types.Decimal128;
const schema = new Schema({
num: Decimal128,
@@ -1815,11 +1792,11 @@ describe('schema', function() {
const casted = schema.path('num').cast('6.2e+23');
assert.ok(casted instanceof mongoose.Types.Decimal128);
assert.equal(casted.toString(), '6.2E+23');
- done();
+
});
describe('clone()', function() {
- it('copies methods, statics, and query helpers (gh-5752)', function(done) {
+ it('copies methods, statics, and query helpers (gh-5752)', function() {
const schema = new Schema({});
schema.methods.fakeMethod = function() { return 'fakeMethod'; };
@@ -1830,10 +1807,10 @@ describe('schema', function() {
assert.equal(clone.methods.fakeMethod, schema.methods.fakeMethod);
assert.equal(clone.statics.fakeStatic, schema.statics.fakeStatic);
assert.equal(clone.query.fakeQueryHelper, schema.query.fakeQueryHelper);
- done();
+
});
- it('copies validators declared with validate() (gh-5607)', function(done) {
+ it('copies validators declared with validate() (gh-5607)', function() {
const schema = new Schema({
num: Number
});
@@ -1846,10 +1823,10 @@ describe('schema', function() {
assert.equal(clone.path('num').validators.length, 1);
assert.ok(clone.path('num').validators[0].validator(42));
assert.ok(!clone.path('num').validators[0].validator(41));
- done();
+
});
- it('copies virtuals (gh-6133)', function(done) {
+ it('copies virtuals (gh-6133)', function() {
const userSchema = new Schema({
firstName: { type: String, required: true },
lastName: { type: String, required: true }
@@ -1863,10 +1840,10 @@ describe('schema', function() {
const clonedUserSchema = userSchema.clone();
assert.ok(clonedUserSchema.virtuals.fullName);
- done();
+
});
- it('with nested virtuals (gh-6274)', function(done) {
+ it('with nested virtuals (gh-6274)', function() {
const PersonSchema = new Schema({
name: {
first: String,
@@ -1890,10 +1867,10 @@ describe('schema', function() {
const doc = new M({ name: { first: 'Axl', last: 'Rose' } });
assert.equal(doc.name.full, 'Axl Rose');
- done();
+
});
- it('with alternative option syntaxes (gh-6274)', function(done) {
+ it('with alternative option syntaxes (gh-6274)', function() {
const TestSchema = new Schema({}, { _id: false, id: false });
TestSchema.virtual('test').get(() => 42);
@@ -1914,7 +1891,7 @@ describe('schema', function() {
assert.deepEqual(doc.toJSON(), { test: 42 });
assert.deepEqual(doc.toObject(), { test: 42 });
- done();
+
});
it('copies base for using custom types after cloning (gh-7377)', function() {
@@ -2022,9 +1999,24 @@ describe('schema', function() {
const test2 = test.clone();
assert.equal(test2.localTest(), 42);
});
+
+ it('avoids creating duplicate array constructors when cloning doc array underneath subdoc (gh-13626)', function() {
+ const schema = new mongoose.Schema({
+ config: {
+ type: new mongoose.Schema({
+ attributes: [{ value: 'Mixed' }]
+ })
+ }
+ }).clone();
+
+ assert.strictEqual(
+ schema.paths['config'].schema.paths['attributes'].Constructor,
+ schema.singleNestedPaths['config.attributes'].Constructor
+ );
+ });
});
- it('childSchemas prop (gh-5695)', function(done) {
+ it('childSchemas prop (gh-5695)', function() {
const schema1 = new Schema({ name: String });
const schema2 = new Schema({ test: String });
let schema = new Schema({
@@ -2041,7 +2033,7 @@ describe('schema', function() {
assert.strictEqual(schema.childSchemas[0].schema, schema1);
assert.strictEqual(schema.childSchemas[1].schema, schema2);
- done();
+
});
});
@@ -2176,7 +2168,7 @@ describe('schema', function() {
});
it('SchemaStringOptions line up with schema/string (gh-8256)', function() {
- const SchemaStringOptions = require('../lib/options/SchemaStringOptions');
+ const SchemaStringOptions = require('../lib/options/schemaStringOptions');
const keys = Object.keys(SchemaStringOptions.prototype).
filter(key => key !== 'constructor' && key !== 'populate');
const functions = Object.keys(Schema.Types.String.prototype).
@@ -2434,6 +2426,25 @@ describe('schema', function() {
assert.ok(threw);
});
+ it('with function cast error format', function() {
+ const schema = Schema({
+ num: {
+ type: Number,
+ cast: [null, value => `${value} isn't a number`]
+ }
+ });
+
+ let threw = false;
+ try {
+ schema.path('num').cast('horseradish');
+ } catch (err) {
+ threw = true;
+ assert.equal(err.name, 'CastError');
+ assert.equal(err.message, 'horseradish isn\'t a number');
+ }
+ assert.ok(threw);
+ });
+
it('with objectids', function() {
const schema = Schema({
userId: {
@@ -2600,7 +2611,7 @@ describe('schema', function() {
});
const casted = schema.path('ids').cast([[]]);
- assert.equal(casted[0].$path(), 'ids.$');
+ assert.equal(casted[0].$path(), 'ids.0');
});
describe('cast option (gh-8407)', function() {
@@ -2641,7 +2652,7 @@ describe('schema', function() {
myStr: { type: String, cast: v => '' + v }
});
- assert.equal(schema.path('myId').cast('12charstring').toHexString(), '313263686172737472696e67');
+ assert.equal(schema.path('myId').cast('0'.repeat(24)).toHexString(), '0'.repeat(24));
assert.equal(schema.path('myNum').cast(3.14), 4);
assert.equal(schema.path('myDate').cast('2012-06-01').getFullYear(), 2012);
assert.equal(schema.path('myBool').cast('hello'), true);
@@ -2864,6 +2875,43 @@ describe('schema', function() {
assert(batch.message);
});
+ it('supports numbers with Schema.discriminator() (gh-13788)', async() => {
+ const baseClassSchema = new Schema({
+ type: { type: Number, required: true }
+ }, { discriminatorKey: 'type' });
+
+ class BaseClass {
+ whoAmI() {
+ return 'I am base';
+ }
+ }
+ BaseClass.type = 1;
+
+ baseClassSchema.loadClass(BaseClass);
+
+ class NumberTyped extends BaseClass {
+ whoAmI() {
+ return 'I am NumberTyped';
+ }
+ }
+ NumberTyped.type = 2;
+
+ class StringTyped extends BaseClass {
+ whoAmI() {
+ return 'I am StringTyped';
+ }
+ }
+ StringTyped.type = '3';
+
+ baseClassSchema.discriminator(2, new Schema({}).loadClass(NumberTyped));
+ baseClassSchema.discriminator('3', new Schema({}).loadClass(StringTyped));
+ const Test = db.model('Test', { item: baseClassSchema });
+ let doc = await Test.create({ item: { type: 2 } });
+ assert.equal(doc.item.whoAmI(), 'I am NumberTyped');
+ doc = await Test.create({ item: { type: '3' } });
+ assert.equal(doc.item.whoAmI(), 'I am StringTyped');
+ });
+
it('can use on as a schema property (gh-11580)', async() => {
const testSchema = new mongoose.Schema({
on: String
@@ -2967,8 +3015,8 @@ describe('schema', function() {
}
});
- assert.equal(schema.path('testId').instance, 'ObjectID');
- assert.equal(schema.path('testId2').instance, 'ObjectID');
+ assert.equal(schema.path('testId').instance, 'ObjectId');
+ assert.equal(schema.path('testId2').instance, 'ObjectId');
assert.equal(schema.path('num').instance, 'Decimal128');
assert.equal(schema.path('num2').instance, 'Decimal128');
});
@@ -2986,6 +3034,31 @@ describe('schema', function() {
assert.equal(schema._getSchema('child.testMap.foo.bar').instance, 'Mixed');
});
+ it('should not allow to create a path with primitive values (gh-7558)', () => {
+ assert.throws(() => {
+ new Schema({
+ foo: false
+ });
+ }, /invalid.*false.*foo/i);
+
+ assert.throws(() => {
+ const schema = new Schema();
+ schema.add({ foo: false });
+ }, /invalid.*false.*foo/i);
+
+ assert.throws(() => {
+ new Schema({
+ foo: 1
+ });
+ }, /invalid.*1.*foo/i);
+
+ assert.throws(() => {
+ new Schema({
+ foo: 'invalid'
+ });
+ }, /invalid.*invalid.*foo/i);
+ });
+
it('should allow deleting a virtual path off the schema gh-8397', async function() {
const schema = new Schema({
name: String
@@ -3001,9 +3074,13 @@ describe('schema', function() {
assert.ok(schema.virtuals.foo);
schema.removeVirtual('foo');
assert.ok(!schema.virtuals.foo);
+ assert.ok(!schema.tree.foo);
+
+ schema.virtual('foo').get(v => v || 99);
+
const Test = db.model('gh-8397', schema);
const doc = new Test({ name: 'Test' });
- assert.equal(doc.foo, undefined);
+ assert.equal(doc.foo, 99);
});
it('should allow deleting multiple virtuals gh-8397', async function() {
@@ -3042,6 +3119,205 @@ describe('schema', function() {
assert.throws(() => {
schema.removeVirtual('foo');
}, { message: 'Attempting to remove virtual "foo" that does not exist.' });
+ });
+ it('should throw an error if using schema with "timeseries" option as a nested schema', function() {
+ const subSchema = new Schema({
+ name: String
+ }, { timeseries: { timeField: 'timestamp', metaField: 'metadata', granularity: 'hours' } });
+ assert.throws(() => {
+ new Schema({
+ name: String,
+ array: [subSchema]
+ });
+ }, { message: 'Cannot create use schema for property "array" because the schema has the timeseries option enabled.' });
+ assert.throws(() => {
+ new Schema({
+ name: String,
+ subdoc: subSchema
+ });
+ }, { message: 'Cannot create use schema for property "subdoc" because the schema has the timeseries option enabled.' });
+ });
+ it('should allow timestamps on a sub document when having _id field in the main document gh-13343', async function() {
+ const ImageSchema = new Schema({
+ dimensions: {
+ type: new Schema({
+ width: { type: Number, required: true },
+ height: { type: Number, required: true }
+ }, { timestamps: true }),
+ required: true
+ }
+ }, { timestamps: true });
+
+ const DataSchema = new Schema({
+ tags: { type: ImageSchema, required: false, _id: false }
+ });
+
+ const Test = db.model('gh13343', DataSchema);
+ const res = await Test.insertMany([
+ {
+ tags: {
+ dimensions: { width: 960, height: 589 }
+ }
+ }
+ ]);
+ assert.ok(res);
+ assert.ok(res[0].tags.createdAt);
+ assert.ok(res[0].tags.updatedAt);
+ });
+ it('should not save objectids as strings when using the `flattenObjectIds` option (gh-13648)', async function() {
+ const testSchema = new Schema({
+ name: String
+ }, { toObject: { flattenObjectIds: true } });
+ const Test = db.model('gh13648', testSchema);
+
+ const doc = await Test.create({ name: 'Test Testerson' });
+ const res = await Test.findOne({ _id: { $eq: doc._id, $type: 'objectId' } });
+ assert.equal(res.name, 'Test Testerson');
+ });
+ it('deduplicates idGetter (gh-14457)', function() {
+ const schema = new Schema({ name: String });
+ schema._preCompile();
+ assert.equal(schema.virtual('id').getters.length, 1);
+ schema._preCompile();
+ assert.equal(schema.virtual('id').getters.length, 1);
+ });
+
+ it('handles recursive definitions in discriminators (gh-13978)', function() {
+ const base = new Schema({
+ type: { type: Number, required: true }
+ }, { discriminatorKey: 'type' });
+
+ const recursive = new Schema({
+ self: { type: base, required: true }
+ });
+
+ base.discriminator(1, recursive);
+ const TestModel = db.model('gh13978', base);
+
+ const doc = new TestModel({ type: 1, self: { type: 1 } });
+ assert.strictEqual(doc.self.type, 1);
+ });
+ it('handles recursive definitions of arrays in discriminators (gh-14055)', function() {
+ const base = new Schema({
+ type: { type: Number, required: true }
+ }, { discriminatorKey: 'type' });
+
+ const recursive = new Schema({
+ self: { type: [base], required: true }
+ });
+ base.discriminator(1, recursive);
+ const baseModel = db.model('gh14055', base);
+ const doc = new baseModel({ type: 1, self: [{ type: 1 }] });
+ assert.equal(doc.self[0].type, 1);
+ });
+ it('should have the correct schema definition with array schemas (gh-14416)', function() {
+ const schema = new Schema({
+ nums: [{ type: Array, of: Number }],
+ tags: [{ type: 'Array', of: String }],
+ subdocs: [{ type: Array, of: Schema({ name: String }) }]
+ });
+ assert.equal(schema.path('nums.$').caster.instance, 'Number'); // actually Mixed
+ assert.equal(schema.path('tags.$').caster.instance, 'String'); // actually Mixed
+ assert.equal(schema.path('subdocs.$').casterConstructor.schema.path('name').instance, 'String'); // actually Mixed
+ });
+ it('handles discriminator options with Schema.prototype.discriminator (gh-14448)', async function() {
+ const eventSchema = new mongoose.Schema({
+ name: String
+ }, { discriminatorKey: 'kind' });
+ const clickedEventSchema = new mongoose.Schema({ element: String });
+ eventSchema.discriminator(
+ 'Test2',
+ clickedEventSchema,
+ { value: 'click' }
+ );
+ const Event = db.model('Test', eventSchema);
+ const ClickedModel = db.model('Test2');
+
+ const doc = await Event.create({ kind: 'click', element: '#hero' });
+ assert.equal(doc.element, '#hero');
+ assert.ok(doc instanceof ClickedModel);
+ });
+
+ it('supports schema-level readConcern (gh-14511)', async function() {
+ const eventSchema = new mongoose.Schema({
+ name: String
+ }, { readConcern: { level: 'available' } });
+ const Event = db.model('Test', eventSchema);
+
+ let q = Event.find();
+ let options = q._optionsForExec();
+ assert.deepStrictEqual(options.readConcern, { level: 'available' });
+
+ q = Event.find().setOptions({ readConcern: { level: 'local' } });
+ options = q._optionsForExec();
+ assert.deepStrictEqual(options.readConcern, { level: 'local' });
+
+ q = Event.find().setOptions({ readConcern: null });
+ options = q._optionsForExec();
+ assert.deepStrictEqual(options.readConcern, null);
+
+ await q;
+ });
+
+ it('supports casting object to subdocument (gh-14748) (gh-9076)', function() {
+ const nestedSchema = new Schema({ name: String });
+ nestedSchema.methods.getAnswer = () => 42;
+
+ const schema = new Schema({
+ arr: [nestedSchema],
+ singleNested: nestedSchema
+ });
+
+ // Cast to doc array
+ let subdoc = schema.path('arr').cast([{ name: 'foo' }])[0];
+ assert.ok(subdoc instanceof mongoose.Document);
+ assert.equal(subdoc.getAnswer(), 42);
+
+ // Cast to single nested subdoc
+ subdoc = schema.path('singleNested').cast({ name: 'bar' });
+ assert.ok(subdoc instanceof mongoose.Document);
+ assert.equal(subdoc.getAnswer(), 42);
+ });
+ it('throws "already has an index" error if duplicate index definition (gh-15056)', function() {
+ sinon.stub(utils, 'warn').callsFake(() => {});
+ try {
+ const ObjectKeySchema = new mongoose.Schema({
+ key: {
+ type: String,
+ required: true,
+ unique: true
+ },
+ type: {
+ type: String,
+ required: false
+ }
+ });
+
+ ObjectKeySchema.index({ key: 1 });
+ assert.equal(utils.warn.getCalls().length, 1);
+ let [message] = utils.warn.getCalls()[0].args;
+ assert.equal(
+ message,
+ 'Duplicate schema index on {"key":1} found. This is often due to declaring an index using both "index: true" and "schema.index()". Please remove the duplicate index definition.'
+ );
+
+ ObjectKeySchema.index({ key: 1, type: 1 });
+ assert.equal(utils.warn.getCalls().length, 1);
+ ObjectKeySchema.index({ key: 1, type: 1 });
+ assert.equal(utils.warn.getCalls().length, 2);
+ [message] = utils.warn.getCalls()[1].args;
+ assert.equal(
+ message,
+ 'Duplicate schema index on {"key":1,"type":1} found. This is often due to declaring an index using both "index: true" and "schema.index()". Please remove the duplicate index definition.'
+ );
+
+ ObjectKeySchema.index({ type: 1, key: 1 });
+ ObjectKeySchema.index({ key: 1, type: -1 });
+ ObjectKeySchema.index({ key: 1, type: 1 }, { unique: true, name: 'special index' });
+ assert.equal(utils.warn.getCalls().length, 2);
+ } finally {
+ sinon.restore();
+ }
});
});
diff --git a/test/schema.uuid.test.js b/test/schema.uuid.test.js
index 3cf350071c0..77b7b2300fa 100644
--- a/test/schema.uuid.test.js
+++ b/test/schema.uuid.test.js
@@ -5,6 +5,7 @@ const util = require('./util');
const assert = require('assert');
const bson = require('bson');
+const { randomUUID } = require('crypto');
const mongoose = start.mongoose;
const Schema = mongoose.Schema;
@@ -130,7 +131,7 @@ describe('SchemaUUID', function() {
it('works with populate (gh-13267)', async function() {
const userSchema = new mongoose.Schema({
- _id: { type: 'UUID', default: () => uuidv4() },
+ _id: { type: 'UUID', default: () => randomUUID() },
name: String,
createdBy: {
type: 'UUID',
@@ -149,6 +150,59 @@ describe('SchemaUUID', function() {
await pop.save();
});
+ it('handles built-in UUID type (gh-13103)', async function() {
+ const schema = new Schema({
+ _id: {
+ type: Schema.Types.UUID
+ }
+ }, { _id: false });
+
+ db.deleteModel(/Test/);
+ const Test = db.model('Test', schema);
+
+ const uuid = new mongoose.Types.UUID();
+ let { _id } = await Test.create({ _id: uuid });
+ assert.ok(_id);
+ assert.equal(typeof _id, 'string');
+ assert.equal(_id, uuid.toString());
+
+ ({ _id } = await Test.findById(uuid));
+ assert.ok(_id);
+ assert.equal(typeof _id, 'string');
+ assert.equal(_id, uuid.toString());
+ });
+
+ it('avoids converting maps of uuids to strings (gh-13657)', async function() {
+ const schema = new mongoose.Schema(
+ {
+ doc_map: {
+ type: mongoose.Schema.Types.Map,
+ of: mongoose.Schema.Types.UUID
+ }
+ }
+ );
+ db.deleteModel(/Test/);
+ const Test = db.model('Test', schema);
+ await Test.deleteMany({});
+
+ const user = new Test({
+ doc_map: new Map([
+ ['role_1', new mongoose.Types.UUID()],
+ ['role_2', new mongoose.Types.UUID()]
+ ])
+ });
+
+ await user.save();
+
+ user.doc_map.set('role_1', new mongoose.Types.UUID());
+ await user.save();
+
+ const exists = await Test.findOne({ 'doc_map.role_1': { $type: 'binData' } });
+ assert.ok(exists);
+
+ assert.equal(typeof user.get('doc_map.role_1'), 'string');
+ });
+
// the following are TODOs based on SchemaUUID.prototype.$conditionalHandlers which are not tested yet
it('should work with $bits* operators');
it('should work with $all operator');
diff --git a/test/schema.validation.test.js b/test/schema.validation.test.js
index 4d627786a2c..d70643c1825 100644
--- a/test/schema.validation.test.js
+++ b/test/schema.validation.test.js
@@ -6,7 +6,6 @@
const start = require('./common');
-const Promise = require('bluebird');
const assert = require('assert');
const random = require('./util').random;
const { v4: uuidv4 } = require('uuid');
@@ -178,27 +177,24 @@ describe('schema', function() {
await db.close();
});
- it('and can be set to "undefined" (gh-1594)', function(done) {
+ it('and can be set to "undefined" (gh-1594)', async function() {
const p = new Person({ name: 'Daniel' });
p.num_cars = 25;
- p.save(function(err) {
- assert.ifError(err);
- assert.equal(p.num_cars, 25);
- p.num_cars = undefined;
+ await p.save();
+ assert.equal(p.num_cars, 25);
+ p.num_cars = undefined;
- p.save(function(err) {
- assert.ifError(err);
- assert.equal(p.num_cars, undefined);
- p.num_cars = 5;
+ await p.save();
+ assert.equal(p.num_cars, undefined);
+ p.num_cars = 5;
- p.save(function(err) {
- // validation should still work for non-undefined values
- assert.ok(err);
- done();
- });
- });
- });
+ try {
+ await p.save();
+ assert.ok(false);
+ } catch (err) {
+ assert.ok(err);
+ }
});
});
@@ -515,7 +511,7 @@ describe('schema', function() {
let executed = 0;
function validator(value) {
- return new global.Promise(function(resolve) {
+ return new Promise(function(resolve) {
setTimeout(function() {
executed++;
resolve(value === true);
@@ -543,7 +539,7 @@ describe('schema', function() {
let called = false;
function validator() {
- return new global.Promise(resolve => {
+ return new Promise(resolve => {
assert.equal(this.a, 'b');
setTimeout(function() {
called = true;
@@ -607,7 +603,7 @@ describe('schema', function() {
describe('messages', function() {
describe('are customizable', function() {
- it('within schema definitions', function(done) {
+ it('within schema definitions', async function() {
const schema = new Schema({
name: { type: String, enum: ['one', 'two'] },
myenum: { type: String, enum: { values: ['x'], message: 'enum validator failed for path: {PATH} with {VALUE}' } },
@@ -624,38 +620,38 @@ describe('schema', function() {
const A = mongoose.model('schema-validation-messages-' + random(), schema);
const a = new A();
- a.validate(function(err) {
- assert.equal(err.errors.requiredString1, 'Path `requiredString1` is required.');
- assert.equal(err.errors.requiredString2, 'oops, requiredString2 is missing. required');
-
- a.requiredString1 = a.requiredString2 = 'hi';
- a.name = 'three';
- a.myenum = 'y';
- a.matchString0 = a.matchString1 = 'no match';
- a.numMin0 = a.numMin1 = 2;
- a.numMax0 = a.numMax1 = 30;
-
- a.validate(function(err) {
- assert.equal(err.errors.name, '`three` is not a valid enum value for path `name`.');
- assert.equal(err.errors.myenum, 'enum validator failed for path: myenum with y');
- assert.equal(err.errors.matchString0, 'Path `matchString0` is invalid (no match).');
- assert.equal(err.errors.matchString1, 'invalid string for matchString1 with value: no match');
- assert.equal(String(err.errors.numMin0), 'Path `numMin0` (2) is less than minimum allowed value (10).');
- assert.equal(String(err.errors.numMin1), 'hey, numMin1 is too small');
- assert.equal(err.errors.numMax0, 'Path `numMax0` (30) is more than maximum allowed value (20).');
- assert.equal(String(err.errors.numMax1), 'hey, numMax1 (30) is greater than 20');
-
- a.name = 'one';
- a.myenum = 'x';
- a.requiredString1 = 'fixed';
- a.matchString1 = a.matchString0 = 'bryancranston is an actor';
- a.numMin0 = a.numMax0 = a.numMin1 = a.numMax1 = 15;
- a.validate(done);
- });
- });
- });
-
- it('for custom validators', function(done) {
+ const err = await a.validate().then(() => null, err => err);
+ assert.equal(err.errors.requiredString1.message, 'Path `requiredString1` is required.');
+ assert.equal(err.errors.requiredString2.message, 'oops, requiredString2 is missing. required');
+
+ a.requiredString1 = a.requiredString2 = 'hi';
+ a.name = 'three';
+ a.myenum = 'y';
+ a.matchString0 = a.matchString1 = 'no match';
+ a.numMin0 = a.numMin1 = 2;
+ a.numMax0 = a.numMax1 = 30;
+
+ const err2 = await a.validate().then(() => null, err => err);
+ assert.equal(err2.errors.name.message, '`three` is not a valid enum value for path `name`.');
+ assert.equal(err2.errors.myenum.message, 'enum validator failed for path: myenum with y');
+ assert.equal(err2.errors.matchString0.message, 'Path `matchString0` is invalid (no match).');
+ assert.equal(err2.errors.matchString1.message, 'invalid string for matchString1 with value: no match');
+ assert.equal(err2.errors.numMin0.message, 'Path `numMin0` (2) is less than minimum allowed value (10).');
+ assert.equal(err2.errors.numMin1.message, 'hey, numMin1 is too small');
+ assert.equal(err2.errors.numMax0.message, 'Path `numMax0` (30) is more than maximum allowed value (20).');
+ assert.equal(err2.errors.numMax1.message, 'hey, numMax1 (30) is greater than 20');
+
+ a.name = 'one';
+ a.myenum = 'x';
+ a.requiredString1 = 'fixed';
+ a.matchString1 = a.matchString0 = 'bryancranston is an actor';
+ a.numMin0 = a.numMax0 = a.numMin1 = a.numMax1 = 15;
+
+ const err3 = await a.validate().then(() => null, err => err);
+ assert(!err3);
+ });
+
+ it('for custom validators', async function() {
const validate = function() {
return false;
};
@@ -666,14 +662,10 @@ describe('schema', function() {
const m = new M({ x: [3, 4, 5, 6] });
- m.validate(function(err) {
- assert.equal(String(err.errors.x), 'x failed validation (3,4,5,6)');
- assert.equal(err.errors.x.kind, 'user defined');
- done();
- });
+ await assert.rejects(m.validate(), /x failed validation/);
});
- it('custom validators with promise (gh-5171)', function(done) {
+ it('custom validators with promise (gh-5171)', async function() {
const validate = async function(v) {
return Promise.resolve(v === 'test');
};
@@ -691,13 +683,15 @@ describe('schema', function() {
const m = new M({ x: 'not test' });
- m.validate(function(err) {
+ try {
+ await m.validate();
+ assert.ok(false);
+ } catch (err) {
assert.ok(err.errors['x']);
- done();
- });
+ }
});
- it('supports custom properties (gh-2132)', function(done) {
+ it('supports custom properties (gh-2132)', async function() {
const schema = new Schema({
x: {
type: String,
@@ -713,15 +707,17 @@ describe('schema', function() {
const M = mongoose.model('gh-2132', schema, 'gh-2132');
const m = new M({ x: 'a' });
- m.validate(function(err) {
+ try {
+ await m.validate();
+ assert.ok(false);
+ } catch (err) {
assert.equal(err.errors.x.toString(), 'Error code 25');
assert.equal(err.errors.x.properties.message, 'Error code 25');
assert.equal(err.errors.x.properties.errorCode, 25);
- done();
- });
+ }
});
- it('supports dynamic message for validators with callback (gh-1936)', function(done) {
+ it('supports dynamic message for validators with callback (gh-1936)', async function() {
const schema = new Schema({
x: {
type: String,
@@ -736,17 +732,15 @@ describe('schema', function() {
const M = mongoose.model('gh-1936', schema, 'gh-1936');
const m = new M({ x: 'whatever' });
- m.validate(function(err) {
- assert.equal(err.errors.x.toString(), 'Custom message');
- done();
- });
+ const err = await m.validate().then(() => null, err => err);
+ assert.equal(err.errors.x.toString(), 'Custom message');
});
});
});
describe('types', function() {
describe('are customizable', function() {
- it('for single custom validators', function(done) {
+ it('for single custom validators', async function() {
function validate() {
return false;
}
@@ -758,15 +752,17 @@ describe('schema', function() {
const m = new M({ x: [3, 4, 5, 6] });
- m.validate(function(err) {
+ try {
+ await m.validate();
+ assert.ok(false);
+ } catch (err) {
assert.equal(String(err.errors.x), 'x failed validation (3,4,5,6)');
assert.equal(err.errors.x.properties.message, 'x failed validation (3,4,5,6)');
assert.equal(err.errors.x.kind, 'customType');
- done();
- });
+ }
});
- it('for many custom validators', function(done) {
+ it('for many custom validators', async function() {
function validate() {
return false;
}
@@ -779,34 +775,33 @@ describe('schema', function() {
const m = new M({ x: [3, 4, 5, 6] });
- m.validate(function(err) {
+ try {
+ await m.validate();
+ assert.ok(false);
+ } catch (err) {
assert.equal(String(err.errors.x), 'x failed validation (3,4,5,6)');
assert.equal(err.errors.x.kind, 'customType');
- done();
- });
+ }
});
});
});
- it('should clear validator errors (gh-2302)', function(done) {
+ it('should clear validator errors (gh-2302)', async function() {
const userSchema = new Schema({ name: { type: String, required: true } });
const User = mongoose.model('gh-2302', userSchema, 'gh-2302');
const user = new User();
- user.validate(function(err) {
- assert.ok(err);
- assert.ok(user.errors);
- assert.ok(user.errors.name);
- user.name = 'bacon';
- user.validate(function(err) {
- assert.ok(!err);
- assert.ok(!user.$__.validationError);
- done();
- });
- });
+ const err = await user.validate().then(() => null, err => err);
+ assert.ok(err);
+ assert.ok(user.errors);
+ assert.ok(user.errors.name);
+ user.name = 'bacon';
+ const err2 = await user.validate().then(() => null, err => err);
+ assert.ok(!err2);
+ assert.ok(!user.$__.validationError);
});
- it('should allow an array of enums (gh-661)', function(done) {
+ it('should allow an array of enums (gh-661)', async function() {
const validBreakfastFoods = ['bacon', 'eggs', 'steak', 'coffee', 'butter'];
const breakfastSchema = new Schema({
foods: [{ type: String, enum: validBreakfastFoods }]
@@ -814,26 +809,38 @@ describe('schema', function() {
const Breakfast = mongoose.model('gh-661', breakfastSchema, 'gh-661');
const goodBreakfast = new Breakfast({ foods: ['eggs', 'bacon'] });
- goodBreakfast.validate(function(error) {
- assert.ifError(error);
+ await goodBreakfast.validate();
- const badBreakfast = new Breakfast({ foods: ['tofu', 'waffles', 'coffee'] });
- badBreakfast.validate(function(error) {
- assert.ok(error);
- assert.ok(error.errors['foods.0']);
- assert.equal(error.errors['foods.0'].message,
- '`tofu` is not a valid enum value for path `foods.0`.');
- assert.ok(error.errors['foods.1']);
- assert.equal(error.errors['foods.1'].message,
- '`waffles` is not a valid enum value for path `foods.1`.');
- assert.ok(!error.errors['foods.2']);
+ const badBreakfast = new Breakfast({ foods: ['tofu', 'waffles', 'coffee'] });
+ try {
+ await badBreakfast.validate();
+ throw new Error('should not get here');
+ } catch (error) {
+ assert.ok(error);
+ assert.ok(error.errors['foods.0']);
+ assert.equal(error.errors['foods.0'].message,
+ '`tofu` is not a valid enum value for path `foods.0`.');
+ assert.ok(error.errors['foods.1']);
+ assert.equal(error.errors['foods.1'].message,
+ '`waffles` is not a valid enum value for path `foods.1`.');
+ assert.ok(!error.errors['foods.2']);
+ }
+ });
- done();
- });
+ it('should allow null values for enum gh-3044', async function() {
+ const testSchema = new Schema({
+ name: {
+ type: String,
+ enum: ['test']
+ }
});
+ const Test = mongoose.model('allow-null' + random(), testSchema);
+ const a = new Test({ name: null });
+ const err = await a.validate().then(() => null, err => err);
+ assert.equal(err, null);
});
- it('should allow an array of subdocuments with enums (gh-3521)', function(done) {
+ it('should allow an array of subdocuments with enums (gh-3521)', async function() {
const coolSchema = new Schema({
votes: [{
vote: { type: String, enum: ['cool', 'not-cool'] }
@@ -845,26 +852,25 @@ describe('schema', function() {
cool.votes.push(cool.votes.create({
vote: 'cool'
}));
- cool.validate(function(error) {
- assert.ifError(error);
+ await cool.validate();
- const terrible = new Cool();
- terrible.votes.push(terrible.votes.create({
- vote: 'terrible'
- }));
-
- terrible.validate(function(error) {
- assert.ok(error);
- assert.ok(error.errors['votes.0.vote']);
- assert.equal(error.errors['votes.0.vote'].message,
- '`terrible` is not a valid enum value for path `vote`.');
+ const terrible = new Cool();
+ terrible.votes.push(terrible.votes.create({
+ vote: 'terrible'
+ }));
- done();
- });
- });
+ try {
+ await terrible.validate();
+ throw new Error('Should have failed validation');
+ } catch (error) {
+ assert.ok(error);
+ assert.ok(error.errors['votes.0.vote']);
+ assert.equal(error.errors['votes.0.vote'].message,
+ '`terrible` is not a valid enum value for path `vote`.');
+ }
});
- it('should validate subdocuments subproperty enums (gh-4111)', function(done) {
+ it('should validate subdocuments subproperty enums (gh-4111)', async function() {
const M = mongoose.model('M', new Schema({
p: {
val: { type: String, enum: ['test'] }
@@ -885,24 +891,27 @@ describe('schema', function() {
model.children.push(child);
- model.validate(function(error) {
+ try {
+ await model.validate();
+ } catch (error) {
assert.ifError(error);
+ }
- child.prop.val = 'invalid';
-
- assert.equal(model.children[0].prop.val, 'invalid');
+ child.prop.val = 'invalid';
- model.validate(function(error) {
- assert.ok(error);
- assert.equal(error.errors['children.0.prop.val'].message,
- '`invalid` is not a valid enum value for path `prop.val`.');
+ assert.equal(model.children[0].prop.val, 'invalid');
- done();
- });
- });
+ try {
+ await model.validate();
+ assert.ok(false);
+ } catch (error) {
+ assert.ok(error);
+ assert.equal(error.errors['children.0.prop.val'].message,
+ '`invalid` is not a valid enum value for path `prop.val`.');
+ }
});
- it('doesnt do double validation on document arrays (gh-2618)', function(done) {
+ it('doesnt do double validation on document arrays (gh-2618)', async function() {
const A = new Schema({ str: String });
let B = new Schema({ a: [A] });
let validateCalls = 0;
@@ -915,11 +924,8 @@ describe('schema', function() {
const p = new B();
p.a.push({ str: 'asdf' });
- p.validate(function(err) {
- assert.ifError(err);
- assert.equal(validateCalls, 1);
- done();
- });
+ await p.validate();
+ assert.equal(validateCalls, 1);
});
it('doesnt do double validation on document arrays underneath nested (gh-5411)', function(done) {
@@ -968,7 +974,7 @@ describe('schema', function() {
done();
});
- it('no double validation on set nested docarray (gh-4145)', function(done) {
+ it('no double validation on set nested docarray (gh-4145)', async() => {
let calls = 0;
const myValidator = function() {
++calls;
@@ -1006,40 +1012,38 @@ describe('schema', function() {
}]
};
- instance.validate(function(error) {
- assert.ifError(error);
- assert.equal(calls, 1);
- done();
- });
+ await instance.validate();
+
+ assert.equal(calls, 1);
});
- it('returns cast errors', function(done) {
+ it('returns cast errors', async function() {
const breakfastSchema = new Schema({
eggs: Number
});
const Breakfast = mongoose.model('gh-2611', breakfastSchema, 'gh-2611');
const bad = new Breakfast({ eggs: 'none' });
- bad.validate(function(error) {
- assert.ok(error);
- done();
- });
+ const error = await bad.validate().then(() => null, err => err);
+ assert.ok(error);
});
- it('handles multiple subdocument errors (gh-2589)', function(done) {
+ it('handles multiple subdocument errors (gh-2589)', async function() {
const foodSchema = new Schema({ name: { type: String, required: true, enum: ['bacon', 'eggs'] } });
const breakfast = new Schema({ foods: [foodSchema], id: Number });
const Breakfast = mongoose.model('gh-2589', breakfast, 'gh-2589');
const bad = new Breakfast({ foods: [{ name: 'tofu' }, { name: 'waffles' }], id: 'Not a number' });
- bad.validate(function(error) {
+ try {
+ await bad.validate();
+ assert.ok(false);
+ } catch (error) {
assert.ok(error);
assert.deepEqual(['id', 'foods.0.name', 'foods.1.name'], Object.keys(error.errors));
- done();
- });
+ }
});
- it('handles subdocument cast errors (gh-2819)', function(done) {
+ it('handles subdocument cast errors (gh-2819)', async function() {
const foodSchema = new Schema({ eggs: { type: Number, required: true } });
const breakfast = new Schema({ foods: [foodSchema], id: Number });
@@ -1047,73 +1051,70 @@ describe('schema', function() {
// Initially creating subdocs with cast errors
const bad = new Breakfast({ foods: [{ eggs: 'Not a number' }], id: 'Not a number' });
- bad.validate(function(error) {
- assert.ok(error);
- assert.deepEqual(['foods.0.eggs', 'id'], Object.keys(error.errors).sort());
- assert.ok(error.errors['foods.0.eggs'] instanceof mongoose.Error.CastError);
-
- // Pushing docs with cast errors
- bad.foods.push({ eggs: 'Also not a number' });
- bad.validate(function(error) {
- assert.deepEqual(['foods.0.eggs', 'foods.1.eggs', 'id'], Object.keys(error.errors).sort());
- assert.ok(error.errors['foods.1.eggs'] instanceof mongoose.Error.CastError);
-
- // Splicing docs with cast errors
- bad.foods.splice(1, 1, { eggs: 'fail1' }, { eggs: 'fail2' });
- bad.validate(function(error) {
- assert.deepEqual(['foods.0.eggs', 'foods.1.eggs', 'foods.2.eggs', 'id'], Object.keys(error.errors).sort());
- assert.ok(error.errors['foods.0.eggs'] instanceof mongoose.Error.CastError);
- assert.ok(error.errors['foods.1.eggs'] instanceof mongoose.Error.CastError);
- assert.ok(error.errors['foods.2.eggs'] instanceof mongoose.Error.CastError);
-
- // Remove the cast error by setting field
- bad.foods[2].eggs = 3;
- bad.validate(function(error) {
- assert.deepEqual(['foods.0.eggs', 'foods.1.eggs', 'id'], Object.keys(error.errors).sort());
- assert.ok(error.errors['foods.0.eggs'] instanceof mongoose.Error.CastError);
- assert.ok(error.errors['foods.1.eggs'] instanceof mongoose.Error.CastError);
-
- // Remove the cast error using array.set()
- bad.foods.set(1, { eggs: 1 });
- bad.validate(function(error) {
- assert.deepEqual(['foods.0.eggs', 'id'], Object.keys(error.errors).sort());
- assert.ok(error.errors['foods.0.eggs'] instanceof mongoose.Error.CastError);
-
- done();
- });
- });
- });
- });
- });
+ let error = await bad.validate().then(() => null, err => err);
+ assert.ok(error);
+ assert.deepEqual(['foods.0.eggs', 'id'], Object.keys(error.errors).sort());
+ assert.ok(error.errors['foods.0.eggs'] instanceof mongoose.Error.CastError);
+
+ // Pushing docs with cast errors
+ bad.foods.push({ eggs: 'Also not a number' });
+ error = await bad.validate().then(() => null, err => err);
+ assert.deepEqual(['foods.0.eggs', 'foods.1.eggs', 'id'], Object.keys(error.errors).sort());
+ assert.ok(error.errors['foods.1.eggs'] instanceof mongoose.Error.CastError);
+
+ // Splicing docs with cast errors
+ bad.foods.splice(1, 1, { eggs: 'fail1' }, { eggs: 'fail2' });
+ error = await bad.validate().then(() => null, err => err);
+ assert.deepEqual(['foods.0.eggs', 'foods.1.eggs', 'foods.2.eggs', 'id'], Object.keys(error.errors).sort());
+ assert.ok(error.errors['foods.0.eggs'] instanceof mongoose.Error.CastError);
+ assert.ok(error.errors['foods.1.eggs'] instanceof mongoose.Error.CastError);
+ assert.ok(error.errors['foods.2.eggs'] instanceof mongoose.Error.CastError);
+
+ // Remove the cast error by setting field
+ bad.foods[2].eggs = 3;
+ error = await bad.validate().then(() => null, err => err);
+ assert.deepEqual(['foods.0.eggs', 'foods.1.eggs', 'id'], Object.keys(error.errors).sort());
+ assert.ok(error.errors['foods.0.eggs'] instanceof mongoose.Error.CastError);
+ assert.ok(error.errors['foods.1.eggs'] instanceof mongoose.Error.CastError);
+
+ // Remove the cast error using array.set()
+ bad.foods[1].eggs = 1;
+ error = await bad.validate().then(() => null, err => err);
+ assert.deepEqual(['foods.0.eggs', 'id'], Object.keys(error.errors).sort());
+ assert.ok(error.errors['foods.0.eggs'] instanceof mongoose.Error.CastError);
});
- it('fails when you try to set a nested path to a primitive (gh-2592)', function(done) {
+ it('fails when you try to set a nested path to a primitive (gh-2592)', async function() {
const breakfast = new Schema({ foods: { bacon: Number, eggs: Number } });
const Breakfast = mongoose.model('gh-2592', breakfast, 'gh-2592');
const bad = new Breakfast();
bad.foods = 'waffles';
- bad.validate(function(error) {
+ try {
+ await bad.validate();
+ assert.ok(false);
+ } catch (error) {
assert.ok(error);
const errorMessage = 'foods: Cast to Object failed for value ' +
'"waffles" (type string) at path "foods"';
assert.ok(error.toString().indexOf(errorMessage) !== -1, error.toString());
- done();
- });
+ }
});
- it('doesnt execute other validators if required fails (gh-2725)', function(done) {
+ it('doesnt execute other validators if required fails (gh-2725)', async function() {
const breakfast = new Schema({ description: { type: String, required: true, maxlength: 50 } });
const Breakfast = mongoose.model('gh2725', breakfast, 'gh2725');
const bad = new Breakfast({});
- bad.validate(function(error) {
+ try {
+ await bad.validate();
+ assert.ok(false);
+ } catch (error) {
assert.ok(error);
const errorMessage = 'ValidationError: description: Path `description` is required.';
assert.equal(errorMessage, error.toString());
- done();
- });
+ }
});
it('doesnt execute other validators if required fails (gh-3025)', function(done) {
@@ -1146,20 +1147,39 @@ describe('schema', function() {
done();
});
- it('adds required validators to the front of the list (gh-2843)', function(done) {
+ it('validateSync validates array elements when setting pathsToValidate (gh-13159)', function() {
+ const schema = new Schema({
+ permissions: [{ type: String, enum: ['users', 'anotherPermission'] }]
+ });
+
+ const Model = mongoose.model('gh13159', schema);
+
+ const doc = new Model({
+ permissions: ['avocado']
+ });
+
+ const error = doc.validateSync('permissions');
+ assert.ok(error);
+ assert.equal(Object.keys(error.errors).length, 1);
+ assert.ok(error.errors['permissions.0']);
+ });
+
+ it('adds required validators to the front of the list (gh-2843)', async function() {
const breakfast = new Schema({ description: { type: String, maxlength: 50, required: true } });
const Breakfast = mongoose.model('gh2843', breakfast, 'gh2843');
const bad = new Breakfast({});
- bad.validate(function(error) {
+ try {
+ await bad.validate();
+ assert.ok(false);
+ } catch (error) {
assert.ok(error);
const errorMessage = 'ValidationError: description: Path `description` is required.';
assert.equal(errorMessage, error.toString());
- done();
- });
+ }
});
- it('sets path correctly when setter throws exception (gh-2832)', function(done) {
+ it('sets path correctly when setter throws exception (gh-2832)', async function() {
const breakfast = new Schema({
description: {
type: String, set: function() {
@@ -1170,17 +1190,19 @@ describe('schema', function() {
const Breakfast = mongoose.model('gh2832', breakfast, 'gh2832');
const bad = new Breakfast({ description: 'test' });
- bad.validate(function(error) {
+ try {
+ await bad.validate();
+ assert.ok(false);
+ } catch (error) {
assert.ok(error);
const errorMessage = 'ValidationError: description: Cast to String failed for value "test" (type string) at path "description"';
assert.equal(errorMessage, error.toString());
assert.ok(error.errors.description);
assert.equal(error.errors.description.reason.toString(), 'Error: oops');
- done();
- });
+ }
});
- it('allows you to validate embedded doc that was .create()-ed (gh-2902) (gh-2929)', function(done) {
+ it('allows you to validate embedded doc that was .create()-ed (gh-2902) (gh-2929)', async function() {
const parentSchema = mongoose.Schema({
children: [{ name: { type: String, required: true } }]
});
@@ -1189,19 +1211,13 @@ describe('schema', function() {
const p = new Parent();
const n = p.children.create({ name: '2' });
- n.validate(function(error) {
- assert.ifError(error);
- const bad = p.children.create({});
- p.children.push(bad);
- bad.validate(function(error) {
- assert.ok(error);
- assert.ok(error.errors['name']);
- done();
- });
- });
+ await n.validate();
+ const bad = p.children.create({});
+ p.children.push(bad);
+ await assert.rejects(bad.validate(), /name/);
});
- it('returns correct kind for user defined custom validators (gh-2885)', function(done) {
+ it('returns correct kind for user defined custom validators (gh-2885)', async function() {
const s = mongoose.Schema({
n: {
type: String, validate: {
@@ -1214,26 +1230,30 @@ describe('schema', function() {
const M = mongoose.model('gh2885', s);
const m = new M({ n: 'test' });
- m.validate(function(error) {
- assert.ok(error);
+ try {
+ await m.validate();
+ assert.ok(false);
+ } catch (error) {
+ assert.ok(error.errors);
assert.equal(error.errors.n.kind, 'user defined');
- done();
- });
+ }
});
- it('enums report kind (gh-3009)', function(done) {
+ it('enums report kind (gh-3009)', async function() {
const s = mongoose.Schema({ n: { type: String, enum: ['a', 'b'] } });
const M = mongoose.model('gh3009', s);
const m = new M({ n: 'test' });
- m.validate(function(error) {
+ try {
+ await m.validate();
+ assert.ok(false);
+ } catch (error) {
assert.ok(error);
assert.equal(error.errors.n.kind, 'enum');
- done();
- });
+ }
});
- it('enums on arrays (gh-6102) (gh-8449)', function() {
+ it('enums on arrays (gh-6102) (gh-8449)', async function() {
assert.throws(function() {
new Schema({
array: {
@@ -1264,13 +1284,13 @@ describe('schema', function() {
Model = mongoose.model('gh6102', MySchema);
const doc2 = new Model({ array: [1, 2, 3] });
- return doc1.validate().
- then(() => assert.ok(false), err => assert.equal(err.name, 'ValidationError')).
- then(() => doc2.validate()).
- then(() => assert.ok(false), err => assert.equal(err.name, 'ValidationError'));
+ let err = await doc1.validate().then(() => null, err => err);
+ assert.equal(err.name, 'ValidationError');
+ err = await doc2.validate().then(() => null, err => err);
+ assert.equal(err.name, 'ValidationError', err);
});
- it('skips conditional required (gh-3539)', function(done) {
+ it('skips conditional required (gh-3539)', async function() {
const s = mongoose.Schema({
n: {
type: Number, required: function() {
@@ -1281,10 +1301,8 @@ describe('schema', function() {
const M = mongoose.model('gh3539', s);
const m = new M();
- m.validate(function(error) {
- assert.ifError(error);
- done();
- });
+ const error = await m.validate().then(() => null, err => err);
+ assert.ifError(error);
});
it('handles function for date min/max (gh-7600)', function() {
@@ -1307,7 +1325,7 @@ describe('schema', function() {
assert.ifError(err);
});
- it('evaluate message function gh6523', function(done) {
+ it('evaluate message function gh6523', async function() {
const s = mongoose.Schema({
n: {
type: String,
@@ -1325,13 +1343,15 @@ describe('schema', function() {
const M = mongoose.model('gh6523', s);
const m = new M({ n: 0 });
- m.validate(function(error) {
+ try {
+ await m.validate();
+ assert.ok(false);
+ } catch (error) {
assert.equal('fail 0', error.errors['n'].message);
- done();
- });
+ }
});
- it('Allows for doc to be passed as another parameter (gh-12564)', function(done) {
+ it('Allows for doc to be passed as another parameter (gh-12564)', async function() {
let document = null;
const s = mongoose.Schema({
n: {
@@ -1351,15 +1371,17 @@ describe('schema', function() {
const M = mongoose.model('gh-12564', s);
const m = new M({ n: null, field: 'Yo' });
- m.validate(function(error) {
+ try {
+ await m.validate();
+ assert.ok(false);
+ } catch (error) {
assert.strictEqual(document, m);
assert.ok(error.errors['n'].message.includes(m._id));
assert.equal('fail n on doc ' + m._id, error.errors['n'].message);
- done();
- });
+ }
});
- it('evaluate message function for required field gh6523', function(done) {
+ it('evaluate message function for required field gh6523', async function() {
const s = mongoose.Schema({
n: {
type: String,
@@ -1372,11 +1394,14 @@ describe('schema', function() {
const M = mongoose.model('gh6523-2', s);
const m = new M();
- m.validate(function(error) {
+ try {
+ await m.validate();
+ assert.ok(false);
+ } catch (error) {
assert.equal('fail n', error.errors['n'].message);
- done();
- });
+ }
});
+
describe('`enum` accepts an object to support TypeScript enums (gh-9546) (gh-9535)', function() {
it('strings', function() {
// Arrange
diff --git a/test/schematype.cast.test.js b/test/schematype.cast.test.js
index acf2658f3af..77f28e2d9a7 100644
--- a/test/schematype.cast.test.js
+++ b/test/schematype.cast.test.js
@@ -29,6 +29,9 @@ describe('SchemaType.cast() (gh-7045)', function() {
class CustomObjectId extends Schema.ObjectId {}
CustomObjectId.cast(v => {
+ if (v === 'special') {
+ return original.objectid('0'.repeat(24));
+ }
assert.ok(v == null || (typeof v === 'string' && v.length === 24));
return original.objectid(v);
});
@@ -38,7 +41,7 @@ describe('SchemaType.cast() (gh-7045)', function() {
let threw = false;
try {
- objectid.cast('12charstring');
+ baseObjectId.cast('special');
} catch (error) {
threw = true;
assert.equal(error.name, 'CastError');
@@ -46,7 +49,7 @@ describe('SchemaType.cast() (gh-7045)', function() {
assert.doesNotThrow(function() {
objectid.cast('000000000000000000000000');
- baseObjectId.cast('12charstring');
+ objectid.cast('special');
baseObjectId.cast('000000000000000000000000');
});
diff --git a/test/schematype.test.js b/test/schematype.test.js
index 6e07390a2bc..725e21966a4 100644
--- a/test/schematype.test.js
+++ b/test/schematype.test.js
@@ -4,7 +4,9 @@
* Module dependencies.
*/
-const mongoose = require('./common').mongoose;
+const start = require('./common');
+
+const mongoose = start.mongoose;
const assert = require('assert');
@@ -198,25 +200,75 @@ describe('schematype', function() {
});
});
+ describe('get()', function() {
+ Object.values(mongoose.SchemaTypes).forEach(schemaType => {
+ it(`${schemaType.name} has a \`get\` method`, () => {
+ assert.strictEqual(typeof schemaType.get, 'function');
+ });
+ });
+ });
+
+ it('merges default validators (gh-14070)', function() {
+ class TestSchemaType extends mongoose.SchemaType {}
+ TestSchemaType.set('validate', checkIfString);
+
+ const schemaType = new TestSchemaType('test-path', {
+ validate: checkIfLength2
+ });
+
+ assert.equal(schemaType.validators.length, 2);
+ assert.equal(schemaType.validators[0].validator, checkIfString);
+ assert.equal(schemaType.validators[1].validator, checkIfLength2);
+
+ let err = schemaType.doValidateSync([1, 2]);
+ assert.ok(err);
+ assert.equal(err.name, 'ValidatorError');
+
+ err = schemaType.doValidateSync('foo');
+ assert.ok(err);
+ assert.equal(err.name, 'ValidatorError');
+
+ err = schemaType.doValidateSync('ab');
+ assert.ifError(err);
+
+ function checkIfString(v) {
+ return typeof v === 'string';
+ }
+ function checkIfLength2(v) {
+ return v.length === 2;
+ }
+ });
+
describe('set()', function() {
describe('SchemaType.set()', function() {
it('SchemaType.set, is a function', () => {
assert.equal(typeof mongoose.SchemaType.set, 'function');
});
+ it('should allow setting values to a given property gh-13510', async function() {
+ const m = new mongoose.Mongoose();
+ await m.connect(start.uri);
+ m.SchemaTypes.Date.setters.push(v => typeof v === 'string' && /^\d{8}$/.test(v) ? new Date(v.slice(0, 4), +v.slice(4, 6) - 1, v.slice(6, 8)) : v);
+ const testSchema = new m.Schema({
+ myDate: Date
+ });
+ const Test = m.model('Test', testSchema);
+ await Test.deleteMany({});
+ const doc = new Test();
+ doc.myDate = '20220601';
+ await doc.save();
+ await m.connections[0].close();
+ assert(doc.myDate instanceof Date);
+ });
+
+ after(() => {
+ mongoose.SchemaTypes.Date.setters = [];
+ });
});
- [
- mongoose.SchemaTypes.String,
- mongoose.SchemaTypes.Number,
- mongoose.SchemaTypes.Boolean,
- mongoose.SchemaTypes.Array,
- mongoose.SchemaTypes.Buffer,
- mongoose.SchemaTypes.Date,
- mongoose.SchemaTypes.ObjectId,
- mongoose.SchemaTypes.Mixed,
- mongoose.SchemaTypes.Decimal128,
- mongoose.SchemaTypes.Map
- ].forEach((type) => {
+ const typesToTest = Object.values(mongoose.SchemaTypes).
+ filter(t => t.name !== 'SchemaSubdocument' && t.name !== 'SchemaDocumentArray');
+
+ typesToTest.forEach((type) => {
it(type.name + ', when given a default option, set its', () => {
// Act
type.set('someRandomOption', true);
@@ -229,4 +281,47 @@ describe('schematype', function() {
});
});
});
+ it('demonstrates the `validateAll()` function (gh-6910)', function() {
+ const validateSchema = new Schema({ name: String, password: String });
+ validateSchema.path('name').validate({
+ validator: function(v) {
+ return v.length > 5;
+ },
+ message: 'name must be longer than 5 characters'
+ });
+ validateSchema.path('password').validateAll([
+ {
+ validator: function(v) {
+ return this.name !== v;
+ },
+ message: 'password must not equal name'
+ },
+ {
+ validator: function(v) {
+ return v.length > 5;
+ },
+ message: 'password must be at least six characters'
+ }
+ ]);
+ assert.equal(validateSchema.path('password').validators.length, 2);
+
+ const passwordPath = validateSchema.path('password');
+ assert.throws(
+ () => { throw passwordPath.doValidateSync('john', { name: 'john' }); },
+ /password must not equal name/
+ );
+ assert.throws(
+ () => { throw passwordPath.doValidateSync('short', { name: 'john' }); },
+ /password must be at least six characters/
+ );
+ });
+
+ it('supports getEmbeddedSchemaType() (gh-8389)', function() {
+ const schema = new Schema({ name: String, tags: [String] });
+ assert.strictEqual(schema.path('name').getEmbeddedSchemaType(), undefined);
+ const schemaType = schema.path('tags').getEmbeddedSchemaType();
+ assert.ok(schemaType);
+ assert.equal(schemaType.instance, 'String');
+ assert.equal(schemaType.path, 'tags');
+ });
});
diff --git a/test/timestamps.test.js b/test/timestamps.test.js
index 1315c16af16..49ab3e82762 100644
--- a/test/timestamps.test.js
+++ b/test/timestamps.test.js
@@ -24,7 +24,7 @@ describe('timestamps', function() {
// These tests use schemas only, no database connection needed.
describe('schema options', function() {
- it('should have createdAt and updatedAt fields', function(done) {
+ it('should have createdAt and updatedAt fields', function() {
const TestSchema = new Schema({
name: String
}, {
@@ -33,10 +33,10 @@ describe('timestamps', function() {
assert.ok(TestSchema.path('createdAt'));
assert.ok(TestSchema.path('updatedAt'));
- done();
+
});
- it('should have createdAt and updatedAt fields', function(done) {
+ it('should have createdAt and updatedAt fields', function() {
const TestSchema = new Schema({
name: String
});
@@ -45,10 +45,10 @@ describe('timestamps', function() {
assert.ok(TestSchema.path('createdAt'));
assert.ok(TestSchema.path('updatedAt'));
- done();
+
});
- it('should have created and updatedAt fields', function(done) {
+ it('should have created and updatedAt fields', function() {
const TestSchema = new Schema({
name: String
}, {
@@ -59,10 +59,10 @@ describe('timestamps', function() {
assert.ok(TestSchema.path('created'));
assert.ok(TestSchema.path('updatedAt'));
- done();
+
});
- it('should have created and updatedAt fields', function(done) {
+ it('should have created and updatedAt fields', function() {
const TestSchema = new Schema({
name: String
});
@@ -73,10 +73,10 @@ describe('timestamps', function() {
assert.ok(TestSchema.path('created'));
assert.ok(TestSchema.path('updatedAt'));
- done();
+
});
- it('should have created and updated fields', function(done) {
+ it('should have created and updated fields', function() {
const TestSchema = new Schema({
name: String
}, {
@@ -88,10 +88,10 @@ describe('timestamps', function() {
assert.ok(TestSchema.path('created'));
assert.ok(TestSchema.path('updated'));
- done();
+
});
- it('should have just createdAt if updatedAt set to falsy', function(done) {
+ it('should have just createdAt if updatedAt set to falsy', function() {
const TestSchema = new Schema({
name: String
});
@@ -102,10 +102,10 @@ describe('timestamps', function() {
assert.ok(TestSchema.path('createdAt'));
assert.ok(!TestSchema.path('updatedAt'));
- done();
+
});
- it('should have created and updated fields', function(done) {
+ it('should have created and updated fields', function() {
const TestSchema = new Schema({
name: String
});
@@ -117,7 +117,7 @@ describe('timestamps', function() {
assert.ok(TestSchema.path('created'));
assert.ok(TestSchema.path('updated'));
- done();
+
});
it('TTL index with timestamps (gh-5656)', function() {
@@ -136,7 +136,7 @@ describe('timestamps', function() {
});
});
- it('does not override timestamp params defined in schema (gh-4868)', function(done) {
+ it('does not override timestamp params defined in schema (gh-4868)', async function() {
const startTime = Date.now();
const schema = new mongoose.Schema({
createdAt: {
@@ -151,38 +151,28 @@ describe('timestamps', function() {
}, { timestamps: true });
const M = db.model('Test', schema);
- M.create({ name: 'Test' }, function(error, doc) {
- assert.ifError(error);
- M.findOne({ _id: doc._id }, function(error, doc) {
- assert.ifError(error);
- assert.ok(!doc.createdAt);
- assert.ok(doc.updatedAt);
- assert.ok(doc.updatedAt.valueOf() >= startTime);
- done();
- });
- });
+ let doc = await M.create({ name: 'Test' });
+ doc = await M.findOne({ _id: doc._id });
+ assert.ok(!doc.createdAt);
+ assert.ok(doc.updatedAt);
+ assert.ok(doc.updatedAt.valueOf() >= startTime);
});
- it('updatedAt without createdAt (gh-5598)', function(done) {
+ it('updatedAt without createdAt (gh-5598)', async function() {
const startTime = Date.now();
const schema = new mongoose.Schema({
name: String
}, { timestamps: { createdAt: null, updatedAt: true } });
const M = db.model('Test', schema);
- M.create({ name: 'Test' }, function(error, doc) {
- assert.ifError(error);
- M.findOne({ _id: doc._id }, function(error, doc) {
- assert.ifError(error);
- assert.ok(!doc.createdAt);
- assert.ok(doc.updatedAt);
- assert.ok(doc.updatedAt.valueOf() >= startTime);
- done();
- });
- });
+ let doc = await M.create({ name: 'Test' });
+ doc = await M.findOne({ _id: doc._id });
+ assert.ok(!doc.createdAt);
+ assert.ok(doc.updatedAt);
+ assert.ok(doc.updatedAt.valueOf() >= startTime);
});
- it('updatedAt without createdAt for nested (gh-5598)', function(done) {
+ it('updatedAt without createdAt for nested (gh-5598)', async function() {
const startTime = Date.now();
const schema = new mongoose.Schema({
name: String
@@ -192,39 +182,31 @@ describe('timestamps', function() {
});
const M = db.model('Test', parentSchema);
- M.create({ child: { name: 'test' } }, function(error, doc) {
- assert.ifError(error);
- M.findOne({ _id: doc._id }, function(error, doc) {
- assert.ifError(error);
- assert.ok(!doc.child.createdAt);
- assert.ok(doc.child.updatedAt);
- assert.ok(doc.child.updatedAt.valueOf() >= startTime);
- done();
- });
- });
+ let doc = await M.create({ child: { name: 'test' } });
+ doc = await M.findOne({ _id: doc._id });
+ assert.ok(!doc.child.createdAt);
+ assert.ok(doc.child.updatedAt);
+ assert.ok(doc.child.updatedAt.valueOf() >= startTime);
+
});
- it('nested paths (gh-4503)', function(done) {
+ it('nested paths (gh-4503)', async function() {
const startTime = Date.now();
const schema = new mongoose.Schema({
name: String
}, { timestamps: { createdAt: 'ts.c', updatedAt: 'ts.a' } });
const M = db.model('Test', schema);
- M.create({ name: 'Test' }, function(error, doc) {
- assert.ifError(error);
- M.findOne({ _id: doc._id }, function(error, doc) {
- assert.ifError(error);
- assert.ok(doc.ts.c);
- assert.ok(doc.ts.c.valueOf() >= startTime);
- assert.ok(doc.ts.a);
- assert.ok(doc.ts.a.valueOf() >= startTime);
- done();
- });
- });
+ let doc = await M.create({ name: 'Test' });
+ doc = await M.findOne({ _id: doc._id });
+ assert.ok(doc.ts.c);
+ assert.ok(doc.ts.c.valueOf() >= startTime);
+ assert.ok(doc.ts.a);
+ assert.ok(doc.ts.a.valueOf() >= startTime);
+
});
- it('does not override nested timestamp params defined in schema (gh-4868)', function(done) {
+ it('does not override nested timestamp params defined in schema (gh-4868)', async function() {
const startTime = Date.now();
const schema = new mongoose.Schema({
ts: {
@@ -241,19 +223,14 @@ describe('timestamps', function() {
}, { timestamps: { createdAt: 'ts.createdAt', updatedAt: 'ts.updatedAt' } });
const M = db.model('Test', schema);
- M.create({ name: 'Test' }, function(error, doc) {
- assert.ifError(error);
- M.findOne({ _id: doc._id }, function(error, doc) {
- assert.ifError(error);
- assert.ok(!doc.ts.createdAt);
- assert.ok(doc.ts.updatedAt);
- assert.ok(doc.ts.updatedAt.valueOf() >= startTime);
- done();
- });
- });
+ let doc = await M.create({ name: 'Test' });
+ doc = await M.findOne({ _id: doc._id });
+ assert.ok(!doc.ts.createdAt);
+ assert.ok(doc.ts.updatedAt);
+ assert.ok(doc.ts.updatedAt.valueOf() >= startTime);
});
- it('does not override timestamps in nested schema (gh-4868)', function(done) {
+ it('does not override timestamps in nested schema (gh-4868)', async function() {
const startTime = Date.now();
const tsSchema = new mongoose.Schema({
createdAt: {
@@ -271,33 +248,25 @@ describe('timestamps', function() {
}, { timestamps: { createdAt: 'ts.createdAt', updatedAt: 'ts.updatedAt' } });
const M = db.model('Test', schema);
- M.create({ name: 'Test' }, function(error, doc) {
- assert.ifError(error);
- M.findOne({ _id: doc._id }, function(error, doc) {
- assert.ifError(error);
- assert.ok(!doc.ts.createdAt);
- assert.ok(doc.ts.updatedAt);
- assert.ok(doc.ts.updatedAt.valueOf() >= startTime);
- done();
- });
- });
+ let doc = await M.create({ name: 'Test' });
+ doc = await M.findOne({ _id: doc._id });
+ assert.ok(!doc.ts.createdAt);
+ assert.ok(doc.ts.updatedAt);
+ assert.ok(doc.ts.updatedAt.valueOf() >= startTime);
});
- it('no timestamps added when parent/child timestamps explicitly false (gh-7202)', function(done) {
+ it('no timestamps added when parent/child timestamps explicitly false (gh-7202)', async function() {
const subSchema = new Schema({}, { timestamps: false });
const schema = new Schema({ sub: subSchema }, { timestamps: false });
const Test = db.model('Test', schema);
const test = new Test({ sub: {} });
- test.save((err, saved) => {
- assert.ifError(err);
- assert.strictEqual(saved.createdAt, undefined);
- assert.strictEqual(saved.updatedAt, undefined);
- assert.strictEqual(saved.sub.createdAt, undefined);
- assert.strictEqual(saved.sub.updatedAt, undefined);
- done();
- });
+ const saved = await test.save();
+ assert.strictEqual(saved.createdAt, undefined);
+ assert.strictEqual(saved.updatedAt, undefined);
+ assert.strictEqual(saved.sub.createdAt, undefined);
+ assert.strictEqual(saved.sub.updatedAt, undefined);
});
it('avoids calling createdAt getters when setting updatedAt (gh-7496)', function() {
@@ -506,46 +475,42 @@ describe('timestamps', function() {
return Model.create({ set: 'test' }).then(doc => assert.ok(doc.createdAt));
});
- it('should not override createdAt when not selected (gh-4340)', function(done) {
+ it('should not override createdAt when not selected (gh-4340)', async function() {
const TestSchema = new Schema({ name: String }, { timestamps: true });
const Test = db.model('Test', TestSchema);
- Test.create({
+ let doc = await Test.create({
name: 'hello'
- }, function(err, doc) {
- // Let’s save the dates to compare later.
- const createdAt = doc.createdAt;
- const updatedAt = doc.updatedAt;
+ });
+ // Let’s save the dates to compare later.
+ const createdAt = doc.createdAt;
+ const updatedAt = doc.updatedAt;
- assert.ok(doc.createdAt);
+ assert.ok(doc.createdAt);
- Test.findById(doc._id, { name: true }, function(err, doc) {
- // The dates shouldn’t be selected here.
- assert.ok(!doc.createdAt);
- assert.ok(!doc.updatedAt);
+ doc = await Test.findById(doc._id, { name: true });
+ // The dates shouldn’t be selected here.
+ assert.ok(!doc.createdAt);
+ assert.ok(!doc.updatedAt);
- doc.name = 'world';
+ doc.name = 'world';
- doc.save(function(err, doc) {
- // Let’s save the new updatedAt date as it should have changed.
- const newUpdatedAt = doc.updatedAt;
+ await doc.save();
+ // Let’s save the new updatedAt date as it should have changed.
+ const newUpdatedAt = doc.updatedAt;
- assert.ok(!doc.createdAt);
- assert.ok(doc.updatedAt);
+ assert.ok(!doc.createdAt);
+ assert.ok(doc.updatedAt);
+
+ doc = await Test.findById(doc._id);
+ // Let’s make sure that everything is working again by
+ // comparing the dates with the ones we saved.
+ assert.equal(doc.createdAt.valueOf(), createdAt.valueOf());
+ assert.notEqual(doc.updatedAt.valueOf(), updatedAt.valueOf());
+ assert.equal(doc.updatedAt.valueOf(), newUpdatedAt.valueOf());
- Test.findById(doc._id, function(err, doc) {
- // Let’s make sure that everything is working again by
- // comparing the dates with the ones we saved.
- assert.equal(doc.createdAt.valueOf(), createdAt.valueOf());
- assert.notEqual(doc.updatedAt.valueOf(), updatedAt.valueOf());
- assert.equal(doc.updatedAt.valueOf(), newUpdatedAt.valueOf());
- done();
- });
- });
- });
- });
});
describe('auto update createdAt and updatedAt when create/save/update document', function() {
@@ -561,55 +526,57 @@ describe('timestamps', function() {
return Cat.deleteMany({}).then(() => Cat.create({ name: 'newcat' }));
});
- it('should have fields when create', function(done) {
+ it('should have fields when create', async function() {
const cat = new Cat({ name: 'newcat' });
- cat.save(function(err, doc) {
- assert.ok(doc.createdAt);
- assert.ok(doc.updatedAt);
- assert.ok(doc.createdAt.getTime() === doc.updatedAt.getTime());
- done();
- });
+ const doc = await cat.save();
+ assert.ok(doc.createdAt);
+ assert.ok(doc.updatedAt);
+ assert.ok(doc.createdAt.getTime() === doc.updatedAt.getTime());
});
- it('sets timestamps on findOneAndUpdate', function(done) {
- Cat.findOneAndUpdate({ name: 'notexistname' }, { $set: {} }, { upsert: true, new: true }, function(err, doc) {
- assert.ok(doc.createdAt);
- assert.ok(doc.updatedAt);
- assert.ok(doc.createdAt.getTime() === doc.updatedAt.getTime());
- done();
- });
+ it('sets timestamps on findOneAndUpdate', async function() {
+ const doc = await Cat.findOneAndUpdate({ name: 'notexistname' }, { $set: {} }, { upsert: true, new: true });
+ assert.ok(doc.createdAt);
+ assert.ok(doc.updatedAt);
+ assert.ok(doc.createdAt.getTime() === doc.updatedAt.getTime());
});
- it('sets timestamps on findOneAndReplace (gh-9951)', function() {
- return Cat.findOneAndReplace({ name: 'notexistname' }, {}, { upsert: true, new: true }).then(doc => {
- assert.ok(doc.createdAt);
- assert.ok(doc.updatedAt);
- assert.ok(doc.createdAt.getTime() === doc.updatedAt.getTime());
- });
+ it('sets timestamps on findOneAndReplace (gh-9951)', async function() {
+ const doc = await Cat.findOneAndReplace({ name: 'notexistname' }, {}, { upsert: true, new: true });
+ assert.ok(doc.createdAt);
+ assert.ok(doc.updatedAt);
+ assert.ok(doc.createdAt.getTime() === doc.updatedAt.getTime());
+ });
+
+ it('sets timestamps on replaceOne (gh-9951)', async function() {
+ await Cat.deleteMany({});
+ const { _id } = await Cat.create({ name: 'notexistname' });
+ await Cat.replaceOne({ name: 'notexistname' }, {});
+ const docs = await Cat.find({});
+ assert.equal(docs.length, 1);
+ const [doc] = docs;
+ assert.equal(doc._id.toHexString(), _id.toHexString());
+ assert.ok(doc.createdAt);
+ assert.ok(doc.updatedAt);
+ assert.ok(doc.createdAt.getTime() === doc.updatedAt.getTime());
});
- it('should change updatedAt when save', function(done) {
- Cat.findOne({ name: 'newcat' }, function(err, doc) {
- const old = doc.updatedAt;
+ it('should change updatedAt when save', async function() {
+ const doc = await Cat.findOne({ name: 'newcat' });
+ const old = doc.updatedAt;
- doc.hobby = 'coding';
+ doc.hobby = 'coding';
- doc.save(function(err, doc) {
- assert.ok(doc.updatedAt.getTime() > old.getTime());
- done();
- });
- });
+ await doc.save();
+ assert.ok(doc.updatedAt.getTime() > old.getTime());
});
- it('should not change updatedAt when save with no modifications', function(done) {
- Cat.findOne({ name: 'newcat' }, function(err, doc) {
- const old = doc.updatedAt;
+ it('should not change updatedAt when save with no modifications', async function() {
+ const doc = await Cat.findOne({ name: 'newcat' });
+ const old = doc.updatedAt;
- doc.save(function(err, doc) {
- assert.ok(doc.updatedAt.getTime() === old.getTime());
- done();
- });
- });
+ await doc.save();
+ assert.ok(doc.updatedAt.getTime() === old.getTime());
});
it('can skip with timestamps: false (gh-7357)', async function() {
@@ -640,17 +607,14 @@ describe('timestamps', function() {
assert.strictEqual(cat.updatedAt, old);
});
- it('should change updatedAt when findOneAndUpdate', function(done) {
- Cat.create({ name: 'test123' }, function(err) {
- assert.ifError(err);
- Cat.findOne({ name: 'test123' }, function(err, doc) {
- const old = doc.updatedAt;
- Cat.findOneAndUpdate({ name: 'test123' }, { $set: { hobby: 'fish' } }, { new: true }, function(err, doc) {
- assert.ok(doc.updatedAt.getTime() > old.getTime());
- done();
- });
- });
- });
+ it('should change updatedAt when findOneAndUpdate', async function() {
+ await Cat.deleteMany({});
+ await Cat.create({ name: 'test123' });
+ let doc = await Cat.findOne({ name: 'test123' });
+ const old = doc.updatedAt;
+ await new Promise(resolve => setTimeout(resolve, 10));
+ doc = await Cat.findOneAndUpdate({ name: 'test123' }, { $set: { hobby: 'fish' } }, { new: true });
+ assert.ok(doc.updatedAt.getTime() > old.getTime(), `Expected ${doc.updatedAt} > ${old}`);
});
it('insertMany with createdAt off (gh-6381)', async function() {
@@ -683,77 +647,59 @@ describe('timestamps', function() {
assert.equal(cats[1].createdAt.valueOf(), new Date('2011-06-01').valueOf());
});
- it('should have fields when update', function(done) {
- Cat.findOne({ name: 'newcat' }, function(err, doc) {
- const old = doc.updatedAt;
- Cat.update({ name: 'newcat' }, { $set: { hobby: 'fish' } }, function() {
- Cat.findOne({ name: 'newcat' }, function(err, doc) {
- assert.ok(doc.updatedAt.getTime() > old.getTime());
- done();
- });
- });
- });
+ it('should have fields when updateOne', async function() {
+ let doc = await Cat.findOne({ name: 'newcat' });
+ const old = doc.updatedAt;
+ await Cat.updateOne({ name: 'newcat' }, { $set: { hobby: 'fish' } });
+ doc = await Cat.findOne({ name: 'newcat' });
+ assert.ok(doc.updatedAt.getTime() > old.getTime());
+
});
- it('should change updatedAt when updateOne', function(done) {
- Cat.findOne({ name: 'newcat' }, function(err, doc) {
- const old = doc.updatedAt;
- Cat.updateOne({ name: 'newcat' }, { $set: { hobby: 'fish' } }, function() {
- Cat.findOne({ name: 'newcat' }, function(err, doc) {
- assert.ok(doc.updatedAt.getTime() > old.getTime());
- done();
- });
- });
- });
+ it('should change updatedAt when updateOne', async function() {
+ let doc = await Cat.findOne({ name: 'newcat' });
+ const old = doc.updatedAt;
+ await Cat.updateOne({ name: 'newcat' }, { $set: { hobby: 'fish' } });
+ doc = await Cat.findOne({ name: 'newcat' });
+ assert.ok(doc.updatedAt.getTime() > old.getTime());
+
});
- it('should change updatedAt when updateMany', function(done) {
- Cat.findOne({ name: 'newcat' }, function(err, doc) {
- const old = doc.updatedAt;
- Cat.updateMany({ name: 'newcat' }, { $set: { hobby: 'fish' } }, function() {
- Cat.findOne({ name: 'newcat' }, function(err, doc) {
- assert.ok(doc.updatedAt.getTime() > old.getTime());
- done();
- });
- });
- });
+ it('should change updatedAt when updateMany', async function() {
+ let doc = await Cat.findOne({ name: 'newcat' });
+ const old = doc.updatedAt;
+ await Cat.updateMany({ name: 'newcat' }, { $set: { hobby: 'fish' } });
+ doc = await Cat.findOne({ name: 'newcat' });
+ assert.ok(doc.updatedAt.getTime() > old.getTime());
+
});
- it('nested docs (gh-4049)', function(done) {
+ it('nested docs (gh-4049)', async function() {
const GroupSchema = new Schema({
cats: [CatSchema]
});
const Group = db.model('Test', GroupSchema);
const now = Date.now();
- Group.create({ cats: [{ name: 'Garfield' }] }, function(error, group) {
- assert.ifError(error);
- assert.ok(group.cats[0].createdAt);
- assert.ok(group.cats[0].createdAt.getTime() >= now);
- done();
- });
+ const group = await Group.create({ cats: [{ name: 'Garfield' }] });
+ assert.ok(group.cats[0].createdAt);
+ assert.ok(group.cats[0].createdAt.getTime() >= now);
});
- it('nested docs with push (gh-4049)', function(done) {
+ it('nested docs with push (gh-4049)', async function() {
const GroupSchema = new Schema({
cats: [CatSchema]
});
const Group = db.model('Test', GroupSchema);
const now = Date.now();
- Group.create({ cats: [{ name: 'Garfield' }] }, function(error, group) {
- assert.ifError(error);
- group.cats.push({ name: 'Keanu' });
- group.save(function(error) {
- assert.ifError(error);
- Group.findById(group._id, function(error, group) {
- assert.ifError(error);
- assert.ok(group.cats[1].createdAt);
- assert.ok(group.cats[1].createdAt.getTime() > now);
- done();
- });
- });
- });
+ let group = await Group.create({ cats: [{ name: 'Garfield' }] });
+ group.cats.push({ name: 'Keanu' });
+ await group.save();
+ group = await Group.findById(group._id);
+ assert.ok(group.cats[1].createdAt);
+ assert.ok(group.cats[1].createdAt.getTime() > now);
+
});
});
@@ -1064,6 +1010,39 @@ describe('timestamps', function() {
assert.deepStrictEqual(keys, ['location', '_id', 'createdAt', 'updatedAt']);
}
});
+ it('should avoid setting null update when updating document with timestamps gh-13379', async function() {
+
+ const subWithTimestampSchema = new Schema({
+ subName: {
+ type: String,
+ default: 'anonymous',
+ required: true
+ }
+ });
+
+ subWithTimestampSchema.set('timestamps', true);
+
+ const testSchema = new Schema({
+ name: String,
+ sub: { type: subWithTimestampSchema }
+ });
+
+ const Test = db.model('gh13379', testSchema);
+
+ const doc = new Test({
+ name: 'Test Testerson',
+ sub: { subName: 'John' }
+ });
+ await doc.save();
+ await Test.updateMany({}, [{ $set: { updateCounter: 1 } }]);
+ // oddly enough, the null property is not accessible. Doing check.null doesn't return anything even though
+ // if you were to console.log() the output of a findOne you would be able to see it. This is the workaround.
+ const test = await Test.countDocuments({ null: { $exists: true } });
+ assert.equal(test, 0);
+ // now we need to make sure that the solution didn't prevent the updateCounter addition
+ const check = await Test.findOne();
+ assert(check.toString().includes('updateCounter: 1'));
+ });
});
async function delay(ms) {
diff --git a/test/types.array.test.js b/test/types.array.test.js
index bf07fb58792..3aea341915c 100644
--- a/test/types.array.test.js
+++ b/test/types.array.test.js
@@ -90,7 +90,7 @@ describe('types array', function() {
});
describe('indexOf()', function() {
- it('works', function(done) {
+ it('works', async function() {
const User = db.model('User', UserSchema);
const Pet = db.model('Pet', PetSchema);
@@ -103,38 +103,21 @@ describe('types array', function() {
tj.pets.push(loki);
tj.pets.push(jane);
- let pending = 3;
-
- function cb() {
- Pet.find({}, function(err) {
- assert.ifError(err);
- tj.save(function(err) {
- assert.ifError(err);
- User.findOne({ name: 'tj' }, function(err, user) {
- assert.ifError(err);
- assert.equal(user.pets.length, 3);
- assert.equal(user.pets.indexOf(tobi.id), 0);
- assert.equal(user.pets.indexOf(loki.id), 1);
- assert.equal(user.pets.indexOf(jane.id), 2);
- assert.equal(user.pets.indexOf(tobi._id), 0);
- assert.equal(user.pets.indexOf(loki._id), 1);
- assert.equal(user.pets.indexOf(jane._id), 2);
- done();
- });
- });
- });
- }
-
- [tobi, loki, jane].forEach(function(pet) {
- pet.save(function() {
- --pending || cb();
- });
- });
+ await Pet.create([tobi, loki, jane]);
+ await tj.save();
+ const user = await User.findOne({ name: 'tj' });
+ assert.equal(user.pets.length, 3);
+ assert.equal(user.pets.indexOf(tobi.id), 0);
+ assert.equal(user.pets.indexOf(loki.id), 1);
+ assert.equal(user.pets.indexOf(jane.id), 2);
+ assert.equal(user.pets.indexOf(tobi._id), 0);
+ assert.equal(user.pets.indexOf(loki._id), 1);
+ assert.equal(user.pets.indexOf(jane._id), 2);
});
});
describe('includes()', function() {
- it('works', function(done) {
+ it('works', async function() {
const User = db.model('User', UserSchema);
const Pet = db.model('Pet', PetSchema);
@@ -147,195 +130,136 @@ describe('types array', function() {
tj.pets.push(loki);
tj.pets.push(jane);
- let pending = 3;
-
- function cb() {
- Pet.find({}, function(err) {
- assert.ifError(err);
- tj.save(function(err) {
- assert.ifError(err);
- User.findOne({ name: 'tj' }, function(err, user) {
- assert.ifError(err);
- assert.equal(user.pets.length, 3);
- assert.equal(user.pets.includes(tobi.id), true);
- assert.equal(user.pets.includes(loki.id), true);
- assert.equal(user.pets.includes(jane.id), true);
- assert.equal(user.pets.includes(tobi.id, 1), false);
- assert.equal(user.pets.includes(loki.id, 1), true);
- done();
- });
- });
- });
- }
-
- [tobi, loki, jane].forEach(function(pet) {
- pet.save(function() {
- --pending || cb();
- });
- });
+ await Pet.create([tobi, loki, jane]);
+ await tj.save();
+ const user = await User.findOne({ name: 'tj' });
+ assert.equal(user.pets.length, 3);
+ assert.equal(user.pets.includes(tobi.id), true);
+ assert.equal(user.pets.includes(loki.id), true);
+ assert.equal(user.pets.includes(jane.id), true);
+ assert.equal(user.pets.includes(tobi.id, 1), false);
+ assert.equal(user.pets.includes(loki.id, 1), true);
});
});
describe('push()', function() {
- function save(doc, cb) {
- doc.save(function(err) {
- if (err) {
- cb(err);
- return;
- }
- doc.constructor.findById(doc._id, cb);
- });
+ async function save(doc) {
+ await doc.save();
+ return doc.constructor.findById(doc._id);
}
- it('works with numbers', function(done) {
+ it('works with numbers', async function() {
const N = db.model('Test', Schema({ arr: [Number] }));
const m = new N({ arr: [3, 4, 5, 6] });
- save(m, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.arr.length, 4);
- doc.arr.push(8);
- assert.strictEqual(8, doc.arr[doc.arr.length - 1]);
- assert.strictEqual(8, doc.arr[4]);
-
- save(doc, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.arr.length, 5);
- assert.strictEqual(3, doc.arr[0]);
- assert.strictEqual(4, doc.arr[1]);
- assert.strictEqual(5, doc.arr[2]);
- assert.strictEqual(6, doc.arr[3]);
- assert.strictEqual(8, doc.arr[4]);
-
- done();
- });
- });
+ let doc = await save(m);
+ assert.equal(doc.arr.length, 4);
+ doc.arr.push(8);
+ assert.strictEqual(8, doc.arr[doc.arr.length - 1]);
+ assert.strictEqual(8, doc.arr[4]);
+
+ doc = await save(doc);
+ assert.equal(doc.arr.length, 5);
+ assert.strictEqual(3, doc.arr[0]);
+ assert.strictEqual(4, doc.arr[1]);
+ assert.strictEqual(5, doc.arr[2]);
+ assert.strictEqual(6, doc.arr[3]);
+ assert.strictEqual(8, doc.arr[4]);
});
- it('works with strings', function(done) {
+ it('works with strings', async function() {
const S = db.model('Test', Schema({ arr: [String] }));
const m = new S({ arr: [3, 4, 5, 6] });
- save(m, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.arr.length, 4);
- doc.arr.push(8);
- assert.strictEqual('8', doc.arr[doc.arr.length - 1]);
- assert.strictEqual('8', doc.arr[4]);
-
- save(doc, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.arr.length, 5);
- assert.strictEqual('3', doc.arr[0]);
- assert.strictEqual('4', doc.arr[1]);
- assert.strictEqual('5', doc.arr[2]);
- assert.strictEqual('6', doc.arr[3]);
- assert.strictEqual('8', doc.arr[4]);
-
- done();
- });
- });
+ let doc = await save(m);
+ assert.equal(doc.arr.length, 4);
+ doc.arr.push(8);
+ assert.strictEqual('8', doc.arr[doc.arr.length - 1]);
+ assert.strictEqual('8', doc.arr[4]);
+
+ doc = await save(doc);
+ assert.equal(doc.arr.length, 5);
+ assert.strictEqual('3', doc.arr[0]);
+ assert.strictEqual('4', doc.arr[1]);
+ assert.strictEqual('5', doc.arr[2]);
+ assert.strictEqual('6', doc.arr[3]);
+ assert.strictEqual('8', doc.arr[4]);
+
+
});
- it('works with buffers', function(done) {
+ it('works with buffers', async function() {
const B = db.model('Test', Schema({ arr: [Buffer] }));
const m = new B({ arr: [[0], Buffer.alloc(1)] });
- save(m, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.arr.length, 2);
- assert.ok(doc.arr[0].isMongooseBuffer);
- assert.ok(doc.arr[1].isMongooseBuffer);
- doc.arr.push('nice');
- assert.equal(doc.arr.length, 3);
- assert.ok(doc.arr[2].isMongooseBuffer);
- assert.strictEqual('nice', doc.arr[2].toString('utf8'));
-
- save(doc, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.arr.length, 3);
- assert.ok(doc.arr[0].isMongooseBuffer);
- assert.ok(doc.arr[1].isMongooseBuffer);
- assert.ok(doc.arr[2].isMongooseBuffer);
- assert.strictEqual('\u0000', doc.arr[0].toString());
- assert.strictEqual('nice', doc.arr[2].toString());
- done();
- });
- });
+ let doc = await save(m);
+ assert.equal(doc.arr.length, 2);
+ assert.ok(doc.arr[0].isMongooseBuffer);
+ assert.ok(doc.arr[1].isMongooseBuffer);
+ doc.arr.push('nice');
+ assert.equal(doc.arr.length, 3);
+ assert.ok(doc.arr[2].isMongooseBuffer);
+ assert.strictEqual('nice', doc.arr[2].toString('utf8'));
+
+ doc = await save(doc);
+ assert.equal(doc.arr.length, 3);
+ assert.ok(doc.arr[0].isMongooseBuffer);
+ assert.ok(doc.arr[1].isMongooseBuffer);
+ assert.ok(doc.arr[2].isMongooseBuffer);
+ assert.strictEqual('\u0000', doc.arr[0].toString());
+ assert.strictEqual('nice', doc.arr[2].toString());
+
});
- it('works with mixed', function(done) {
+ it('works with mixed', async function() {
const M = db.model('Test', Schema({ arr: [] }));
const m = new M({ arr: [3, { x: 1 }, 'yes', [5]] });
- save(m, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.arr.length, 4);
- doc.arr.push(null);
- assert.equal(doc.arr.length, 5);
- assert.strictEqual(null, doc.arr[4]);
-
- save(doc, function(err, doc) {
- assert.ifError(err);
-
- assert.equal(doc.arr.length, 5);
- assert.strictEqual(3, doc.arr[0]);
- assert.strictEqual(1, doc.arr[1].x);
- assert.strictEqual('yes', doc.arr[2]);
- assert.ok(Array.isArray(doc.arr[3]));
- assert.strictEqual(5, doc.arr[3][0]);
- assert.strictEqual(null, doc.arr[4]);
-
- doc.arr.push(Infinity);
- assert.equal(doc.arr.length, 6);
- assert.strictEqual(Infinity, doc.arr[5]);
-
- doc.arr.push(Buffer.alloc(0));
- assert.equal(doc.arr.length, 7);
- assert.strictEqual('', doc.arr[6].toString());
-
- save(doc, function(err, doc) {
- assert.ifError(err);
-
- assert.equal(doc.arr.length, 7);
- assert.strictEqual(3, doc.arr[0]);
- assert.strictEqual(1, doc.arr[1].x);
- assert.strictEqual('yes', doc.arr[2]);
- assert.ok(Array.isArray(doc.arr[3]));
- assert.strictEqual(5, doc.arr[3][0]);
- assert.strictEqual(null, doc.arr[4]);
- assert.strictEqual(Infinity, doc.arr[5]);
- assert.strictEqual('', doc.arr[6].toString());
-
- done();
- });
- });
- });
+ let doc = await save(m);
+ assert.equal(doc.arr.length, 4);
+ doc.arr.push(null);
+ assert.equal(doc.arr.length, 5);
+ assert.strictEqual(null, doc.arr[4]);
+
+ doc = await save(doc);
+
+ assert.equal(doc.arr.length, 5);
+ assert.strictEqual(3, doc.arr[0]);
+ assert.strictEqual(1, doc.arr[1].x);
+ assert.strictEqual('yes', doc.arr[2]);
+ assert.ok(Array.isArray(doc.arr[3]));
+ assert.strictEqual(5, doc.arr[3][0]);
+ assert.strictEqual(null, doc.arr[4]);
+
+ doc.arr.push(Infinity);
+ assert.equal(doc.arr.length, 6);
+ assert.strictEqual(Infinity, doc.arr[5]);
+
+ doc.arr.push(Buffer.alloc(0));
+ assert.equal(doc.arr.length, 7);
+ assert.strictEqual('', doc.arr[6].toString());
+
+ doc = await save(doc);
});
- it('works with sub-docs', function(done) {
+ it('works with sub-docs', async function() {
const D = db.model('Test', Schema({ arr: [{ name: String }] }));
const m = new D({ arr: [{ name: 'aaron' }, { name: 'moombahton ' }] });
- save(m, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.arr.length, 2);
- doc.arr.push({ name: 'Restrepo' });
- assert.equal(doc.arr.length, 3);
- assert.equal(doc.arr[2].name, 'Restrepo');
-
- save(doc, function(err, doc) {
- assert.ifError(err);
-
- // validate
- assert.equal(doc.arr.length, 3);
- assert.equal(doc.arr[0].name, 'aaron');
- assert.equal(doc.arr[1].name, 'moombahton ');
- assert.equal(doc.arr[2].name, 'Restrepo');
-
- done();
- });
- });
+ let doc = await save(m);
+ assert.equal(doc.arr.length, 2);
+ doc.arr.push({ name: 'Restrepo' });
+ assert.equal(doc.arr.length, 3);
+ assert.equal(doc.arr[2].name, 'Restrepo');
+
+ doc = await save(doc);
+
+ // validate
+ assert.equal(doc.arr.length, 3);
+ assert.equal(doc.arr[0].name, 'aaron');
+ assert.equal(doc.arr[1].name, 'moombahton ');
+ assert.equal(doc.arr[2].name, 'Restrepo');
+
});
- it('applies setters (gh-3032)', function(done) {
+ it('applies setters (gh-3032)', async function() {
const ST = db.model('Test', Schema({
arr: [{
type: String,
@@ -344,95 +268,68 @@ describe('types array', function() {
}));
const m = new ST({ arr: ['ONE', 'TWO'] });
- save(m, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.arr.length, 2);
- doc.arr.push('THREE');
- assert.strictEqual('one', doc.arr[0]);
- assert.strictEqual('two', doc.arr[1]);
- assert.strictEqual('three', doc.arr[2]);
-
- save(doc, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.arr.length, 3);
- assert.strictEqual('one', doc.arr[0]);
- assert.strictEqual('two', doc.arr[1]);
- assert.strictEqual('three', doc.arr[2]);
-
- done();
- });
- });
+ let doc = await save(m);
+ assert.equal(doc.arr.length, 2);
+ doc.arr.push('THREE');
+ assert.strictEqual('one', doc.arr[0]);
+ assert.strictEqual('two', doc.arr[1]);
+ assert.strictEqual('three', doc.arr[2]);
+
+ doc = await save(doc);
+ assert.equal(doc.arr.length, 3);
+ assert.strictEqual('one', doc.arr[0]);
+ assert.strictEqual('two', doc.arr[1]);
+ assert.strictEqual('three', doc.arr[2]);
+
});
});
describe('splice()', function() {
- it('works', function(done) {
+ it('works', async function() {
const schema = new Schema({ numbers: [Number] });
const A = db.model('Test', schema);
const a = new A({ numbers: [4, 5, 6, 7] });
- a.save(function(err) {
- assert.ifError(err);
- A.findById(a._id, function(err, doc) {
- assert.ifError(err);
- const removed = doc.numbers.splice(1, 1, '10');
- assert.deepEqual(removed, [5]);
- assert.equal(typeof doc.numbers[1], 'number');
- assert.deepStrictEqual(doc.numbers.toObject(), [4, 10, 6, 7]);
- doc.save(function(err) {
- assert.ifError(err);
- A.findById(a._id, function(err, doc) {
- assert.ifError(err);
- assert.deepStrictEqual(doc.numbers.toObject(), [4, 10, 6, 7]);
-
- A.collection.drop(function(err) {
- assert.ifError(err);
- done();
- });
- });
- });
- });
- });
+ await a.save();
+ let doc = await A.findById(a._id);
+ const removed = doc.numbers.splice(1, 1, '10');
+ assert.deepEqual(removed, [5]);
+ assert.equal(typeof doc.numbers[1], 'number');
+ assert.deepStrictEqual(doc.numbers.toObject(), [4, 10, 6, 7]);
+ await doc.save();
+ doc = await A.findById(a._id);
+ assert.deepStrictEqual(doc.numbers.toObject(), [4, 10, 6, 7]);
});
- it('on embedded docs', function(done) {
+ it('on embedded docs', async function() {
const schema = new Schema({ types: [new Schema({ type: String })] });
const A = db.model('Test', schema);
const a = new A({ types: [{ type: 'bird' }, { type: 'boy' }, { type: 'frog' }, { type: 'cloud' }] });
- a.save(function(err) {
- assert.ifError(err);
- A.findById(a._id, function(err, doc) {
- assert.ifError(err);
-
- doc.types.$pop();
-
- const removed = doc.types.splice(1, 1);
- assert.equal(removed.length, 1);
- assert.equal(removed[0].type, 'boy');
-
- const obj = doc.types.toObject();
- assert.equal(obj[0].type, 'bird');
- assert.equal(obj[1].type, 'frog');
-
- doc.save(function(err) {
- assert.ifError(err);
- A.findById(a._id, function(err, doc) {
- assert.ifError(err);
-
- const obj = doc.types.toObject();
- assert.equal(obj[0].type, 'bird');
- assert.equal(obj[1].type, 'frog');
- done();
- });
- });
- });
- });
+ await a.save();
+ let doc = await A.findById(a._id);
+
+ doc.types.$pop();
+
+ const removed = doc.types.splice(1, 1);
+ assert.equal(removed.length, 1);
+ assert.equal(removed[0].type, 'boy');
+
+ let obj = doc.types.toObject();
+ assert.equal(obj[0].type, 'bird');
+ assert.equal(obj[1].type, 'frog');
+
+ await doc.save();
+ doc = await A.findById(a._id);
+
+ obj = doc.types.toObject();
+ assert.equal(obj[0].type, 'bird');
+ assert.equal(obj[1].type, 'frog');
});
});
describe('unshift()', function() {
- it('works', function(done) {
+ it('works', async function() {
const schema = new Schema({
types: [new Schema({ type: String })],
nums: [Number],
@@ -446,72 +343,64 @@ describe('types array', function() {
strs: 'one two three'.split(' ')
});
- a.save(function(err) {
- assert.ifError(err);
- A.findById(a._id, function(err, doc) {
- assert.ifError(err);
-
- const tlen = doc.types.unshift({ type: 'tree' });
- const nlen = doc.nums.unshift(0);
- const slen = doc.strs.unshift('zero');
-
- assert.equal(tlen, 5);
- assert.equal(nlen, 4);
- assert.equal(slen, 4);
-
- doc.types.push({ type: 'worm' });
- let obj = doc.types.toObject();
- assert.equal(obj[0].type, 'tree');
- assert.equal(obj[1].type, 'bird');
- assert.equal(obj[2].type, 'boy');
- assert.equal(obj[3].type, 'frog');
- assert.equal(obj[4].type, 'cloud');
- assert.equal(obj[5].type, 'worm');
-
- obj = doc.nums.toObject();
- assert.equal(obj[0].valueOf(), 0);
- assert.equal(obj[1].valueOf(), 1);
- assert.equal(obj[2].valueOf(), 2);
- assert.equal(obj[3].valueOf(), 3);
-
- obj = doc.strs.toObject();
- assert.equal(obj[0], 'zero');
- assert.equal(obj[1], 'one');
- assert.equal(obj[2], 'two');
- assert.equal(obj[3], 'three');
-
- doc.save(function(err) {
- assert.ifError(err);
- A.findById(a._id, function(err, doc) {
- assert.ifError(err);
-
- let obj = doc.types.toObject();
- assert.equal(obj[0].type, 'tree');
- assert.equal(obj[1].type, 'bird');
- assert.equal(obj[2].type, 'boy');
- assert.equal(obj[3].type, 'frog');
- assert.equal(obj[4].type, 'cloud');
- assert.equal(obj[5].type, 'worm');
-
- obj = doc.nums.toObject();
- assert.equal(obj[0].valueOf(), 0);
- assert.equal(obj[1].valueOf(), 1);
- assert.equal(obj[2].valueOf(), 2);
- assert.equal(obj[3].valueOf(), 3);
-
- obj = doc.strs.toObject();
- assert.equal(obj[0], 'zero');
- assert.equal(obj[1], 'one');
- assert.equal(obj[2], 'two');
- assert.equal(obj[3], 'three');
- done();
- });
- });
- });
- });
+ await a.save();
+ let doc = await A.findById(a._id);
+
+ const tlen = doc.types.unshift({ type: 'tree' });
+ const nlen = doc.nums.unshift(0);
+ const slen = doc.strs.unshift('zero');
+
+ assert.equal(tlen, 5);
+ assert.equal(nlen, 4);
+ assert.equal(slen, 4);
+
+ doc.types.push({ type: 'worm' });
+ let obj = doc.types.toObject();
+ assert.equal(obj[0].type, 'tree');
+ assert.equal(obj[1].type, 'bird');
+ assert.equal(obj[2].type, 'boy');
+ assert.equal(obj[3].type, 'frog');
+ assert.equal(obj[4].type, 'cloud');
+ assert.equal(obj[5].type, 'worm');
+
+ obj = doc.nums.toObject();
+ assert.equal(obj[0].valueOf(), 0);
+ assert.equal(obj[1].valueOf(), 1);
+ assert.equal(obj[2].valueOf(), 2);
+ assert.equal(obj[3].valueOf(), 3);
+
+ obj = doc.strs.toObject();
+ assert.equal(obj[0], 'zero');
+ assert.equal(obj[1], 'one');
+ assert.equal(obj[2], 'two');
+ assert.equal(obj[3], 'three');
+
+ await doc.save();
+ doc = await A.findById(a._id);
+
+ obj = doc.types.toObject();
+ assert.equal(obj[0].type, 'tree');
+ assert.equal(obj[1].type, 'bird');
+ assert.equal(obj[2].type, 'boy');
+ assert.equal(obj[3].type, 'frog');
+ assert.equal(obj[4].type, 'cloud');
+ assert.equal(obj[5].type, 'worm');
+
+ obj = doc.nums.toObject();
+ assert.equal(obj[0].valueOf(), 0);
+ assert.equal(obj[1].valueOf(), 1);
+ assert.equal(obj[2].valueOf(), 2);
+ assert.equal(obj[3].valueOf(), 3);
+
+ obj = doc.strs.toObject();
+ assert.equal(obj[0], 'zero');
+ assert.equal(obj[1], 'one');
+ assert.equal(obj[2], 'two');
+ assert.equal(obj[3], 'three');
+
});
- it('applies setters (gh-3032)', function(done) {
+ it('applies setters (gh-3032)', async function() {
const ST = db.model('Test', Schema({
arr: [{
type: String,
@@ -519,29 +408,25 @@ describe('types array', function() {
}]
}));
const m = new ST({ arr: ['ONE', 'TWO'] });
- m.save(function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.arr.length, 2);
- doc.arr.unshift('THREE');
- assert.strictEqual('three', doc.arr[0]);
- assert.strictEqual('one', doc.arr[1]);
- assert.strictEqual('two', doc.arr[2]);
-
- doc.save(function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.arr.length, 3);
- assert.strictEqual('three', doc.arr[0]);
- assert.strictEqual('one', doc.arr[1]);
- assert.strictEqual('two', doc.arr[2]);
-
- done();
- });
- });
+ const doc = await m.save();
+ assert.equal(doc.arr.length, 2);
+ doc.arr.unshift('THREE');
+ assert.strictEqual('three', doc.arr[0]);
+ assert.strictEqual('one', doc.arr[1]);
+ assert.strictEqual('two', doc.arr[2]);
+
+ await doc.save();
+ assert.equal(doc.arr.length, 3);
+ assert.strictEqual('three', doc.arr[0]);
+ assert.strictEqual('one', doc.arr[1]);
+ assert.strictEqual('two', doc.arr[2]);
+
+
});
});
describe('shift()', function() {
- it('works', function(done) {
+ it('works', async function() {
const schema = new Schema({
types: [new Schema({ type: String })],
nums: [Number],
@@ -556,102 +441,83 @@ describe('types array', function() {
strs: 'one two three'.split(' ')
});
- a.save(function(err) {
- assert.ifError(err);
- A.findById(a._id, function(err, doc) {
- assert.ifError(err);
-
- const t = doc.types.shift();
- const n = doc.nums.shift();
- const s = doc.strs.shift();
-
- assert.equal(t.type, 'bird');
- assert.equal(n, 1);
- assert.equal(s, 'one');
-
- let obj = doc.types.toObject();
- assert.equal(obj[0].type, 'boy');
- assert.equal(obj[1].type, 'frog');
- assert.equal(obj[2].type, 'cloud');
-
- doc.nums.push(4);
- obj = doc.nums.toObject();
- assert.equal(obj[0].valueOf(), 2);
- assert.equal(obj[1].valueOf(), 3);
- assert.equal(obj[2].valueOf(), 4);
-
- obj = doc.strs.toObject();
- assert.equal(obj[0], 'two');
- assert.equal(obj[1], 'three');
-
- doc.save(function(err) {
- assert.ifError(err);
- A.findById(a._id, function(err, doc) {
- assert.ifError(err);
-
- let obj = doc.types.toObject();
- assert.equal(obj[0].type, 'boy');
- assert.equal(obj[1].type, 'frog');
- assert.equal(obj[2].type, 'cloud');
-
- obj = doc.nums.toObject();
- assert.equal(obj[0].valueOf(), 2);
- assert.equal(obj[1].valueOf(), 3);
- assert.equal(obj[2].valueOf(), 4);
-
- obj = doc.strs.toObject();
- assert.equal(obj[0], 'two');
- assert.equal(obj[1], 'three');
- done();
- });
- });
- });
- });
+ await a.save();
+ let doc = await A.findById(a._id);
+
+ const t = doc.types.shift();
+ const n = doc.nums.shift();
+ const s = doc.strs.shift();
+
+ assert.equal(t.type, 'bird');
+ assert.equal(n, 1);
+ assert.equal(s, 'one');
+
+ let obj = doc.types.toObject();
+ assert.equal(obj[0].type, 'boy');
+ assert.equal(obj[1].type, 'frog');
+ assert.equal(obj[2].type, 'cloud');
+
+ doc.nums.push(4);
+ obj = doc.nums.toObject();
+ assert.equal(obj[0].valueOf(), 2);
+ assert.equal(obj[1].valueOf(), 3);
+ assert.equal(obj[2].valueOf(), 4);
+
+ obj = doc.strs.toObject();
+ assert.equal(obj[0], 'two');
+ assert.equal(obj[1], 'three');
+
+ await doc.save();
+ doc = await A.findById(a._id);
+
+ obj = doc.types.toObject();
+ assert.equal(obj[0].type, 'boy');
+ assert.equal(obj[1].type, 'frog');
+ assert.equal(obj[2].type, 'cloud');
+
+ obj = doc.nums.toObject();
+ assert.equal(obj[0].valueOf(), 2);
+ assert.equal(obj[1].valueOf(), 3);
+ assert.equal(obj[2].valueOf(), 4);
+
+ obj = doc.strs.toObject();
+ assert.equal(obj[0], 'two');
+ assert.equal(obj[1], 'three');
+
});
});
describe('$shift', function() {
- it('works', function(done) {
+ it('works', async function() {
// atomic shift uses $pop -1
const painting = new Schema({ colors: [] });
const Painting = db.model('Test', painting);
const p = new Painting({ colors: ['blue', 'green', 'yellow'] });
- p.save(function(err) {
- assert.ifError(err);
-
- Painting.findById(p, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.colors.length, 3);
- let color = doc.colors.$shift();
- assert.equal(doc.colors.length, 2);
- assert.equal(color, 'blue');
- // MongoDB pop command can only be called once per save, each
- // time only removing one element.
- color = doc.colors.$shift();
- assert.equal(color, undefined);
- assert.equal(doc.colors.length, 2);
- doc.save(function(err) {
- assert.equal(err, null);
- const color = doc.colors.$shift();
- assert.equal(doc.colors.length, 1);
- assert.equal(color, 'green');
- doc.save(function(err) {
- assert.equal(err, null);
- Painting.findById(doc, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.colors.length, 1);
- assert.equal(doc.colors[0], 'yellow');
- done();
- });
- });
- });
- });
- });
+ await p.save();
+
+ let doc = await Painting.findById(p);
+ assert.equal(doc.colors.length, 3);
+ let color = doc.colors.$shift();
+ assert.equal(doc.colors.length, 2);
+ assert.equal(color, 'blue');
+ // MongoDB pop command can only be called once per save, each
+ // time only removing one element.
+ color = doc.colors.$shift();
+ assert.equal(color, undefined);
+ assert.equal(doc.colors.length, 2);
+ await doc.save();
+ color = doc.colors.$shift();
+ assert.equal(doc.colors.length, 1);
+ assert.equal(color, 'green');
+ await doc.save();
+ doc = await Painting.findById(doc);
+ assert.equal(doc.colors.length, 1);
+ assert.equal(doc.colors[0], 'yellow');
});
});
describe('pop()', function() {
- it('works', function(done) {
+ it('works', async function() {
const schema = new Schema({
types: [new Schema({ type: String })],
nums: [Number],
@@ -666,62 +532,54 @@ describe('types array', function() {
strs: 'one two three'.split(' ')
});
- a.save(function(err) {
- assert.ifError(err);
- A.findById(a._id, function(err, doc) {
- assert.ifError(err);
-
- const t = doc.types.pop();
- const n = doc.nums.pop();
- const s = doc.strs.pop();
-
- assert.equal(t.type, 'cloud');
- assert.equal(n, 3);
- assert.equal(s, 'three');
-
- let obj = doc.types.toObject();
- assert.equal(obj[0].type, 'bird');
- assert.equal(obj[1].type, 'boy');
- assert.equal(obj[2].type, 'frog');
-
- doc.nums.push(4);
- obj = doc.nums.toObject();
- assert.equal(obj[0].valueOf(), 1);
- assert.equal(obj[1].valueOf(), 2);
- assert.equal(obj[2].valueOf(), 4);
-
- obj = doc.strs.toObject();
- assert.equal(obj[0], 'one');
- assert.equal(obj[1], 'two');
-
- doc.save(function(err) {
- assert.ifError(err);
- A.findById(a._id, function(err, doc) {
- assert.ifError(err);
-
- let obj = doc.types.toObject();
- assert.equal(obj[0].type, 'bird');
- assert.equal(obj[1].type, 'boy');
- assert.equal(obj[2].type, 'frog');
-
- obj = doc.nums.toObject();
- assert.equal(obj[0].valueOf(), 1);
- assert.equal(obj[1].valueOf(), 2);
- assert.equal(obj[2].valueOf(), 4);
-
- obj = doc.strs.toObject();
- assert.equal(obj[0], 'one');
- assert.equal(obj[1], 'two');
- done();
- });
- });
- });
- });
+ await a.save();
+ let doc = await A.findById(a._id);
+
+ const t = doc.types.pop();
+ const n = doc.nums.pop();
+ const s = doc.strs.pop();
+
+ assert.equal(t.type, 'cloud');
+ assert.equal(n, 3);
+ assert.equal(s, 'three');
+
+ let obj = doc.types.toObject();
+ assert.equal(obj[0].type, 'bird');
+ assert.equal(obj[1].type, 'boy');
+ assert.equal(obj[2].type, 'frog');
+
+ doc.nums.push(4);
+ obj = doc.nums.toObject();
+ assert.equal(obj[0].valueOf(), 1);
+ assert.equal(obj[1].valueOf(), 2);
+ assert.equal(obj[2].valueOf(), 4);
+
+ obj = doc.strs.toObject();
+ assert.equal(obj[0], 'one');
+ assert.equal(obj[1], 'two');
+
+ await doc.save();
+ doc = await A.findById(a._id);
+
+ obj = doc.types.toObject();
+ assert.equal(obj[0].type, 'bird');
+ assert.equal(obj[1].type, 'boy');
+ assert.equal(obj[2].type, 'frog');
+
+ obj = doc.nums.toObject();
+ assert.equal(obj[0].valueOf(), 1);
+ assert.equal(obj[1].valueOf(), 2);
+ assert.equal(obj[2].valueOf(), 4);
+
+ obj = doc.strs.toObject();
+ assert.equal(obj[0], 'one');
+ assert.equal(obj[1], 'two');
+
});
});
describe('pull()', function() {
- it('works', function(done) {
+ it('works', async function() {
const catschema = new Schema({ name: String });
const Cat = db.model('Cat', catschema);
const schema = new Schema({
@@ -729,25 +587,40 @@ describe('types array', function() {
});
const A = db.model('Test', schema);
const cat = new Cat({ name: 'peanut' });
- cat.save(function(err) {
- assert.ifError(err);
-
- const a = new A({ a: [cat._id] });
- a.save(function(err) {
- assert.ifError(err);
-
- A.findById(a, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.a.length, 1);
- doc.a.pull(cat.id);
- assert.equal(doc.a.length, 0);
- done();
- });
- });
+ await cat.save();
+
+ const a = new A({ a: [cat._id] });
+ await a.save();
+
+ const doc = await A.findById(a);
+ assert.equal(doc.a.length, 1);
+ doc.a.pull(cat.id);
+ assert.equal(doc.a.length, 0);
+
+ assert.ok(doc.getChanges().$pullAll.a);
+ });
+
+ it('registers $pull atomic if pulling from middle (gh-14502)', async function() {
+ const schema = new Schema({
+ a: [{ type: Schema.ObjectId, ref: 'Cat' }]
});
+ const A = db.model('Test', schema);
+
+ const oid1 = new mongoose.Types.ObjectId();
+ const oid2 = new mongoose.Types.ObjectId();
+ const oid3 = new mongoose.Types.ObjectId();
+ const a = new A({ a: [oid1, oid2, oid3] });
+ await a.save();
+
+ const doc = await A.findById(a);
+ assert.equal(doc.a.length, 3);
+ doc.a.pull(oid2);
+ assert.equal(doc.a.length, 2);
+
+ assert.ok(doc.getChanges().$pullAll.a);
});
- it('handles pulling with no _id (gh-3341)', function(done) {
+ it('handles pulling with no _id (gh-3341)', async function() {
const personSchema = new Schema({
name: String,
role: String
@@ -759,7 +632,7 @@ describe('types array', function() {
const Band = db.model('Test', bandSchema);
- const gnr = new Band({
+ let gnr = new Band({
name: 'Guns N\' Roses',
members: [
{ name: 'Axl', role: 'Lead Singer' },
@@ -770,30 +643,24 @@ describe('types array', function() {
]
});
- gnr.save(function(error) {
- assert.ifError(error);
- gnr.members.pull({ name: 'Slash', role: 'Guitar' });
- gnr.save(function(error) {
- assert.ifError(error);
- assert.equal(gnr.members.length, 4);
- assert.equal(gnr.members[0].name, 'Axl');
- assert.equal(gnr.members[1].name, 'Izzy');
- assert.equal(gnr.members[2].name, 'Duff');
- assert.equal(gnr.members[3].name, 'Adler');
- Band.findById(gnr._id, function(error, gnr) {
- assert.ifError(error);
- assert.equal(gnr.members.length, 4);
- assert.equal(gnr.members[0].name, 'Axl');
- assert.equal(gnr.members[1].name, 'Izzy');
- assert.equal(gnr.members[2].name, 'Duff');
- assert.equal(gnr.members[3].name, 'Adler');
- done();
- });
- });
- });
+ await gnr.save();
+ gnr.members.pull({ name: 'Slash', role: 'Guitar' });
+ await gnr.save();
+ assert.equal(gnr.members.length, 4);
+ assert.equal(gnr.members[0].name, 'Axl');
+ assert.equal(gnr.members[1].name, 'Izzy');
+ assert.equal(gnr.members[2].name, 'Duff');
+ assert.equal(gnr.members[3].name, 'Adler');
+ gnr = await Band.findById(gnr._id);
+ assert.equal(gnr.members.length, 4);
+ assert.equal(gnr.members[0].name, 'Axl');
+ assert.equal(gnr.members[1].name, 'Izzy');
+ assert.equal(gnr.members[2].name, 'Duff');
+ assert.equal(gnr.members[3].name, 'Adler');
+
});
- it('properly works with undefined', function(done) {
+ it('properly works with undefined', async function() {
const catschema = new Schema({ name: String, colors: [{ hex: String }] });
const Cat = db.model('Test', catschema);
@@ -801,26 +668,19 @@ describe('types array', function() {
{ hex: '#FFF' }, { hex: '#000' }, null
] });
- cat.save(function(err) {
- assert.ifError(err);
+ await cat.save();
- cat.colors.pull(undefined); // converted to null (as mongodb does)
- assert.equal(cat.colors.length, 2);
- assert.equal(cat.colors[0].hex, '#FFF');
- assert.equal(cat.colors[1].hex, '#000');
+ cat.colors.pull(undefined); // converted to null (as mongodb does)
+ assert.equal(cat.colors.length, 2);
+ assert.equal(cat.colors[0].hex, '#FFF');
+ assert.equal(cat.colors[1].hex, '#000');
- cat.save(function(err) {
- assert.ifError(err);
+ await cat.save();
- Cat.findById(cat._id, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.colors.length, 2);
- assert.equal(doc.colors[0].hex, '#FFF');
- assert.equal(doc.colors[1].hex, '#000');
- done();
- });
- });
- });
+ const doc = await Cat.findById(cat._id);
+ assert.equal(doc.colors.length, 2);
+ assert.equal(doc.colors[0].hex, '#FFF');
+ assert.equal(doc.colors[1].hex, '#000');
});
it('avoids adding default paths to query filter (gh-12294)', async function() {
@@ -875,47 +735,35 @@ describe('types array', function() {
});
describe('$pop()', function() {
- it('works', function(done) {
+ it('works', async function() {
const painting = new Schema({ colors: [] });
const Painting = db.model('Test', painting);
const p = new Painting({ colors: ['blue', 'green', 'yellow'] });
- p.save(function(err) {
- assert.ifError(err);
-
- Painting.findById(p, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.colors.length, 3);
- let color = doc.colors.$pop();
- assert.equal(doc.colors.length, 2);
- assert.equal(color, 'yellow');
- // MongoDB pop command can only be called once per save, each
- // time only removing one element.
- color = doc.colors.$pop();
- assert.equal(color, undefined);
- assert.equal(doc.colors.length, 2);
- assert.ok(!('$set' in doc.colors.$atomics()), 'invalid $atomic op used');
- doc.save(function(err) {
- assert.equal(err, null);
- const color = doc.colors.$pop();
- assert.equal(doc.colors.length, 1);
- assert.equal(color, 'green');
- doc.save(function(err) {
- assert.equal(err, null);
- Painting.findById(doc, function(err, doc) {
- assert.strictEqual(null, err);
- assert.equal(doc.colors.length, 1);
- assert.equal(doc.colors[0], 'blue');
- done();
- });
- });
- });
- });
- });
+ await p.save();
+ let doc = await Painting.findById(p);
+ assert.equal(doc.colors.length, 3);
+ let color = doc.colors.$pop();
+ assert.equal(doc.colors.length, 2);
+ assert.equal(color, 'yellow');
+ // MongoDB pop command can only be called once per save, each
+ // time only removing one element.
+ color = doc.colors.$pop();
+ assert.equal(color, undefined);
+ assert.equal(doc.colors.length, 2);
+ assert.ok(!('$set' in doc.colors.$atomics()), 'invalid $atomic op used');
+ await doc.save();
+ color = doc.colors.$pop();
+ assert.equal(doc.colors.length, 1);
+ assert.equal(color, 'green');
+ await doc.save();
+ doc = await Painting.findById(doc);
+ assert.equal(doc.colors.length, 1);
+ assert.equal(doc.colors[0], 'blue');
});
});
describe('addToSet()', function() {
- it('works', function(done) {
+ it('works', async function() {
const e = new Schema({ name: String, arr: [] });
const schema = new Schema({
num: [Number],
@@ -926,7 +774,7 @@ describe('types array', function() {
});
const M = db.model('Test', schema);
- const m = new M();
+ let m = new M();
m.num.push(1, 2, 3);
m.str.push('one', 'two', 'tres');
@@ -965,193 +813,180 @@ describe('types array', function() {
m.date.addToSet(d3);
assert.equal(m.date.length, 3);
- m.save(function(err) {
- assert.ifError(err);
- M.findById(m, function(err, m) {
- assert.ifError(err);
-
- assert.equal(m.num.length, 5);
- assert.ok(~m.num.indexOf(1));
- assert.ok(~m.num.indexOf(2));
- assert.ok(~m.num.indexOf(3));
- assert.ok(~m.num.indexOf(4));
- assert.ok(~m.num.indexOf(5));
-
- assert.equal(m.str.length, 5);
- assert.ok(~m.str.indexOf('one'));
- assert.ok(~m.str.indexOf('two'));
- assert.ok(~m.str.indexOf('tres'));
- assert.ok(~m.str.indexOf('four'));
- assert.ok(~m.str.indexOf('five'));
-
- assert.equal(m.id.length, 3);
- assert.ok(~m.id.indexOf(id1));
- assert.ok(~m.id.indexOf(id2));
- assert.ok(~m.id.indexOf(id3));
-
- assert.equal(m.date.length, 3);
- assert.ok(~m.date.indexOf(d1.toString()));
- assert.ok(~m.date.indexOf(d2.toString()));
- assert.ok(~m.date.indexOf(d3.toString()));
-
- assert.equal(m.doc.length, 3);
- assert.ok(m.doc.some(function(v) {
- return v.name === 'Waltz';
- }));
- assert.ok(m.doc.some(function(v) {
- return v.name === 'Dubstep';
- }));
- assert.ok(m.doc.some(function(v) {
- return v.name === 'Polka';
- }));
-
- // test single $addToSet
- m.num.addToSet(3, 4, 5, 6);
- assert.equal(m.num.length, 6);
- m.str.addToSet('four', 'five', 'two', 'six');
- assert.equal(m.str.length, 6);
- m.id.addToSet(id2, id3, id4);
- assert.equal(m.id.length, 4);
-
- m.date.addToSet(d1, d3, d4);
- assert.equal(m.date.length, 4);
-
- m.doc.addToSet(m.doc[0], { name: '8bit' });
- assert.equal(m.doc.length, 4);
-
- m.save(function(err) {
- assert.ifError(err);
-
- M.findById(m, function(err, m) {
- assert.ifError(err);
-
- assert.equal(m.num.length, 6);
- assert.ok(~m.num.indexOf(1));
- assert.ok(~m.num.indexOf(2));
- assert.ok(~m.num.indexOf(3));
- assert.ok(~m.num.indexOf(4));
- assert.ok(~m.num.indexOf(5));
- assert.ok(~m.num.indexOf(6));
-
- assert.equal(m.str.length, 6);
- assert.ok(~m.str.indexOf('one'));
- assert.ok(~m.str.indexOf('two'));
- assert.ok(~m.str.indexOf('tres'));
- assert.ok(~m.str.indexOf('four'));
- assert.ok(~m.str.indexOf('five'));
- assert.ok(~m.str.indexOf('six'));
-
- assert.equal(m.id.length, 4);
- assert.ok(~m.id.indexOf(id1));
- assert.ok(~m.id.indexOf(id2));
- assert.ok(~m.id.indexOf(id3));
- assert.ok(~m.id.indexOf(id4));
-
- assert.equal(m.date.length, 4);
- assert.ok(~m.date.indexOf(d1.toString()));
- assert.ok(~m.date.indexOf(d2.toString()));
- assert.ok(~m.date.indexOf(d3.toString()));
- assert.ok(~m.date.indexOf(d4.toString()));
-
- assert.equal(m.doc.length, 4);
- assert.ok(m.doc.some(function(v) {
- return v.name === 'Waltz';
- }));
- assert.ok(m.doc.some(function(v) {
- return v.name === 'Dubstep';
- }));
- assert.ok(m.doc.some(function(v) {
- return v.name === 'Polka';
- }));
- assert.ok(m.doc.some(function(v) {
- return v.name === '8bit';
- }));
-
- // test multiple $addToSet
- m.num.addToSet(7, 8);
- assert.equal(m.num.length, 8);
- m.str.addToSet('seven', 'eight');
- assert.equal(m.str.length, 8);
- m.id.addToSet(id5, id6);
- assert.equal(m.id.length, 6);
-
- m.date.addToSet(d5, d6);
- assert.equal(m.date.length, 6);
-
- m.doc.addToSet(m.doc[1], { name: 'BigBeat' }, { name: 'Funk' });
- assert.equal(m.doc.length, 6);
-
- m.save(function(err) {
- assert.ifError(err);
-
- M.findById(m, function(err, m) {
- assert.ifError(err);
-
- assert.equal(m.num.length, 8);
- assert.ok(~m.num.indexOf(1));
- assert.ok(~m.num.indexOf(2));
- assert.ok(~m.num.indexOf(3));
- assert.ok(~m.num.indexOf(4));
- assert.ok(~m.num.indexOf(5));
- assert.ok(~m.num.indexOf(6));
- assert.ok(~m.num.indexOf(7));
- assert.ok(~m.num.indexOf(8));
-
- assert.equal(m.str.length, 8);
- assert.ok(~m.str.indexOf('one'));
- assert.ok(~m.str.indexOf('two'));
- assert.ok(~m.str.indexOf('tres'));
- assert.ok(~m.str.indexOf('four'));
- assert.ok(~m.str.indexOf('five'));
- assert.ok(~m.str.indexOf('six'));
- assert.ok(~m.str.indexOf('seven'));
- assert.ok(~m.str.indexOf('eight'));
-
- assert.equal(m.id.length, 6);
- assert.ok(~m.id.indexOf(id1));
- assert.ok(~m.id.indexOf(id2));
- assert.ok(~m.id.indexOf(id3));
- assert.ok(~m.id.indexOf(id4));
- assert.ok(~m.id.indexOf(id5));
- assert.ok(~m.id.indexOf(id6));
-
- assert.equal(m.date.length, 6);
- assert.ok(~m.date.indexOf(d1.toString()));
- assert.ok(~m.date.indexOf(d2.toString()));
- assert.ok(~m.date.indexOf(d3.toString()));
- assert.ok(~m.date.indexOf(d4.toString()));
- assert.ok(~m.date.indexOf(d5.toString()));
- assert.ok(~m.date.indexOf(d6.toString()));
-
- assert.equal(m.doc.length, 6);
- assert.ok(m.doc.some(function(v) {
- return v.name === 'Waltz';
- }));
- assert.ok(m.doc.some(function(v) {
- return v.name === 'Dubstep';
- }));
- assert.ok(m.doc.some(function(v) {
- return v.name === 'Polka';
- }));
- assert.ok(m.doc.some(function(v) {
- return v.name === '8bit';
- }));
- assert.ok(m.doc.some(function(v) {
- return v.name === 'BigBeat';
- }));
- assert.ok(m.doc.some(function(v) {
- return v.name === 'Funk';
- }));
- done();
- });
- });
- });
- });
- });
- });
+ await m.save();
+ m = await M.findById(m);
+
+ assert.equal(m.num.length, 5);
+ assert.ok(~m.num.indexOf(1));
+ assert.ok(~m.num.indexOf(2));
+ assert.ok(~m.num.indexOf(3));
+ assert.ok(~m.num.indexOf(4));
+ assert.ok(~m.num.indexOf(5));
+
+ assert.equal(m.str.length, 5);
+ assert.ok(~m.str.indexOf('one'));
+ assert.ok(~m.str.indexOf('two'));
+ assert.ok(~m.str.indexOf('tres'));
+ assert.ok(~m.str.indexOf('four'));
+ assert.ok(~m.str.indexOf('five'));
+
+ assert.equal(m.id.length, 3);
+ assert.ok(m.id.find(id => id.toString() === id1.toString()));
+ assert.ok(m.id.find(id => id.toString() === id2.toString()));
+ assert.ok(m.id.find(id => id.toString() === id3.toString()));
+
+ assert.equal(m.date.length, 3);
+ assert.ok(~m.date.indexOf(d1.toString()));
+ assert.ok(~m.date.indexOf(d2.toString()));
+ assert.ok(~m.date.indexOf(d3.toString()));
+
+ assert.equal(m.doc.length, 3);
+ assert.ok(m.doc.some(function(v) {
+ return v.name === 'Waltz';
+ }));
+ assert.ok(m.doc.some(function(v) {
+ return v.name === 'Dubstep';
+ }));
+ assert.ok(m.doc.some(function(v) {
+ return v.name === 'Polka';
+ }));
+
+ // test single $addToSet
+ m.num.addToSet(3, 4, 5, 6);
+ assert.equal(m.num.length, 6);
+ m.str.addToSet('four', 'five', 'two', 'six');
+ assert.equal(m.str.length, 6);
+ m.id.addToSet(id2, id3, id4);
+ assert.equal(m.id.length, 4);
+
+ m.date.addToSet(d1, d3, d4);
+ assert.equal(m.date.length, 4);
+
+ m.doc.addToSet(m.doc[0], { name: '8bit' });
+ assert.equal(m.doc.length, 4);
+
+ await m.save();
+
+ m = await M.findById(m);
+
+ assert.equal(m.num.length, 6);
+ assert.ok(~m.num.indexOf(1));
+ assert.ok(~m.num.indexOf(2));
+ assert.ok(~m.num.indexOf(3));
+ assert.ok(~m.num.indexOf(4));
+ assert.ok(~m.num.indexOf(5));
+ assert.ok(~m.num.indexOf(6));
+
+ assert.equal(m.str.length, 6);
+ assert.ok(~m.str.indexOf('one'));
+ assert.ok(~m.str.indexOf('two'));
+ assert.ok(~m.str.indexOf('tres'));
+ assert.ok(~m.str.indexOf('four'));
+ assert.ok(~m.str.indexOf('five'));
+ assert.ok(~m.str.indexOf('six'));
+
+ assert.equal(m.id.length, 4);
+ assert.ok(m.id.find(id => id.toString() === id1.toString()));
+ assert.ok(m.id.find(id => id.toString() === id2.toString()));
+ assert.ok(m.id.find(id => id.toString() === id3.toString()));
+ assert.ok(m.id.find(id => id.toString() === id4.toString()));
+
+ assert.equal(m.date.length, 4);
+ assert.ok(~m.date.indexOf(d1.toString()));
+ assert.ok(~m.date.indexOf(d2.toString()));
+ assert.ok(~m.date.indexOf(d3.toString()));
+ assert.ok(~m.date.indexOf(d4.toString()));
+
+ assert.equal(m.doc.length, 4);
+ assert.ok(m.doc.some(function(v) {
+ return v.name === 'Waltz';
+ }));
+ assert.ok(m.doc.some(function(v) {
+ return v.name === 'Dubstep';
+ }));
+ assert.ok(m.doc.some(function(v) {
+ return v.name === 'Polka';
+ }));
+ assert.ok(m.doc.some(function(v) {
+ return v.name === '8bit';
+ }));
+
+ // test multiple $addToSet
+ m.num.addToSet(7, 8);
+ assert.equal(m.num.length, 8);
+ m.str.addToSet('seven', 'eight');
+ assert.equal(m.str.length, 8);
+ m.id.addToSet(id5, id6);
+ assert.equal(m.id.length, 6);
+
+ m.date.addToSet(d5, d6);
+ assert.equal(m.date.length, 6);
+
+ m.doc.addToSet(m.doc[1], { name: 'BigBeat' }, { name: 'Funk' });
+ assert.equal(m.doc.length, 6);
+
+ await m.save();
+
+ m = await M.findById(m);
+
+ assert.equal(m.num.length, 8);
+ assert.ok(~m.num.indexOf(1));
+ assert.ok(~m.num.indexOf(2));
+ assert.ok(~m.num.indexOf(3));
+ assert.ok(~m.num.indexOf(4));
+ assert.ok(~m.num.indexOf(5));
+ assert.ok(~m.num.indexOf(6));
+ assert.ok(~m.num.indexOf(7));
+ assert.ok(~m.num.indexOf(8));
+
+ assert.equal(m.str.length, 8);
+ assert.ok(~m.str.indexOf('one'));
+ assert.ok(~m.str.indexOf('two'));
+ assert.ok(~m.str.indexOf('tres'));
+ assert.ok(~m.str.indexOf('four'));
+ assert.ok(~m.str.indexOf('five'));
+ assert.ok(~m.str.indexOf('six'));
+ assert.ok(~m.str.indexOf('seven'));
+ assert.ok(~m.str.indexOf('eight'));
+
+ assert.equal(m.id.length, 6);
+ assert.ok(~m.id.indexOf(id1));
+ assert.ok(~m.id.indexOf(id2));
+ assert.ok(~m.id.indexOf(id3));
+ assert.ok(~m.id.indexOf(id4));
+ assert.ok(~m.id.indexOf(id5));
+ assert.ok(~m.id.indexOf(id6));
+
+ assert.equal(m.date.length, 6);
+ assert.ok(~m.date.indexOf(d1.toString()));
+ assert.ok(~m.date.indexOf(d2.toString()));
+ assert.ok(~m.date.indexOf(d3.toString()));
+ assert.ok(~m.date.indexOf(d4.toString()));
+ assert.ok(~m.date.indexOf(d5.toString()));
+ assert.ok(~m.date.indexOf(d6.toString()));
+
+ assert.equal(m.doc.length, 6);
+ assert.ok(m.doc.some(function(v) {
+ return v.name === 'Waltz';
+ }));
+ assert.ok(m.doc.some(function(v) {
+ return v.name === 'Dubstep';
+ }));
+ assert.ok(m.doc.some(function(v) {
+ return v.name === 'Polka';
+ }));
+ assert.ok(m.doc.some(function(v) {
+ return v.name === '8bit';
+ }));
+ assert.ok(m.doc.some(function(v) {
+ return v.name === 'BigBeat';
+ }));
+ assert.ok(m.doc.some(function(v) {
+ return v.name === 'Funk';
+ }));
});
- it('handles sub-documents that do not have an _id gh-1973', function(done) {
+ it('handles sub-documents that do not have an _id gh-1973', async function() {
const e = new Schema({ name: String, arr: [] }, { _id: false });
const schema = new Schema({
doc: [e]
@@ -1161,27 +996,22 @@ describe('types array', function() {
const m = new M();
m.doc.addToSet({ name: 'Rap' });
- m.save(function(error, m) {
- assert.ifError(error);
- assert.equal(m.doc.length, 1);
- assert.equal(m.doc[0].name, 'Rap');
- m.doc.addToSet({ name: 'House' });
- assert.equal(m.doc.length, 2);
- m.save(function(error, m) {
- assert.ifError(error);
- assert.equal(m.doc.length, 2);
- assert.ok(m.doc.some(function(v) {
- return v.name === 'Rap';
- }));
- assert.ok(m.doc.some(function(v) {
- return v.name === 'House';
- }));
- done();
- });
- });
+ await m.save();
+ assert.equal(m.doc.length, 1);
+ assert.equal(m.doc[0].name, 'Rap');
+ m.doc.addToSet({ name: 'House' });
+ assert.equal(m.doc.length, 2);
+ await m.save();
+ assert.equal(m.doc.length, 2);
+ assert.ok(m.doc.some(function(v) {
+ return v.name === 'Rap';
+ }));
+ assert.ok(m.doc.some(function(v) {
+ return v.name === 'House';
+ }));
});
- it('applies setters (gh-3032)', function(done) {
+ it('applies setters (gh-3032)', async function() {
const ST = db.model('Test', Schema({
arr: [{
type: String,
@@ -1189,24 +1019,18 @@ describe('types array', function() {
}]
}));
const m = new ST({ arr: ['ONE', 'TWO'] });
- m.save(function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.arr.length, 2);
- doc.arr.addToSet('THREE');
- assert.strictEqual('one', doc.arr[0]);
- assert.strictEqual('two', doc.arr[1]);
- assert.strictEqual('three', doc.arr[2]);
-
- doc.save(function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.arr.length, 3);
- assert.strictEqual('one', doc.arr[0]);
- assert.strictEqual('two', doc.arr[1]);
- assert.strictEqual('three', doc.arr[2]);
-
- done();
- });
- });
+ const doc = await m.save();
+ assert.equal(doc.arr.length, 2);
+ doc.arr.addToSet('THREE');
+ assert.strictEqual('one', doc.arr[0]);
+ assert.strictEqual('two', doc.arr[1]);
+ assert.strictEqual('three', doc.arr[2]);
+
+ await doc.save();
+ assert.equal(doc.arr.length, 3);
+ assert.strictEqual('one', doc.arr[0]);
+ assert.strictEqual('two', doc.arr[1]);
+ assert.strictEqual('three', doc.arr[2]);
});
});
@@ -1282,7 +1106,7 @@ describe('types array', function() {
});
describe('nonAtomicPush()', function() {
- it('works', function(done) {
+ it('works', async function() {
const U = db.model('User', UserSchema);
const ID = mongoose.Types.ObjectId;
@@ -1290,391 +1114,329 @@ describe('types array', function() {
assert.equal(u.pets.length, 1);
u.pets.nonAtomicPush(new ID());
assert.equal(u.pets.length, 2);
- u.save(function(err) {
- assert.ifError(err);
- U.findById(u._id, function(err) {
- assert.ifError(err);
- assert.equal(u.pets.length, 2);
- const id0 = u.pets[0];
- const id1 = u.pets[1];
- const id2 = new ID();
- u.pets.pull(id0);
- u.pets.nonAtomicPush(id2);
- assert.equal(u.pets.length, 2);
- assert.equal(u.pets[0].toString(), id1.toString());
- assert.equal(u.pets[1].toString(), id2.toString());
- u.save(function(err) {
- assert.ifError(err);
- U.findById(u._id, function(err) {
- assert.ifError(err);
- assert.equal(u.pets.length, 2);
- assert.equal(u.pets[0].toString(), id1.toString());
- assert.equal(u.pets[1].toString(), id2.toString());
- done();
- });
- });
- });
- });
+ await u.save();
+ await U.findById(u._id);
+ assert.equal(u.pets.length, 2);
+ const id0 = u.pets[0];
+ const id1 = u.pets[1];
+ const id2 = new ID();
+ u.pets.pull(id0);
+ u.pets.nonAtomicPush(id2);
+ assert.equal(u.pets.length, 2);
+ assert.equal(u.pets[0].toString(), id1.toString());
+ assert.equal(u.pets[1].toString(), id2.toString());
+ await u.save();
+ await U.findById(u._id);
+ assert.equal(u.pets.length, 2);
+ assert.equal(u.pets[0].toString(), id1.toString());
+ assert.equal(u.pets[1].toString(), id2.toString());
+
});
});
describe('sort()', function() {
- it('order should be saved', function(done) {
+ it('order should be saved', async function() {
const M = db.model('Test', new Schema({ x: [Number] }));
- const m = new M({ x: [1, 4, 3, 2] });
- m.save(function(err) {
- assert.ifError(err);
- M.findById(m, function(err, m) {
- assert.ifError(err);
-
- assert.equal(m.x[0], 1);
- assert.equal(m.x[1], 4);
- assert.equal(m.x[2], 3);
- assert.equal(m.x[3], 2);
-
- m.x.sort();
-
- m.save(function(err) {
- assert.ifError(err);
- M.findById(m, function(err, m) {
- assert.ifError(err);
-
- assert.equal(m.x[0], 1);
- assert.equal(m.x[1], 2);
- assert.equal(m.x[2], 3);
- assert.equal(m.x[3], 4);
-
- m.x.sort(function(a, b) {
- return b - a;
- });
-
- m.save(function(err) {
- assert.ifError(err);
- M.findById(m, function(err, m) {
- assert.ifError(err);
-
- assert.equal(m.x[0], 4);
- assert.equal(m.x[1], 3);
- assert.equal(m.x[2], 2);
- assert.equal(m.x[3], 1);
- done();
- });
- });
- });
- });
- });
+ let m = new M({ x: [1, 4, 3, 2] });
+ await m.save();
+ m = await M.findById(m);
+
+ assert.equal(m.x[0], 1);
+ assert.equal(m.x[1], 4);
+ assert.equal(m.x[2], 3);
+ assert.equal(m.x[3], 2);
+
+ m.x.sort();
+
+ await m.save();
+ m = await M.findById(m);
+
+ assert.equal(m.x[0], 1);
+ assert.equal(m.x[1], 2);
+ assert.equal(m.x[2], 3);
+ assert.equal(m.x[3], 4);
+
+ m.x.sort(function(a, b) {
+ return b - a;
});
+
+ await m.save();
+ m = await M.findById(m);
+
+ assert.equal(m.x[0], 4);
+ assert.equal(m.x[1], 3);
+ assert.equal(m.x[2], 2);
+ assert.equal(m.x[3], 1);
+
});
});
describe('set()', function() {
- function save(doc, cb) {
- doc.save(function(err) {
- if (err) {
- cb(err);
- return;
- }
- doc.constructor.findById(doc._id, cb);
- });
+ async function save(doc) {
+ await doc.save();
+ return doc.constructor.findById(doc._id);
}
- it('works combined with other ops', function(done) {
+ it('works combined with other ops', async function() {
const N = db.model('Test', Schema({ arr: [Number] }));
const m = new N({ arr: [3, 4, 5, 6] });
- save(m, function(err, doc) {
- assert.ifError(err);
-
- assert.equal(doc.arr.length, 4);
- doc.arr.push(20);
- doc.arr.set(2, 10);
- assert.equal(doc.arr.length, 5);
- assert.equal(doc.arr[2], 10);
- assert.equal(doc.arr[4], 20);
-
- save(doc, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.arr.length, 5);
- assert.equal(doc.arr[0], 3);
- assert.equal(doc.arr[1], 4);
- assert.equal(doc.arr[2], 10);
- assert.equal(doc.arr[3], 6);
- assert.equal(doc.arr[4], 20);
-
- doc.arr.$pop();
- assert.equal(doc.arr.length, 4);
- doc.arr.set(4, 99);
- assert.equal(doc.arr.length, 5);
- assert.equal(doc.arr[4], 99);
- doc.arr.remove(10);
- assert.equal(doc.arr.length, 4);
- assert.equal(doc.arr[0], 3);
- assert.equal(doc.arr[1], 4);
- assert.equal(doc.arr[2], 6);
- assert.equal(doc.arr[3], 99);
-
- save(doc, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.arr.length, 4);
- assert.equal(doc.arr[0], 3);
- assert.equal(doc.arr[1], 4);
- assert.equal(doc.arr[2], 6);
- assert.equal(doc.arr[3], 99);
- done();
- });
- });
- });
+ let doc = await save(m);
+
+ assert.equal(doc.arr.length, 4);
+ doc.arr.push(20);
+ doc.arr.set(2, 10);
+ assert.equal(doc.arr.length, 5);
+ assert.equal(doc.arr[2], 10);
+ assert.equal(doc.arr[4], 20);
+
+ doc = await save(doc);
+ assert.equal(doc.arr.length, 5);
+ assert.equal(doc.arr[0], 3);
+ assert.equal(doc.arr[1], 4);
+ assert.equal(doc.arr[2], 10);
+ assert.equal(doc.arr[3], 6);
+ assert.equal(doc.arr[4], 20);
+
+ doc.arr.$pop();
+ assert.equal(doc.arr.length, 4);
+ doc.arr.set(4, 99);
+ assert.equal(doc.arr.length, 5);
+ assert.equal(doc.arr[4], 99);
+ doc.arr.remove(10);
+ assert.equal(doc.arr.length, 4);
+ assert.equal(doc.arr[0], 3);
+ assert.equal(doc.arr[1], 4);
+ assert.equal(doc.arr[2], 6);
+ assert.equal(doc.arr[3], 99);
+
+ doc = await save(doc);
+ assert.equal(doc.arr.length, 4);
+ assert.equal(doc.arr[0], 3);
+ assert.equal(doc.arr[1], 4);
+ assert.equal(doc.arr[2], 6);
+ assert.equal(doc.arr[3], 99);
+
});
- it('works with numbers', function(done) {
+ it('works with numbers', async function() {
const N = db.model('Test', Schema({ arr: [Number] }));
const m = new N({ arr: [3, 4, 5, 6] });
- save(m, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.arr.length, 4);
- doc.arr.set(2, 10);
- assert.equal(doc.arr.length, 4);
- assert.equal(doc.arr[2], 10);
- doc.arr.set(doc.arr.length, 11);
- assert.equal(doc.arr.length, 5);
- assert.equal(doc.arr[4], 11);
-
- save(doc, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.arr.length, 5);
- assert.equal(doc.arr[0], 3);
- assert.equal(doc.arr[1], 4);
- assert.equal(doc.arr[2], 10);
- assert.equal(doc.arr[3], 6);
- assert.equal(doc.arr[4], 11);
-
- // casting + setting beyond current array length
- doc.arr.set(8, '1');
- assert.equal(doc.arr.length, 9);
- assert.strictEqual(1, doc.arr[8]);
- assert.equal(doc.arr[7], undefined);
-
- save(doc, function(err, doc) {
- assert.ifError(err);
-
- assert.equal(doc.arr.length, 9);
- assert.equal(doc.arr[0], 3);
- assert.equal(doc.arr[1], 4);
- assert.equal(doc.arr[2], 10);
- assert.equal(doc.arr[3], 6);
- assert.equal(doc.arr[4], 11);
- assert.equal(doc.arr[5], null);
- assert.equal(doc.arr[6], null);
- assert.equal(doc.arr[7], null);
- assert.strictEqual(1, doc.arr[8]);
- done();
- });
- });
- });
+ let doc = await save(m);
+ assert.equal(doc.arr.length, 4);
+ doc.arr.set(2, 10);
+ assert.equal(doc.arr.length, 4);
+ assert.equal(doc.arr[2], 10);
+ doc.arr.set(doc.arr.length, 11);
+ assert.equal(doc.arr.length, 5);
+ assert.equal(doc.arr[4], 11);
+
+ doc = await save(doc);
+ assert.equal(doc.arr.length, 5);
+ assert.equal(doc.arr[0], 3);
+ assert.equal(doc.arr[1], 4);
+ assert.equal(doc.arr[2], 10);
+ assert.equal(doc.arr[3], 6);
+ assert.equal(doc.arr[4], 11);
+
+ // casting + setting beyond current array length
+ doc.arr.set(8, '1');
+ assert.equal(doc.arr.length, 9);
+ assert.strictEqual(1, doc.arr[8]);
+ assert.equal(doc.arr[7], undefined);
+
+ doc = await save(doc);
+
+ assert.equal(doc.arr.length, 9);
+ assert.equal(doc.arr[0], 3);
+ assert.equal(doc.arr[1], 4);
+ assert.equal(doc.arr[2], 10);
+ assert.equal(doc.arr[3], 6);
+ assert.equal(doc.arr[4], 11);
+ assert.equal(doc.arr[5], null);
+ assert.equal(doc.arr[6], null);
+ assert.equal(doc.arr[7], null);
+ assert.strictEqual(1, doc.arr[8]);
+
});
- it('works with strings', function(done) {
+ it('works with strings', async function() {
const S = db.model('Test', Schema({ arr: [String] }));
const m = new S({ arr: [3, 4, 5, 6] });
- save(m, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.arr.length, '4');
- doc.arr[2] = 10;
- assert.equal(doc.arr.length, 4);
- assert.equal(doc.arr[2], '10');
- doc.arr.set(doc.arr.length, '11');
- assert.equal(doc.arr.length, 5);
- assert.equal(doc.arr[4], '11');
-
- save(doc, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.arr.length, 5);
- assert.equal(doc.arr[0], '3');
- assert.equal(doc.arr[1], '4');
- assert.equal(doc.arr[2], '10');
- assert.equal(doc.arr[3], '6');
- assert.equal(doc.arr[4], '11');
-
- // casting + setting beyond current array length
- doc.arr.set(8, 'yo');
- assert.equal(doc.arr.length, 9);
- assert.strictEqual('yo', doc.arr[8]);
- assert.equal(doc.arr[7], undefined);
-
- save(doc, function(err, doc) {
- assert.ifError(err);
-
- assert.equal(doc.arr.length, '9');
- assert.equal(doc.arr[0], '3');
- assert.equal(doc.arr[1], '4');
- assert.equal(doc.arr[2], '10');
- assert.equal(doc.arr[3], '6');
- assert.equal(doc.arr[4], '11');
- assert.equal(doc.arr[5], null);
- assert.equal(doc.arr[6], null);
- assert.equal(doc.arr[7], null);
- assert.strictEqual('yo', doc.arr[8]);
- done();
- });
- });
- });
+ let doc = await save(m);
+ assert.equal(doc.arr.length, '4');
+ doc.arr[2] = 10;
+ assert.equal(doc.arr.length, 4);
+ assert.equal(doc.arr[2], '10');
+ doc.arr.set(doc.arr.length, '11');
+ assert.equal(doc.arr.length, 5);
+ assert.equal(doc.arr[4], '11');
+
+ doc = await save(doc);
+ assert.equal(doc.arr.length, 5);
+ assert.equal(doc.arr[0], '3');
+ assert.equal(doc.arr[1], '4');
+ assert.equal(doc.arr[2], '10');
+ assert.equal(doc.arr[3], '6');
+ assert.equal(doc.arr[4], '11');
+
+ // casting + setting beyond current array length
+ doc.arr.set(8, 'yo');
+ assert.equal(doc.arr.length, 9);
+ assert.strictEqual('yo', doc.arr[8]);
+ assert.equal(doc.arr[7], undefined);
+
+ doc = await save(doc);
+
+ assert.equal(doc.arr.length, '9');
+ assert.equal(doc.arr[0], '3');
+ assert.equal(doc.arr[1], '4');
+ assert.equal(doc.arr[2], '10');
+ assert.equal(doc.arr[3], '6');
+ assert.equal(doc.arr[4], '11');
+ assert.equal(doc.arr[5], null);
+ assert.equal(doc.arr[6], null);
+ assert.equal(doc.arr[7], null);
+ assert.strictEqual('yo', doc.arr[8]);
+
});
- it('works with buffers', function(done) {
+ it('works with buffers', async function() {
const B = db.model('Test', Schema({ arr: [Buffer] }));
const m = new B({ arr: [[0], Buffer.alloc(1)] });
- save(m, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.arr.length, 2);
- assert.ok(doc.arr[0].isMongooseBuffer);
- assert.ok(doc.arr[1].isMongooseBuffer);
- doc.arr.set(1, 'nice');
- assert.equal(doc.arr.length, 2);
- assert.ok(doc.arr[1].isMongooseBuffer);
- assert.equal(doc.arr[1].toString('utf8'), 'nice');
- doc.arr.set(doc.arr.length, [11]);
- assert.equal(doc.arr.length, 3);
- assert.equal(doc.arr[2][0], 11);
-
- save(doc, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.arr.length, 3);
- assert.ok(doc.arr[0].isMongooseBuffer);
- assert.ok(doc.arr[1].isMongooseBuffer);
- assert.ok(doc.arr[2].isMongooseBuffer);
- assert.equal(doc.arr[0].toString(), '\u0000');
- assert.equal(doc.arr[1].toString(), 'nice');
- assert.equal(doc.arr[2][0], 11);
- done();
- });
- });
+ let doc = await save(m);
+ assert.equal(doc.arr.length, 2);
+ assert.ok(doc.arr[0].isMongooseBuffer);
+ assert.ok(doc.arr[1].isMongooseBuffer);
+ doc.arr.set(1, 'nice');
+ assert.equal(doc.arr.length, 2);
+ assert.ok(doc.arr[1].isMongooseBuffer);
+ assert.equal(doc.arr[1].toString('utf8'), 'nice');
+ doc.arr.set(doc.arr.length, [11]);
+ assert.equal(doc.arr.length, 3);
+ assert.equal(doc.arr[2][0], 11);
+
+ doc = await save(doc);
+ assert.equal(doc.arr.length, 3);
+ assert.ok(doc.arr[0].isMongooseBuffer);
+ assert.ok(doc.arr[1].isMongooseBuffer);
+ assert.ok(doc.arr[2].isMongooseBuffer);
+ assert.equal(doc.arr[0].toString(), '\u0000');
+ assert.equal(doc.arr[1].toString(), 'nice');
+ assert.equal(doc.arr[2][0], 11);
+
});
- it('works with mixed', function(done) {
+ it('works with mixed', async function() {
const M = db.model('Test', Schema({ arr: [] }));
const m = new M({ arr: [3, { x: 1 }, 'yes', [5]] });
- save(m, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.arr.length, 4);
- doc.arr.set(2, null);
- assert.equal(doc.arr.length, 4);
- assert.equal(doc.arr[2], null);
- doc.arr.set(doc.arr.length, 'last');
- assert.equal(doc.arr.length, 5);
- assert.equal(doc.arr[4], 'last');
-
- save(doc, function(err, doc) {
- assert.ifError(err);
-
- assert.equal(doc.arr.length, 5);
- assert.equal(doc.arr[0], 3);
- assert.strictEqual(1, doc.arr[1].x);
- assert.equal(doc.arr[2], null);
- assert.ok(Array.isArray(doc.arr[3]));
- assert.equal(doc.arr[3][0], 5);
- assert.equal(doc.arr[4], 'last');
-
- doc.arr.set(8, Infinity);
- assert.equal(doc.arr.length, 9);
- assert.strictEqual(Infinity, doc.arr[8]);
- assert.equal(doc.arr[7], undefined);
-
- doc.arr.push(Buffer.alloc(0));
- assert.equal(doc.arr[9].toString(), '');
- assert.equal(doc.arr.length, 10);
-
- save(doc, function(err, doc) {
- assert.ifError(err);
-
- assert.equal(doc.arr.length, 10);
- assert.equal(doc.arr[0], 3);
- assert.strictEqual(1, doc.arr[1].x);
- assert.equal(doc.arr[2], null);
- assert.ok(Array.isArray(doc.arr[3]));
- assert.equal(doc.arr[3][0], 5);
- assert.equal(doc.arr[4], 'last');
- assert.strictEqual(null, doc.arr[5]);
- assert.strictEqual(null, doc.arr[6]);
- assert.strictEqual(null, doc.arr[7]);
- assert.strictEqual(Infinity, doc.arr[8]);
- // arr[9] is actually a mongodb Binary since mixed won't cast to buffer
- assert.equal(doc.arr[9].toString(), '');
-
- done();
- });
- });
- });
+ let doc = await save(m);
+ assert.equal(doc.arr.length, 4);
+ doc.arr.set(2, null);
+ assert.equal(doc.arr.length, 4);
+ assert.equal(doc.arr[2], null);
+ doc.arr.set(doc.arr.length, 'last');
+ assert.equal(doc.arr.length, 5);
+ assert.equal(doc.arr[4], 'last');
+
+ doc = await save(doc);
+
+ assert.equal(doc.arr.length, 5);
+ assert.equal(doc.arr[0], 3);
+ assert.strictEqual(1, doc.arr[1].x);
+ assert.equal(doc.arr[2], null);
+ assert.ok(Array.isArray(doc.arr[3]));
+ assert.equal(doc.arr[3][0], 5);
+ assert.equal(doc.arr[4], 'last');
+
+ doc.arr.set(8, Infinity);
+ assert.equal(doc.arr.length, 9);
+ assert.strictEqual(Infinity, doc.arr[8]);
+ assert.equal(doc.arr[7], undefined);
+
+ doc.arr.push(Buffer.alloc(0));
+ assert.equal(doc.arr[9].toString(), '');
+ assert.equal(doc.arr.length, 10);
+
+ doc = await save(doc);
+ assert.equal(doc.arr.length, 10);
+ assert.equal(doc.arr[0], 3);
+ assert.strictEqual(1, doc.arr[1].x);
+ assert.equal(doc.arr[2], null);
+ assert.ok(Array.isArray(doc.arr[3]));
+ assert.equal(doc.arr[3][0], 5);
+ assert.equal(doc.arr[4], 'last');
+ assert.strictEqual(null, doc.arr[5]);
+ assert.strictEqual(null, doc.arr[6]);
+ assert.strictEqual(null, doc.arr[7]);
+ assert.strictEqual(Infinity, doc.arr[8]);
+ // arr[9] is actually a mongodb Binary since mixed won't cast to buffer
+ assert.equal(doc.arr[9].toString(), '');
+
+
});
- it('works with sub-docs', function(done) {
+ it('works with sub-docs', async function() {
const D = db.model('Test', Schema({ arr: [{ name: String }] }));
const m = new D({ arr: [{ name: 'aaron' }, { name: 'moombahton ' }] });
- save(m, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.arr.length, 2);
- doc.arr[0] = { name: 'vdrums' };
- assert.equal(doc.arr.length, 2);
- assert.equal(doc.arr[0].name, 'vdrums');
- doc.arr.set(doc.arr.length, { name: 'Restrepo' });
- assert.equal(doc.arr.length, 3);
- assert.equal(doc.arr[2].name, 'Restrepo');
-
- save(doc, function(err, doc) {
- assert.ifError(err);
-
- // validate
- assert.equal(doc.arr.length, 3);
- assert.equal(doc.arr[0].name, 'vdrums');
- assert.equal(doc.arr[1].name, 'moombahton ');
- assert.equal(doc.arr[2].name, 'Restrepo');
-
- doc.arr.set(10, { name: 'temple of doom' });
- assert.equal(doc.arr.length, 11);
- assert.equal(doc.arr[10].name, 'temple of doom');
- assert.equal(doc.arr[9], null);
-
- save(doc, function(err, doc) {
- assert.ifError(err);
-
- // validate
- assert.equal(doc.arr.length, 11);
- assert.equal(doc.arr[0].name, 'vdrums');
- assert.equal(doc.arr[1].name, 'moombahton ');
- assert.equal(doc.arr[2].name, 'Restrepo');
- assert.equal(doc.arr[3], null);
- assert.equal(doc.arr[9], null);
- assert.equal(doc.arr[10].name, 'temple of doom');
-
- doc.arr.remove(doc.arr[0]);
- doc.arr.set(7, { name: 7 });
- assert.strictEqual('7', doc.arr[7].name);
- assert.equal(doc.arr.length, 10);
-
- save(doc, function(err, doc) {
- assert.ifError(err);
-
- assert.equal(doc.arr.length, 10);
- assert.equal(doc.arr[0].name, 'moombahton ');
- assert.equal(doc.arr[1].name, 'Restrepo');
- assert.equal(doc.arr[2], null);
- assert.ok(doc.arr[7]);
- assert.strictEqual('7', doc.arr[7].name);
- assert.equal(doc.arr[8], null);
- assert.equal(doc.arr[9].name, 'temple of doom');
-
- done();
- });
- });
- });
- });
+ let doc = await save(m);
+ assert.equal(doc.arr.length, 2);
+ doc.arr[0] = { name: 'vdrums' };
+ assert.equal(doc.arr.length, 2);
+ assert.equal(doc.arr[0].name, 'vdrums');
+ doc.arr.set(doc.arr.length, { name: 'Restrepo' });
+ assert.equal(doc.arr.length, 3);
+ assert.equal(doc.arr[2].name, 'Restrepo');
+
+ doc = await save(doc);
+
+ // validate
+ assert.equal(doc.arr.length, 3);
+ assert.equal(doc.arr[0].name, 'vdrums');
+ assert.equal(doc.arr[1].name, 'moombahton ');
+ assert.equal(doc.arr[2].name, 'Restrepo');
+
+ doc.arr.set(10, { name: 'temple of doom' });
+ assert.equal(doc.arr.length, 11);
+ assert.equal(doc.arr[10].name, 'temple of doom');
+ assert.equal(doc.arr[9], null);
+
+ doc = await save(doc);
+
+ // validate
+ assert.equal(doc.arr.length, 11);
+ assert.equal(doc.arr[0].name, 'vdrums');
+ assert.equal(doc.arr[1].name, 'moombahton ');
+ assert.equal(doc.arr[2].name, 'Restrepo');
+ assert.equal(doc.arr[3], null);
+ assert.equal(doc.arr[9], null);
+ assert.equal(doc.arr[10].name, 'temple of doom');
+
+ doc.arr.remove(doc.arr[0]);
+ doc.arr.set(7, { name: 7 });
+ assert.strictEqual('7', doc.arr[7].name);
+ assert.equal(doc.arr.length, 10);
+
+ doc = await save(doc);
+
+ assert.equal(doc.arr.length, 10);
+ assert.equal(doc.arr[0].name, 'moombahton ');
+ assert.equal(doc.arr[1].name, 'Restrepo');
+ assert.equal(doc.arr[2], null);
+ assert.ok(doc.arr[7]);
+ assert.strictEqual('7', doc.arr[7].name);
+ assert.equal(doc.arr[8], null);
+ assert.equal(doc.arr[9].name, 'temple of doom');
+
+
});
- it('applies setters (gh-3032)', function(done) {
+ it('applies setters (gh-3032)', async function() {
const ST = db.model('Test', Schema({
arr: [{
type: String,
@@ -1683,27 +1445,21 @@ describe('types array', function() {
}));
const m = new ST({ arr: ['ONE', 'TWO'] });
- save(m, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.arr.length, 2);
- doc.arr.set(0, 'THREE');
- assert.strictEqual('three', doc.arr[0]);
- assert.strictEqual('two', doc.arr[1]);
- doc.arr[doc.arr.length] = 'FOUR';
- assert.strictEqual('three', doc.arr[0]);
- assert.strictEqual('two', doc.arr[1]);
- assert.strictEqual('four', doc.arr[2]);
-
- save(doc, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.arr.length, 3);
- assert.strictEqual('three', doc.arr[0]);
- assert.strictEqual('two', doc.arr[1]);
- assert.strictEqual('four', doc.arr[2]);
-
- done();
- });
- });
+ let doc = await save(m);
+ assert.equal(doc.arr.length, 2);
+ doc.arr.set(0, 'THREE');
+ assert.strictEqual('three', doc.arr[0]);
+ assert.strictEqual('two', doc.arr[1]);
+ doc.arr[doc.arr.length] = 'FOUR';
+ assert.strictEqual('three', doc.arr[0]);
+ assert.strictEqual('two', doc.arr[1]);
+ assert.strictEqual('four', doc.arr[2]);
+
+ doc = await save(doc);
+ assert.equal(doc.arr.length, 3);
+ assert.strictEqual('three', doc.arr[0]);
+ assert.strictEqual('two', doc.arr[1]);
+ assert.strictEqual('four', doc.arr[2]);
});
});
@@ -1745,12 +1501,12 @@ describe('types array', function() {
});
describe('setting a doc array', function() {
- it('should adjust path positions', function(done) {
+ it('should adjust path positions', async function() {
const D = db.model('Test', new Schema({
em1: [new Schema({ name: String })]
}));
- const d = new D({
+ let d = new D({
em1: [
{ name: 'pos0' },
{ name: 'pos1' },
@@ -1758,35 +1514,27 @@ describe('types array', function() {
]
});
- d.save(function(err) {
- assert.ifError(err);
- D.findById(d, function(err, d) {
- assert.ifError(err);
-
- const n = d.em1.slice();
- n[2].name = 'position two';
- let x = [];
- x[1] = n[2];
- x[2] = n[1];
- x = x.filter(Boolean);
- d.em1 = x;
-
- d.save(function(err) {
- assert.ifError(err);
- D.findById(d, function(err, d) {
- assert.ifError(err);
- assert.equal(d.em1[0].name, 'position two');
- assert.equal(d.em1[1].name, 'pos1');
- done();
- });
- });
- });
- });
+ await d.save();
+ d = await D.findById(d);
+
+ const n = d.em1.slice();
+ n[2].name = 'position two';
+ let x = [];
+ x[1] = n[2];
+ x[2] = n[1];
+ x = x.filter(Boolean);
+ d.em1 = x;
+
+ await d.save();
+ d = await D.findById(d);
+ assert.equal(d.em1[0].name, 'position two');
+ assert.equal(d.em1[1].name, 'pos1');
+
});
});
describe('paths with similar names', function() {
- it('should be saved', function(done) {
+ it('should be saved', async function() {
const D = db.model('Test', new Schema({
account: {
role: String,
@@ -1795,108 +1543,76 @@ describe('types array', function() {
em: [new Schema({ name: String })]
}));
- const d = new D({
+ let d = new D({
account: { role: 'teacher', roles: ['teacher', 'admin'] },
em: [{ name: 'bob' }]
});
- d.save(function(err) {
- assert.ifError(err);
- D.findById(d, function(err, d) {
- assert.ifError(err);
-
- d.account.role = 'president';
- d.account.roles = ['president', 'janitor'];
- d.em[0].name = 'memorable';
- d.em = [{ name: 'frida' }];
-
- d.save(function(err) {
- assert.ifError(err);
- D.findById(d, function(err, d) {
- assert.ifError(err);
- assert.equal(d.account.role, 'president');
- assert.equal(d.account.roles.length, 2);
- assert.equal(d.account.roles[0], 'president');
- assert.equal(d.account.roles[1], 'janitor');
- assert.equal(d.em.length, 1);
- assert.equal(d.em[0].name, 'frida');
- done();
- });
- });
- });
- });
+ await d.save();
+ d = await D.findById(d);
+
+ d.account.role = 'president';
+ d.account.roles = ['president', 'janitor'];
+ d.em[0].name = 'memorable';
+ d.em = [{ name: 'frida' }];
+
+ await d.save();
+ d = await D.findById(d);
+ assert.equal(d.account.role, 'president');
+ assert.equal(d.account.roles.length, 2);
+ assert.equal(d.account.roles[0], 'president');
+ assert.equal(d.account.roles[1], 'janitor');
+ assert.equal(d.em.length, 1);
+ assert.equal(d.em[0].name, 'frida');
+
});
});
describe('of number', function() {
- it('allows null and undefined', function(done) {
+ it('allows null and undefined', async function() {
const schema = new Schema({ x: [Number] });
const M = db.model('Test', schema);
let m;
m = new M({ x: [1, null, 3] });
- m.save(function(err) {
- assert.ifError(err);
-
- m = new M({ x: [1, undefined, 3] });
- m.save(function(err) {
- assert.ifError(err);
-
- m.x = [1,, 3]; // eslint-disable-line no-sparse-arrays
- m.save(function(err) {
- assert.ifError(err);
- assert.strictEqual(m.x[1], void 0);
- m.x.set(1, 2);
- m.save(function(err) {
- assert.ifError(err);
- assert.deepEqual(m.toObject().x, [1, 2, 3]);
- done();
- });
- });
- });
- });
+ await m.save();
+
+ m = new M({ x: [1, undefined, 3] });
+ await m.save();
+ m.x = [1,, 3]; // eslint-disable-line no-sparse-arrays
+ await m.save();
+ assert.strictEqual(m.x[1], void 0);
+ m.x.set(1, 2);
+ await m.save();
+ assert.deepEqual(m.toObject().x, [1, 2, 3]);
+
});
});
describe('bug fixes', function() {
- it('modifying subdoc props and manipulating the array works (gh-842)', function(done) {
+ it('modifying subdoc props and manipulating the array works (gh-842)', async function() {
const schema = new Schema({ em: [new Schema({ username: String })] });
const M = db.model('Test', schema);
- const m = new M({ em: [{ username: 'Arrietty' }] });
-
- m.save(function(err) {
- assert.ifError(err);
- M.findById(m, function(err, m) {
- assert.ifError(err);
- assert.equal(m.em[0].username, 'Arrietty');
-
- m.em[0].username = 'Shawn';
- m.em.push({ username: 'Homily' });
- m.save(function(err) {
- assert.ifError(err);
-
- M.findById(m, function(err, m) {
- assert.ifError(err);
- assert.equal(m.em.length, 2);
- assert.equal(m.em[0].username, 'Shawn');
- assert.equal(m.em[1].username, 'Homily');
-
- m.em[0].username = 'Arrietty';
- m.em[1].remove();
- m.save(function(err) {
- assert.ifError(err);
-
- M.findById(m, function(err, m) {
- assert.ifError(err);
- assert.equal(m.em.length, 1);
- assert.equal(m.em[0].username, 'Arrietty');
- done();
- });
- });
- });
- });
- });
- });
+ let m = new M({ em: [{ username: 'Arrietty' }] });
+
+ await m.save();
+ m = await M.findById(m);
+ assert.equal(m.em[0].username, 'Arrietty');
+
+ m.em[0].username = 'Shawn';
+ m.em.push({ username: 'Homily' });
+ await m.save();
+ m = await M.findById(m);
+ assert.equal(m.em.length, 2);
+ assert.equal(m.em[0].username, 'Shawn');
+ assert.equal(m.em[1].username, 'Homily');
+
+ m.em[0].username = 'Arrietty';
+ m.em[1].deleteOne();
+ await m.save();
+ m = await M.findById(m);
+ assert.equal(m.em.length, 1);
+ assert.equal(m.em[0].username, 'Arrietty');
});
it('toObject returns a vanilla JavaScript array (gh-9540)', function() {
@@ -1916,33 +1632,25 @@ describe('types array', function() {
assert.deepStrictEqual(arr, [1, 2, 3]);
});
- it('pushing top level arrays and subarrays works (gh-1073)', function(done) {
+ it('pushing top level arrays and subarrays works (gh-1073)', async function() {
const schema = new Schema({ em: [new Schema({ sub: [String] })] });
const M = db.model('Test', schema);
- const m = new M({ em: [{ sub: [] }] });
- m.save(function() {
- M.findById(m, function(err, m) {
- assert.ifError(err);
-
- m.em[m.em.length - 1].sub.push('a');
- m.em.push({ sub: [] });
-
- assert.equal(m.em.length, 2);
- assert.equal(m.em[0].sub.length, 1);
-
- m.save(function(err) {
- assert.ifError(err);
-
- M.findById(m, function(err, m) {
- assert.ifError(err);
- assert.equal(m.em.length, 2);
- assert.equal(m.em[0].sub.length, 1);
- assert.equal(m.em[0].sub[0], 'a');
- done();
- });
- });
- });
- });
+ let m = new M({ em: [{ sub: [] }] });
+ await m.save();
+ m = await M.findById(m);
+
+ m.em[m.em.length - 1].sub.push('a');
+ m.em.push({ sub: [] });
+
+ assert.equal(m.em.length, 2);
+ assert.equal(m.em[0].sub.length, 1);
+
+ await m.save();
+
+ m = await M.findById(m);
+ assert.equal(m.em.length, 2);
+ assert.equal(m.em[0].sub.length, 1);
+ assert.equal(m.em[0].sub[0], 'a');
});
it('finding ids by string (gh-4011)', function(done) {
@@ -2007,7 +1715,7 @@ describe('types array', function() {
describe('removing from an array atomically using MongooseArray#remove', function() {
let B;
- before(function(done) {
+ before(function() {
const schema = new Schema({
numbers: ['number'],
numberIds: [{ _id: 'number', name: 'string' }],
@@ -2017,42 +1725,28 @@ describe('types array', function() {
});
B = db.model('BlogPost', schema);
- done();
});
- it('works', function(done) {
+ it('works', async function() {
const post = new B();
post.numbers.push(1, 2, 3);
- post.save(function(err) {
- assert.ifError(err);
+ await post.save();
+ let doc = await B.findById(post._id);
- B.findById(post._id, function(err, doc) {
- assert.ifError(err);
+ doc.numbers.remove('1');
+ await doc.save();
- doc.numbers.remove('1');
- doc.save(function(err) {
- assert.ifError(err);
+ doc = await B.findById(post.get('_id'));
- B.findById(post.get('_id'), function(err, doc) {
- assert.ifError(err);
+ assert.equal(doc.numbers.length, 2);
+ doc.numbers.remove('2', '3');
- assert.equal(doc.numbers.length, 2);
- doc.numbers.remove('2', '3');
+ await doc.save();
- doc.save(function(err) {
- assert.ifError(err);
+ doc = await B.findById(post._id);
+ assert.equal(doc.numbers.length, 0);
- B.findById(post._id, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.numbers.length, 0);
- done();
- });
- });
- });
- });
- });
- });
});
describe('with subdocs', function() {
@@ -2062,87 +1756,55 @@ describe('types array', function() {
});
}
- it('supports passing strings', function(done) {
- const post = new B({ stringIds: docs('a b c d'.split(' ')) });
- post.save(function(err) {
- assert.ifError(err);
- B.findById(post, function(err, post) {
- assert.ifError(err);
- post.stringIds.remove('b');
- post.save(function(err) {
- assert.ifError(err);
- B.findById(post, function(err, post) {
- assert.ifError(err);
- assert.equal(post.stringIds.length, 3);
- assert.ok(!post.stringIds.id('b'));
- done();
- });
- });
- });
- });
- });
- it('supports passing numbers', function(done) {
- const post = new B({ numberIds: docs([1, 2, 3, 4]) });
- post.save(function(err) {
- assert.ifError(err);
- B.findById(post, function(err, post) {
- assert.ifError(err);
- post.numberIds.remove(2, 4);
- post.save(function(err) {
- assert.ifError(err);
- B.findById(post, function(err, post) {
- assert.ifError(err);
- assert.equal(post.numberIds.length, 2);
- assert.ok(!post.numberIds.id(2));
- assert.ok(!post.numberIds.id(4));
- done();
- });
- });
- });
- });
- });
- it('supports passing objectids', function(done) {
+ it('supports passing strings', async function() {
+ let post = new B({ stringIds: docs('a b c d'.split(' ')) });
+ await post.save();
+ post = await B.findById(post);
+ post.stringIds.remove('b');
+ await post.save();
+ post = await B.findById(post);
+ assert.equal(post.stringIds.length, 3);
+ assert.ok(!post.stringIds.id('b'));
+
+ });
+ it('supports passing numbers', async function() {
+ let post = new B({ numberIds: docs([1, 2, 3, 4]) });
+ await post.save();
+ post = await B.findById(post);
+ post.numberIds.remove(2, 4);
+ await post.save();
+ post = await B.findById(post);
+ assert.equal(post.numberIds.length, 2);
+ assert.ok(!post.numberIds.id(2));
+ assert.ok(!post.numberIds.id(4));
+
+ });
+ it('supports passing objectids', async function() {
const OID = mongoose.Types.ObjectId;
const a = new OID();
const b = new OID();
const c = new OID();
- const post = new B({ oidIds: docs([a, b, c]) });
- post.save(function(err) {
- assert.ifError(err);
- B.findById(post, function(err, post) {
- assert.ifError(err);
- post.oidIds.remove(a, c);
- post.save(function(err) {
- assert.ifError(err);
- B.findById(post, function(err, post) {
- assert.ifError(err);
- assert.equal(post.oidIds.length, 1);
- assert.ok(!post.oidIds.id(a));
- assert.ok(!post.oidIds.id(c));
- done();
- });
- });
- });
- });
- });
- it('supports passing buffers', function(done) {
- const post = new B({ bufferIds: docs(['a', 'b', 'c', 'd']) });
- post.save(function(err) {
- assert.ifError(err);
- B.findById(post, function(err, post) {
- assert.ifError(err);
- post.bufferIds.remove(Buffer.from('a'));
- post.save(function(err) {
- assert.ifError(err);
- B.findById(post, function(err, post) {
- assert.ifError(err);
- assert.equal(post.bufferIds.length, 3);
- assert.ok(!post.bufferIds.id(Buffer.from('a')));
- done();
- });
- });
- });
- });
+ let post = new B({ oidIds: docs([a, b, c]) });
+ await post.save();
+ post = await B.findById(post);
+ post.oidIds.remove(a, c);
+ await post.save();
+ post = await B.findById(post);
+ assert.equal(post.oidIds.length, 1);
+ assert.ok(!post.oidIds.id(a));
+ assert.ok(!post.oidIds.id(c));
+
+ });
+ it('supports passing buffers', async function() {
+ let post = new B({ bufferIds: docs(['a', 'b', 'c', 'd']) });
+ await post.save();
+ post = await B.findById(post);
+ post.bufferIds.remove(Buffer.from('a'));
+ await post.save();
+ post = await B.findById(post);
+ assert.equal(post.bufferIds.length, 3);
+ assert.ok(!post.bufferIds.id(Buffer.from('a')));
+
});
});
});
@@ -2258,4 +1920,42 @@ describe('types array', function() {
m.Schema.Types.Array.options.castNonArrays = true;
});
+
+ it('supports setting nested arrays directly (gh-13372)', function() {
+ const Test = db.model('Test', new Schema({ intArr: [[Number]] }));
+
+ const intArr = [[1, 2], [3, 4]];
+ const doc = Test.hydrate({ intArr });
+
+ doc.intArr[0][0] = 2;
+ doc.intArr[1][1] = 5;
+ assert.deepStrictEqual(doc.getChanges(), {
+ $set: {
+ 'intArr.0.0': 2,
+ 'intArr.1.1': 5
+ }
+ });
+ });
+
+ it('calls array setters (gh-11380)', function() {
+ let called = 0;
+ const Test = db.model('Test', new Schema({
+ intArr: [{
+ type: Number,
+ set: v => {
+ ++called;
+ return Math.floor(v);
+ }
+ }]
+ }));
+
+ assert.equal(called, 0);
+ const doc = new Test({ intArr: [3.14] });
+ assert.deepStrictEqual(doc.intArr, [3]);
+ assert.equal(called, 1);
+
+ doc.intArr.push(2.718);
+ assert.deepStrictEqual(doc.intArr, [3, 2]);
+ assert.equal(called, 2);
+ });
});
diff --git a/test/types.document.test.js b/test/types.document.test.js
index d30f6e45119..8a87b06917e 100644
--- a/test/types.document.test.js
+++ b/test/types.document.test.js
@@ -9,9 +9,9 @@ const start = require('./common');
const assert = require('assert');
const mongoose = start.mongoose;
-const ArraySubdocument = require('../lib/types/ArraySubdocument');
+const ArraySubdocument = require('../lib/types/arraySubdocument');
const EventEmitter = require('events').EventEmitter;
-const DocumentArray = require('../lib/types/DocumentArray');
+const DocumentArray = require('../lib/types/documentArray');
const Schema = mongoose.Schema;
const ValidationError = mongoose.Document.ValidationError;
@@ -129,7 +129,7 @@ describe('types.document', function() {
assert.equal(super8.ratings.id(id4).stars.valueOf(), 6);
super8.ratings.id(id1).stars = 5;
- super8.ratings.id(id2).remove();
+ super8.ratings.id(id2).deleteOne();
super8.ratings.id(id3).stars = 4;
super8.ratings.id(id4).stars = 3;
@@ -144,7 +144,7 @@ describe('types.document', function() {
assert.equal(movie.ratings.id(id4).stars.valueOf(), 3);
movie.ratings.id(id1).stars = 2;
- movie.ratings.id(id3).remove();
+ movie.ratings.id(id3).deleteOne();
movie.ratings.id(id4).stars = 1;
await movie.save();
@@ -157,8 +157,8 @@ describe('types.document', function() {
assert.equal(modifiedMovie.ratings.id(id4).stars.valueOf(), 1);
// gh-531
- modifiedMovie.ratings[0].remove();
- modifiedMovie.ratings[0].remove();
+ modifiedMovie.ratings[0].deleteOne();
+ modifiedMovie.ratings[0].deleteOne();
await modifiedMovie.save();
const finalMovie = await Movie.findById(super8._id);
diff --git a/test/types.documentarray.test.js b/test/types.documentarray.test.js
index 6c136efe558..1c2749656de 100644
--- a/test/types.documentarray.test.js
+++ b/test/types.documentarray.test.js
@@ -6,8 +6,8 @@
const start = require('./common');
-const DocumentArray = require('../lib/types/DocumentArray');
-const ArraySubdocument = require('../lib/types/ArraySubdocument');
+const DocumentArray = require('../lib/types/documentArray');
+const ArraySubdocument = require('../lib/types/arraySubdocument');
const assert = require('assert');
const idGetter = require('../lib/helpers/schema/idGetter');
const setValue = require('../lib/utils').setValue;
@@ -63,7 +63,7 @@ describe('types.documentarray', function() {
afterEach(() => require('./util').clearTestData(db));
afterEach(() => require('./util').stopRemainingOps(db));
- it('behaves and quacks like an array', function(done) {
+ it('behaves and quacks like an array', function() {
const a = new MongooseDocumentArray();
assert.ok(a instanceof Array);
@@ -73,10 +73,10 @@ describe('types.documentarray', function() {
assert.deepEqual(a.$atomics().constructor, Object);
- done();
+
});
- it('#id', function(done) {
+ it('#id', function() {
let Subdocument = TestDoc();
let sub1 = new Subdocument();
@@ -187,11 +187,11 @@ describe('types.documentarray', function() {
a = new MongooseDocumentArray([sub]);
assert.equal(a.id(id).title, 'Hello again to all my friends');
- done();
+
});
describe('inspect', function() {
- it('works with bad data', function(done) {
+ it('works with bad data', function() {
let threw = false;
const a = new MongooseDocumentArray([null]);
try {
@@ -201,12 +201,12 @@ describe('types.documentarray', function() {
console.error(err.stack);
}
assert.ok(!threw);
- done();
+
});
});
describe('toObject', function() {
- it('works with bad data', function(done) {
+ it('works with bad data', function() {
let threw = false;
const a = new MongooseDocumentArray([null]);
try {
@@ -216,9 +216,9 @@ describe('types.documentarray', function() {
console.error(err.stack);
}
assert.ok(!threw);
- done();
+
});
- it('passes options to its documents (gh-1415) (gh-4455)', function(done) {
+ it('passes options to its documents (gh-1415) (gh-4455)', function() {
const subSchema = new Schema({
title: { type: String }
});
@@ -239,9 +239,9 @@ describe('types.documentarray', function() {
const delta = m.$__delta()[1];
assert.equal(delta.$push.docs.$each[0].changed, undefined);
- done();
+
});
- it('uses the correct transform (gh-1412)', function(done) {
+ it('uses the correct transform (gh-1412)', function() {
const SecondSchema = new Schema({});
SecondSchema.set('toObject', {
@@ -276,12 +276,12 @@ describe('types.documentarray', function() {
assert.ok(obj.second[1].secondToObject);
assert.ok(!obj.second[0].firstToObject);
assert.ok(!obj.second[1].firstToObject);
- done();
+
});
});
describe('create()', function() {
- it('works', function(done) {
+ it('works', function() {
const a = new MongooseDocumentArray([]);
assert.equal(typeof a.create, 'function');
@@ -294,12 +294,12 @@ describe('types.documentarray', function() {
assert.ok(subdoc._id);
assert.equal(subdoc.name, '100');
assert.ok(subdoc instanceof ArraySubdocument);
- done();
+
});
});
describe('push()', function() {
- it('does not re-cast instances of its embedded doc', function(done) {
+ it('does not re-cast instances of its embedded doc', async function() {
const child = new Schema({ name: String, date: Date });
child.pre('save', function(next) {
this.date = new Date();
@@ -308,39 +308,28 @@ describe('types.documentarray', function() {
const schema = new Schema({ children: [child] });
const M = db.model('Test', schema);
const m = new M();
- m.save(function(err) {
- assert.ifError(err);
- M.findById(m._id, function(err, doc) {
- assert.ifError(err);
- const c = doc.children.create({ name: 'first' });
- assert.equal(c.date, undefined);
- doc.children.push(c);
- assert.equal(c.date, undefined);
- doc.save(function(err) {
- assert.ifError(err);
- assert.ok(doc.children[doc.children.length - 1].date);
- assert.equal(c.date, doc.children[doc.children.length - 1].date);
-
- doc.children.push(c);
- doc.children.push(c);
-
- doc.save(function(err) {
- assert.ifError(err);
- M.findById(m._id, function(err, doc) {
- assert.ifError(err);
- assert.equal(doc.children.length, 3);
- doc.children.forEach(function(child) {
- assert.equal(doc.children[0].id, child.id);
- });
- done();
- });
- });
- });
- });
+ await m.save();
+ let doc = await M.findById(m._id);
+ const c = doc.children.create({ name: 'first' });
+ assert.equal(c.date, undefined);
+ doc.children.push(c);
+ assert.equal(c.date, undefined);
+ await doc.save();
+ assert.ok(doc.children[doc.children.length - 1].date);
+ assert.equal(c.date, doc.children[doc.children.length - 1].date);
+
+ doc.children.push(c);
+ doc.children.push(c);
+
+ await doc.save();
+ doc = await M.findById(m._id);
+ assert.equal(doc.children.length, 3);
+ doc.children.forEach(function(child) {
+ assert.equal(doc.children[0].id, child.id);
});
});
- it('corrects #ownerDocument() and index if value was created with array.create() (gh-1385)', function(done) {
+ it('corrects #ownerDocument() and index if value was created with array.create() (gh-1385)', function() {
const mg = new mongoose.Mongoose();
const M = mg.model('Test', { docs: [{ name: String }] });
const m = new M();
@@ -349,10 +338,10 @@ describe('types.documentarray', function() {
m.docs.push(doc);
assert.equal(doc.ownerDocument()._id, String(m._id));
assert.strictEqual(doc.__index, 0);
- done();
+
});
- it('corrects #ownerDocument() if value was created with array.create() and set() (gh-7504)', function(done) {
+ it('corrects #ownerDocument() if value was created with array.create() and set() (gh-7504)', function() {
const M = db.model('Test', {
docs: [{ name: { type: String, validate: () => false } }]
});
@@ -363,7 +352,7 @@ describe('types.documentarray', function() {
assert.strictEqual(doc.__index, 0);
assert.ok(m.validateSync().errors['docs.0.name']);
- done();
+
});
it('reports validation errors with correct index path (gh-7724)', function() {
@@ -396,7 +385,7 @@ describe('types.documentarray', function() {
});
});
- it('#push should work on ArraySubdocument more than 2 levels deep', function(done) {
+ it('#push should work on ArraySubdocument more than 2 levels deep', async function() {
const Comments = new Schema();
Comments.add({
title: String,
@@ -409,7 +398,7 @@ describe('types.documentarray', function() {
const Post = db.model('BlogPost', BlogPost);
- const p = new Post({ title: 'comment nesting' });
+ let p = new Post({ title: 'comment nesting' });
const c1 = p.comments.create({ title: 'c1' });
const c2 = c1.comments.create({ title: 'c2' });
const c3 = c2.comments.create({ title: 'c3' });
@@ -418,28 +407,17 @@ describe('types.documentarray', function() {
c1.comments.push(c2);
c2.comments.push(c3);
- p.save(function(err) {
- assert.ifError(err);
-
- Post.findById(p._id, function(err, p) {
- assert.ifError(err);
-
- p.comments[0].comments[0].comments[0].comments.push({ title: 'c4' });
- p.save(function(err) {
- assert.ifError(err);
+ await p.save();
+ p = await Post.findById(p._id);
- Post.findById(p._id, function(err, p) {
- assert.ifError(err);
- assert.equal(p.comments[0].comments[0].comments[0].comments[0].title, 'c4');
- done();
- });
- });
- });
- });
+ p.comments[0].comments[0].comments[0].comments.push({ title: 'c4' });
+ await p.save();
+ p = await Post.findById(p._id);
+ assert.equal(p.comments[0].comments[0].comments[0].comments[0].title, 'c4');
});
describe('required (gh-6364)', function() {
- it('on top level', function(done) {
+ it('on top level', function() {
const calls = [];
const schema = new Schema({
docs: {
@@ -459,10 +437,10 @@ describe('types.documentarray', function() {
t.validateSync();
assert.equal(calls.length, 1);
- done();
+
});
- it('in arr', function(done) {
+ it('in arr', function() {
const calls = [];
const schema = new Schema({
docs: [{
@@ -484,12 +462,12 @@ describe('types.documentarray', function() {
assert.equal(calls.length, 2);
assert.ok(err);
assert.ok(err.errors['docs.0']);
- done();
+
});
});
describe('invalidate()', function() {
- it('works', function(done) {
+ it('works', async function() {
const schema = new Schema({ docs: [{ name: 'string' }] });
schema.pre('validate', function(next) {
const subdoc = this.docs[this.docs.length - 1];
@@ -506,28 +484,24 @@ describe('types.documentarray', function() {
// has no parent array
subdoc.invalidate('name', 'junk', 47);
});
- t.validate(function() {
- const e = t.errors['docs.0.name'];
- assert.ok(e);
- assert.equal(e.path, 'docs.0.name');
- assert.equal(e.kind, 'user defined');
- assert.equal(e.message, 'boo boo');
- assert.equal(e.value, '%');
- done();
- });
+ await t.validate().catch(() => {});
+ const e = t.errors['docs.0.name'];
+ assert.ok(e);
+ assert.equal(e.path, 'docs.0.name');
+ assert.equal(e.kind, 'user defined');
+ assert.equal(e.message, 'boo boo');
+ assert.equal(e.value, '%');
});
- it('handles validation failures', function(done) {
+ it('handles validation failures', async function() {
const nested = new Schema({ v: { type: Number, max: 30 } });
const schema = new Schema({
docs: [nested]
});
const M = db.model('Test', schema);
const m = new M({ docs: [{ v: 900 }] });
- m.save(function(err) {
- assert.equal(err.errors['docs.0.v'].value, 900);
- done();
- });
+ const err = await m.save().then(() => null, err => err);
+ assert.equal(err.errors['docs.0.v'].value, 900);
});
it('clears listeners on cast error (gh-6723)', function() {
@@ -780,4 +754,28 @@ describe('types.documentarray', function() {
assert.equal(doc.myMap.get('foo').$path(), 'myMap.foo');
});
+
+ it('bubbles up validation errors from doubly nested doc arrays (gh-14101)', async function() {
+ const optionsSchema = new mongoose.Schema({
+ val: {
+ type: Number,
+ required: true
+ }
+ });
+
+ const testSchema = new mongoose.Schema({
+ name: String,
+ options: {
+ type: [[optionsSchema]],
+ required: true
+ }
+ });
+
+ const Test = db.model('Test', testSchema);
+
+ await assert.rejects(
+ Test.create({ name: 'test', options: [[{ val: null }]] }),
+ /options.0.0.val: Path `val` is required./
+ );
+ });
});
diff --git a/test/types.map.test.js b/test/types.map.test.js
index c08482819d2..c6486e507ab 100644
--- a/test/types.map.test.js
+++ b/test/types.map.test.js
@@ -6,7 +6,7 @@
const start = require('./common');
-const SchemaMapOptions = require('../lib/options/SchemaMapOptions');
+const SchemaMapOptions = require('../lib/options/schemaMapOptions');
const assert = require('assert');
const mongoose = start.mongoose;
@@ -1103,4 +1103,51 @@ describe('Map', function() {
assert.equal(doc.addresses.get('home').length, 1);
assert.equal(doc.addresses.get('home')[0].city, 'London');
});
+
+ it('clears nested changes in subdocs (gh-15108)', async function() {
+ const CarSchema = new mongoose.Schema({
+ owners: {
+ type: Map,
+ of: {
+ name: String
+ }
+ }
+ });
+ const CarModel = db.model('Car', CarSchema);
+ const car = await CarModel.create({
+ owners: { abc: { name: 'John' } }
+ });
+
+ car.owners.get('abc').name = undefined;
+ car.owners.delete('abc');
+ assert.deepStrictEqual(car.getChanges(), { $unset: { 'owners.abc': 1 } });
+ await car.save();
+
+ const doc = await CarModel.findById(car._id);
+ assert.strictEqual(doc.owners.get('abc'), undefined);
+ });
+
+ it('clears nested changes in doc arrays (gh-15108)', async function() {
+ const CarSchema = new mongoose.Schema({
+ owners: {
+ type: Map,
+ of: [{
+ _id: false,
+ name: String
+ }]
+ }
+ });
+ const CarModel = db.model('Car', CarSchema);
+ const car = await CarModel.create({
+ owners: { abc: [{ name: 'John' }] }
+ });
+
+ car.owners.get('abc')[0].name = undefined;
+ car.owners.set('abc', [{ name: 'Bill' }]);
+ assert.deepStrictEqual(car.getChanges(), { $inc: { __v: 1 }, $set: { 'owners.abc': [{ name: 'Bill' }] } });
+ await car.save();
+
+ const doc = await CarModel.findById(car._id);
+ assert.deepStrictEqual(doc.owners.get('abc').toObject(), [{ name: 'Bill' }]);
+ });
});
diff --git a/test/types.number.test.js b/test/types.number.test.js
index 34afe3bb055..a718f4d3a1f 100644
--- a/test/types.number.test.js
+++ b/test/types.number.test.js
@@ -23,7 +23,7 @@ describe('types.number', function() {
it('a null number should castForQuery to null', function(done) {
const n = new SchemaNumber();
- assert.strictEqual(n.castForQuery(null), null);
+ assert.strictEqual(n.castForQuery(null, null), null);
done();
});
@@ -104,7 +104,7 @@ describe('types.number', function() {
const n = new SchemaNumber();
let err;
try {
- n.castForQuery({ somePath: { $x: 43 } });
+ n.castForQuery(null, { somePath: { $x: 43 } });
} catch (e) {
err = e;
}
diff --git a/test/types/PipelineStage.test.ts b/test/types/PipelineStage.test.ts
index 967cf28b60d..8fc1991f846 100644
--- a/test/types/PipelineStage.test.ts
+++ b/test/types/PipelineStage.test.ts
@@ -217,6 +217,8 @@ const project14: PipelineStage = {
}
};
const project15: PipelineStage = { $project: { item: 1, result: { $not: [{ $gt: ['$qty', 250] }] } } };
+const project16: PipelineStage = { $project: { maxScores: { $maxN: { input: '$scores', n: 3 } } } };
+const project17: PipelineStage = { $project: { first3Scores: { $firstN: { input: '$scores', n: 3 } } } };
const sort1: PipelineStage = { $sort: { count: -1 } };
const sortByCount1: PipelineStage = { $sortByCount: '$tags' };
@@ -311,6 +313,21 @@ const setWindowFields4: PipelineStage = {
}
};
+const setWindowFields5: PipelineStage = {
+ $setWindowFields: {
+ partitionBy: '$gameId',
+ sortBy: { score: 1 },
+ output: {
+ minScores: {
+ $firstN: { input: '$score', n: 3 }
+ },
+ maxScores: {
+ $lastN: { input: '$score', n: 3 }
+ }
+ }
+ }
+};
+
const setWindowFieldsLinearFill: PipelineStage = {
$setWindowFields: {
partitionBy: '$stock',
@@ -419,6 +436,27 @@ const group5: PipelineStage = {
}
};
const group6: PipelineStage = { $group: { _id: '$author', books: { $push: '$title' } } };
+const group7: PipelineStage = {
+ $group: {
+ _id: '$gameId',
+ topPlayers: {
+ $topN: {
+ output: ['$playerId', '$score'],
+ sortBy: { score: -1 },
+ n: 3
+ }
+ },
+ bottomPlayers: {
+ $bottomN: {
+ output: ['$playerId', '$score'],
+ sortBy: { score: 1 },
+ n: 3
+ }
+ },
+ maxScores: { $maxN: { input: '$score', n: 3 } },
+ minScores: { $minN: { input: '$score', n: 3 } }
+ }
+};
const stages1: PipelineStage[] = [
// First Stage
@@ -520,3 +558,20 @@ function gh12269() {
}
};
}
+const vectorSearchStages: PipelineStage[] = [
+ {
+ $vectorSearch: {
+ index: 'title_vector_index',
+ path: 'embedding',
+ queryVector: [0.522, 0.123, 0.487],
+ limit: 5,
+ numCandidates: 100
+ }
+ },
+ {
+ $project: {
+ title: 1,
+ score: { $meta: 'searchScore' }
+ }
+ }
+];
diff --git a/test/types/aggregate.test.ts b/test/types/aggregate.test.ts
index 8d0d0bd16b3..c3f1472a0a3 100644
--- a/test/types/aggregate.test.ts
+++ b/test/types/aggregate.test.ts
@@ -3,12 +3,12 @@ import { expectType } from 'tsd';
const schema: Schema = new Schema({ name: { type: 'String' } });
-interface ITest extends Document {
+interface ITest {
name?: string;
}
-const Test = model('Test', schema);
-const AnotherTest = model('AnotherTest', schema);
+const Test = model('Test', schema);
+const AnotherTest = model('AnotherTest', schema);
Test.aggregate([{ $match: { name: 'foo' } }]).exec().then((res: any) => console.log(res));
@@ -136,3 +136,19 @@ function gh12311() {
}
};
}
+
+function gh13060() {
+ const schema = new Schema({ status: String });
+ const documentModel = model('Document', schema);
+
+ documentModel.aggregate([{
+ $group: {
+ _id: '$_id',
+ merged: {
+ $mergeObjects: {
+ status: '$status'
+ }
+ }
+ }
+ }]);
+}
diff --git a/test/types/base.test.ts b/test/types/base.test.ts
index 1dfaaa2ef70..9d25cfe30f3 100644
--- a/test/types/base.test.ts
+++ b/test/types/base.test.ts
@@ -8,6 +8,15 @@ Object.values(mongoose.models).forEach(model => {
mongoose.pluralize(null);
+mongoose.overwriteMiddlewareResult('foo');
+const schema = new mongoose.Schema({ name: String });
+schema.pre('save', function() {
+ return mongoose.skipMiddlewareFunction('foobar');
+});
+schema.post('save', function() {
+ return mongoose.overwriteMiddlewareResult('foobar');
+});
+
function gh10746() {
type A = string extends Function ? never : string;
@@ -60,3 +69,5 @@ function setAsObject() {
expectError(mongoose.set({ invalid: true }));
}
+
+const x: { name: string } = mongoose.omitUndefined({ name: 'foo' });
diff --git a/test/types/check-types-filename.js b/test/types/check-types-filename.js
index 8dc0f3f6de8..303c0b05dd0 100644
--- a/test/types/check-types-filename.js
+++ b/test/types/check-types-filename.js
@@ -18,7 +18,7 @@ const checkFolder = (folder) => {
}
continue;
} else {
- console.error('File ' + entry + ' is not having a valid file-extension.\n');
+ console.error('File ' + entry + ' does not have a valid extension, must be .d.ts or .gitignore.\n');
process.exit(1);
}
}
diff --git a/test/types/collection.test.ts b/test/types/collection.test.ts
index 2c9b623e3c9..f6c580e2af3 100644
--- a/test/types/collection.test.ts
+++ b/test/types/collection.test.ts
@@ -2,12 +2,12 @@ import { Schema, model, Document, connection, Collection } from 'mongoose';
const schema: Schema = new Schema({ name: { type: 'String' } });
-interface ITest extends Document {
+interface ITest {
name?: string;
age?: number;
}
-const Test = model('Test', schema);
+const Test = model('Test', schema);
Test.collection.collectionName;
Test.collection.findOne({});
diff --git a/test/types/connect.test.ts b/test/types/connect.test.ts
index 012ea7137c2..1e0dc92e528 100644
--- a/test/types/connect.test.ts
+++ b/test/types/connect.test.ts
@@ -5,14 +5,3 @@ import { expectType } from 'tsd';
expectType>(connect('mongodb://127.0.0.1:27017/test'));
expectType>(connect('mongodb://127.0.0.1:27017/test', {}));
expectType>(connect('mongodb://127.0.0.1:27017/test', { bufferCommands: true }));
-
-// Callback
-expectType(connect('mongodb://127.0.0.1:27017/test', (err: Error | null) => {
- return;
-}));
-expectType(connect('mongodb://127.0.0.1:27017/test', {}, (err: Error | null) => {
- return;
-}));
-expectType(connect('mongodb://127.0.0.1:27017/test', { bufferCommands: true }, (err: Error | null) => {
- return;
-}));
diff --git a/test/types/connection.test.ts b/test/types/connection.test.ts
index 0bd9e1636e0..29ddf02b3ba 100644
--- a/test/types/connection.test.ts
+++ b/test/types/connection.test.ts
@@ -6,7 +6,6 @@ import { AutoTypedSchemaType, autoTypedSchema } from './schema.test';
expectType(createConnection());
expectType(createConnection('mongodb://127.0.0.1:27017/test'));
expectType(createConnection('mongodb://127.0.0.1:27017/test', { appName: 'mongoose' }));
-expectType(createConnection('mongodb://127.0.0.1:27017/test', { appName: 'mongoose' }, (err, res) => (expectType(res))));
const conn = createConnection();
@@ -15,27 +14,22 @@ expectType>(conn.model<{ name: string }>('Test', new Sch
expectType>(conn.openUri('mongodb://127.0.0.1:27017/test'));
expectType>(conn.openUri('mongodb://127.0.0.1:27017/test', { bufferCommands: true }));
-expectType(conn.openUri('mongodb://127.0.0.1:27017/test', { bufferCommands: true }, (err, value) => {
- expectType(value);
-}));
conn.readyState === 0;
conn.readyState === 99;
expectError(conn.readyState = 0);
+expectType>>>(
+ conn.createCollections()
+);
+
expectType(new Connection());
expectType>(new Connection().asPromise());
expectType>>(conn.createCollection('some'));
-expectType(conn.createCollection('some', (err, res) => {
- expectType>(res);
-}));
expectType>(conn.dropCollection('some'));
-expectType(conn.dropCollection('some', () => {
- // do nothing
-}));
expectError(conn.deleteModel());
expectType(conn.deleteModel('something'));
@@ -45,75 +39,54 @@ expectType>(conn.modelNames());
expectType>(createConnection('mongodb://127.0.0.1:27017/test').close());
expectType>(createConnection('mongodb://127.0.0.1:27017/test').close(true));
-expectType(createConnection('mongodb://127.0.0.1:27017/test').close(() => {
- // do nothing.
-}));
-expectType(createConnection('mongodb://127.0.0.1:27017/test').close(true, () => {
- // do nothing.
-}));
-expectType(createConnection('mongodb://127.0.0.1:27017/test').close(false, () => {
- // do nothing.
-}));
-expectType(conn.db);
+expectType(conn.db);
expectType(conn.getClient());
expectType(conn.setClient(new mongodb.MongoClient('mongodb://127.0.0.1:27017/test')));
-expectType>(conn.transaction(async(res) => {
+expectType>(conn.transaction(async(res) => {
expectType(res);
return 'a';
}));
-expectType>(conn.transaction(async(res) => {
+expectType>(conn.transaction(async(res) => {
expectType(res);
return 'a';
}, { readConcern: 'majority' }));
+expectType>(conn.withSession(async(res) => {
+ expectType(res);
+ return 'a';
+}));
+
expectError(conn.user = 'invalid');
expectError(conn.pass = 'invalid');
expectError(conn.host = 'invalid');
expectError(conn.port = 'invalid');
expectType(conn.collection('test'));
-expectType(conn.db.collection('test'));
+expectType(conn.db?.collection('test'));
expectType>(conn.startSession());
expectType>(conn.startSession({ causalConsistency: true }));
-expectType(conn.startSession((err, res) => {
- expectType(res);
-}));
-expectType(conn.startSession(undefined, (err, res) => {
- expectType(res);
-}));
-expectType(conn.startSession(null, (err, res) => {
- expectType(res);
-}));
-expectType(conn.startSession({}, (err, res) => {
- expectType(res);
-}));
expectType>(conn.syncIndexes());
expectType>(conn.syncIndexes({ continueOnError: true }));
expectType>(conn.syncIndexes({ background: true }));
-expectType(conn.syncIndexes(undefined, (err, value) => {
- expectType(value);
-}));
-expectType(conn.syncIndexes(null, (err, value) => {
- expectType(value);
-}));
-expectType(conn.syncIndexes({ continueOnError: true }, (err, value) => {
- expectType(value);
-}));
-expectType(conn.syncIndexes({ background: true }, (err, value) => {
- expectType(value);
-}));
-
expectType(conn.useDb('test'));
expectType(conn.useDb('test', {}));
expectType(conn.useDb('test', { noListener: true }));
expectType(conn.useDb('test', { useCache: true }));
+expectType>(
+ conn.listCollections().then(collections => collections.map(coll => coll.name))
+);
+
+expectType>(
+ conn.listDatabases().then(dbs => dbs.databases.map(db => db.name))
+);
+
export function autoTypedModelConnection() {
const AutoTypedSchema = autoTypedSchema();
const AutoTypedModel = connection.model('AutoTypeModelConnection', AutoTypedSchema);
diff --git a/test/types/create.test.ts b/test/types/create.test.ts
index 033010ae9bb..618ce84a1cf 100644
--- a/test/types/create.test.ts
+++ b/test/types/create.test.ts
@@ -1,7 +1,7 @@
-import { Schema, model, Types, CallbackError } from 'mongoose';
+import { Schema, model, Types, HydratedDocument } from 'mongoose';
import { expectError, expectType } from 'tsd';
-const schema: Schema = new Schema({ name: { type: 'String' } });
+const schema = new Schema({ name: { type: 'String' } });
interface ITest {
_id?: Types.ObjectId;
@@ -40,74 +40,18 @@ Test.create([{ name: 'test' }], { validateBeforeSave: true }).then(docs => {
expectType(docs[0].name);
});
-
-Test.insertMany({ name: 'test' }, {}, (err, docs) => {
- expectType(err);
- expectType(docs[0]._id);
- expectType(docs[0].name);
- expectType(docs[0].isNew);
-});
-
-Test.insertMany({ name: 'test' }, { lean: true }, (err, docs) => {
- expectType(err);
- expectType(docs[0]._id);
- expectType(docs[0].name);
- expectError(docs[0].isNew);
-});
-
-Test.insertMany({ name: 'test' }, (err, docs) => {
- expectType(err);
- expectType(docs[0]._id);
- expectType(docs[0].name);
- expectType(docs[0].isNew);
-});
-
-Test.insertMany({ name: 'test' }, {}, (err, docs) => {
- expectType(err);
- expectType(docs[0]._id);
- expectType(docs[0].name);
- expectType(docs[0].isNew);
-});
-
-Test.insertMany([{ name: 'test' }], { rawResult: true }, (err, result) => {
- expectType(err);
- expectType(result.acknowledged);
- expectType(result.insertedCount);
- expectType<{ [key: number]: Types.ObjectId; }>(result.insertedIds);
-});
-
-Test.insertMany([{ name: 'test' }], { rawResult: true }, (err, result) => {
- expectType(err);
- expectType(result.acknowledged);
- expectType(result.insertedCount);
- expectType<{ [key: number]: Types.ObjectId; }>(result.insertedIds);
-});
-
-Test.insertMany([{ name: 'test' }], { lean: true }, (err, docs) => {
- expectType(err);
- expectType(docs[0]._id);
- expectType(docs[0].name);
- expectError(docs[0].isNew);
+Test.create({}).then(doc => {
+ expectType(doc.name);
});
-Test.insertMany([{ name: 'test' }], (err, docs) => {
- expectType(err);
- expectType(docs[0]._id);
+Test.create([{}]).then(docs => {
expectType(docs[0].name);
- expectType(docs[0].isNew);
});
-Test.insertMany({ _id: '000000000000000000000000', name: 'test' }, (err, docs) => {
- expectType(docs[0]._id);
- expectType(docs[0].name);
- expectType(docs[0].isNew);
-});
+expectError(Test.create({}));
-Test.insertMany({ _id: new Types.ObjectId('000000000000000000000000') }, (err, docs) => {
- expectType(docs[0]._id);
- expectType(docs[0].name);
- expectType(docs[0].isNew);
-});
+Test.create({ name: 'test' });
+Test.create({ _id: new Types.ObjectId('0'.repeat(24)), name: 'test' });
Test.insertMany({ name: 'test' }, {}).then(docs => {
expectType(docs[0]._id);
@@ -187,3 +131,10 @@ Test.insertMany({ _id: new Types.ObjectId('000000000000000000000000'), name: 'te
(await Test.create([{ name: 'test' }]))[0];
(await Test.create({ name: 'test' }))._id;
})();
+
+async function createWithAggregateErrors() {
+ expectType<(HydratedDocument)[]>(await Test.create([{}]));
+ expectType<(HydratedDocument | Error)[]>(await Test.create([{}], { aggregateErrors: true }));
+}
+
+createWithAggregateErrors();
diff --git a/test/types/discriminator.test.ts b/test/types/discriminator.test.ts
index 0519345322c..fa8e9a3a1b0 100644
--- a/test/types/discriminator.test.ts
+++ b/test/types/discriminator.test.ts
@@ -2,7 +2,7 @@ import mongoose, { Document, Model, Schema, SchemaDefinition, SchemaOptions, Typ
const schema: Schema = new Schema({ name: { type: 'String' } });
-interface IBaseTest extends Document {
+interface IBaseTest {
name?: string;
}
@@ -32,7 +32,7 @@ function test(): void {
Land = 'land',
}
- interface CardDb extends Document {
+ interface CardDb {
_id: Types.ObjectId;
type: CardType;
}
diff --git a/test/types/docArray.test.ts b/test/types/docArray.test.ts
index 39417583342..17ab708a967 100644
--- a/test/types/docArray.test.ts
+++ b/test/types/docArray.test.ts
@@ -1,41 +1,10 @@
-import { Schema, model, Document, Types, LeanDocument } from 'mongoose';
+import { Schema, model, Model, Types, InferSchemaType } from 'mongoose';
import { expectError, expectType } from 'tsd';
-const schema: Schema = new Schema({ tags: [new Schema({ name: String })] });
-
-interface Subdoc extends Document {
- name: string
-}
-
-interface ITest extends Document {
- tags: Types.DocumentArray
-}
-
-const Test = model('Test', schema);
-
-void async function main() {
- const doc: ITest = await Test.findOne().orFail();
-
- doc.tags = new Types.DocumentArray([]);
- doc.set('tags', []);
-
- const record: Subdoc = doc.tags.create({ name: 'test' });
- doc.tags.push(record);
-
- doc.tags.push({ name: 'test' });
-
- await doc.save();
-
- const _doc: LeanDocument = await Test.findOne().orFail().lean();
- _doc.tags[0].name.substring(1);
- expectError(_doc.tags.create({ name: 'fail' }));
-}();
-
-// https://github.com/Automattic/mongoose/issues/10293
async function gh10293() {
interface ITest {
name: string;
- arrayOfArray: Types.Array; // <-- Array of Array
+ arrayOfArray: string[][]; // <-- Array of Array
}
const testSchema = new Schema({
@@ -55,3 +24,158 @@ async function gh10293() {
return test.arrayOfArray; // <-- error here if the issue persisted
};
}
+
+function gh13087() {
+ interface Book {
+ author: {
+ name: string;
+ };
+ }
+
+ expectError(new Types.DocumentArray([1, 2, 3]));
+
+ const locationSchema = new Schema(
+ {
+ type: {
+ required: true,
+ type: String,
+ enum: ['Point']
+ },
+ coordinates: {
+ required: true,
+ type: [Number] // [longitude, latitude]
+ }
+ },
+ { _id: false }
+ );
+
+ const pointSchema = new Schema({
+ name: { required: true, type: String },
+ location: { required: true, type: locationSchema }
+ });
+
+ const routeSchema = new Schema({
+ points: { type: [pointSchema] }
+ });
+
+ type Route = InferSchemaType;
+
+ function getTestRouteData(): Route {
+ return {
+ points: new Types.DocumentArray([
+ { name: 'Test', location: { type: 'Point', coordinates: [1, 2] } }
+ ])
+ };
+ }
+
+ const { points } = getTestRouteData();
+ expectType>(points);
+}
+
+async function gh13424() {
+ const subDoc = {
+ name: { type: String, required: true },
+ controls: { type: String, required: true }
+ };
+
+ const testSchema = new Schema({
+ question: { type: String, required: true },
+ subDocArray: { type: [subDoc], required: true }
+ });
+ const TestModel = model('Test', testSchema);
+
+ const doc = new TestModel();
+ expectType(doc.subDocArray[0]._id);
+}
+
+async function gh14367() {
+ const UserSchema = new Schema(
+ {
+ reminders: {
+ type: [
+ {
+ type: { type: Schema.Types.String },
+ date: { type: Schema.Types.Date },
+ toggle: { type: Schema.Types.Boolean },
+ notified: { type: Schema.Types.Boolean }
+ }
+ ],
+ default: [
+ { type: 'vote', date: new Date(), toggle: false, notified: false },
+ { type: 'daily', date: new Date(), toggle: false, notified: false },
+ { type: 'drop', date: new Date(), toggle: false, notified: false },
+ { type: 'claim', date: new Date(), toggle: false, notified: false },
+ { type: 'work', date: new Date(), toggle: false, notified: false }
+ ]
+ },
+ avatar: {
+ type: Schema.Types.String
+ }
+ },
+ { timestamps: true }
+ );
+
+ type IUser = InferSchemaType;
+ expectType({} as IUser['reminders'][0]['type']);
+ expectType({} as IUser['reminders'][0]['date']);
+ expectType({} as IUser['reminders'][0]['toggle']);
+ expectType({} as IUser['avatar']);
+}
+
+function gh14469() {
+ interface Names {
+ _id: Types.ObjectId;
+ firstName: string;
+ }
+ // Document definition
+ interface User {
+ names: Names[];
+ }
+
+ // TMethodsAndOverrides
+ type UserDocumentProps = {
+ names: Types.DocumentArray;
+ };
+ type UserModelType = Model;
+
+ const userSchema = new Schema(
+ {
+ names: [new Schema({ firstName: String })]
+ },
+ { timestamps: true }
+ );
+
+ // Create model
+ const UserModel = model('User', userSchema);
+
+ const doc = new UserModel({ names: [{ firstName: 'John' }] });
+
+ const jsonDoc = doc?.toJSON();
+ expectType(jsonDoc?.names[0]?.firstName);
+
+ const jsonNames = doc?.names[0]?.toJSON();
+ expectType(jsonNames?.firstName);
+}
+
+function gh15041() {
+ const subDoc = {
+ name: { type: String, required: true },
+ age: { type: Number, required: true }
+ };
+
+ const testSchema = new Schema({
+ subdocArray: { type: [subDoc], required: true }
+ });
+
+ const TestModel = model('Test', testSchema);
+
+ const doc = new TestModel({ subdocArray: [{ name: 'John', age: 30 }] });
+ type TestModelDoc = ReturnType<(typeof TestModel)['hydrate']>
+ expectType(doc.subdocArray.splice(0, 1, { name: 'Bill' }));
+}
diff --git a/test/types/document.test.ts b/test/types/document.test.ts
index 95d3ae3bc5c..19ca1083296 100644
--- a/test/types/document.test.ts
+++ b/test/types/document.test.ts
@@ -1,5 +1,16 @@
-import { Schema, model, Model, Document, Types } from 'mongoose';
-import { expectAssignable, expectError, expectType } from 'tsd';
+import {
+ Schema,
+ model,
+ Model,
+ Query,
+ Types,
+ HydratedDocument,
+ HydratedArraySubdocument,
+ HydratedSingleSubdocument,
+ DefaultSchemaOptions
+} from 'mongoose';
+import { DeleteResult } from 'mongodb';
+import { expectAssignable, expectError, expectNotAssignable, expectType } from 'tsd';
import { autoTypedModel } from './models.test';
import { autoTypedModelConnection } from './connection.test';
import { AutoTypedSchemaType } from './schema.test';
@@ -21,22 +32,18 @@ interface ITestBase {
name?: string;
}
-interface ITest extends ITestBase, Document {}
+type ITest = ITestBase;
+type TestDocument = ReturnType