diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 8f45a0266f3..42f2d32a09d 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -22,9 +22,7 @@ jobs: runs-on: ubuntu-22.04 name: Benchmark TypeScript Types steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - with: - fetch-depth: 0 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Setup node uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index fee27e5a05f..08808daa1b9 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 5e25e1c99d2..3f2fda2ccce 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -28,7 +28,7 @@ jobs: runs-on: ubuntu-latest name: Lint Markdown files steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Setup node uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 @@ -48,7 +48,7 @@ jobs: runs-on: ubuntu-22.04 name: Test Generating Docs steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - run: git fetch --depth=1 --tags # download all tags for documentation - name: Setup node diff --git a/.github/workflows/encryption-tests.yml b/.github/workflows/encryption-tests.yml index 886624fd9e4..9646791d735 100644 --- a/.github/workflows/encryption-tests.yml +++ b/.github/workflows/encryption-tests.yml @@ -36,7 +36,7 @@ jobs: env: FORCE_COLOR: true steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Setup node uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 92fa084faa2..933ca546bfb 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -13,7 +13,7 @@ jobs: id-token: write steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6.0.0 - name: Setup Node.js uses: actions/setup-node@v6 @@ -24,7 +24,7 @@ jobs: - name: Install dependencies run: npm install - - name: Dry run publish with provenance + - name: Publish with provenance run: npm publish --provenance --access public env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c3e923ac4e2..8c4c1e53bea 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest name: Lint JS-Files steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Setup node uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 @@ -39,7 +39,7 @@ jobs: strategy: fail-fast: false matrix: - node: [18, 20, 22, 24] + node: [20, 22, 24] os: [ubuntu-22.04, ubuntu-24.04] mongodb: [6.0.15, 7.0.12, 8.2.0] include: @@ -53,7 +53,7 @@ jobs: MONGOMS_PREFER_GLOBAL_PATH: 1 FORCE_COLOR: true steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Setup node uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 @@ -85,7 +85,7 @@ jobs: MONGOMS_PREFER_GLOBAL_PATH: 1 FORCE_COLOR: true steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Setup node uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: @@ -112,9 +112,9 @@ jobs: IS_ATLAS: true MONGOOSE_TEST_URI: mongodb://127.0.0.1:27017/mongoose_test?directConnection=true steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Setup AtlasCLI - uses: mongodb/atlas-github-action@v0.2.1 + uses: mongodb/atlas-github-action@v0.2.2 - name: Setup local Atlas deployment run: atlas deployments setup mycluster --type local --force --port 27017 - name: Setup node @@ -135,7 +135,7 @@ jobs: env: FORCE_COLOR: true steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Setup node uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: @@ -152,6 +152,6 @@ jobs: contents: read steps: - name: Check out repo - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Dependency review uses: actions/dependency-review-action@v4 diff --git a/.github/workflows/tidelift-alignment.yml b/.github/workflows/tidelift-alignment.yml index aa3a56a4895..1fa886463dd 100644 --- a/.github/workflows/tidelift-alignment.yml +++ b/.github/workflows/tidelift-alignment.yml @@ -15,7 +15,7 @@ jobs: if: github.repository == 'Automattic/mongoose' steps: - name: Checkout - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Setup node uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: diff --git a/.github/workflows/tsd.yml b/.github/workflows/tsd.yml index fcdc7889470..068b48eeabf 100644 --- a/.github/workflows/tsd.yml +++ b/.github/workflows/tsd.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest name: Lint TS-Files steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Setup node uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 @@ -38,7 +38,7 @@ jobs: runs-on: ubuntu-latest name: Test Typescript Types steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Setup node uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 diff --git a/.gitignore b/.gitignore index 9b49b5d8695..afa0893602d 100644 --- a/.gitignore +++ b/.gitignore @@ -76,3 +76,4 @@ fle-cluster-config.json # @ark/attest (type testing) .attest +.worktrees/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b908ad9377..2dc71933fc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,53 @@ +8.20.4 / 2025-12-18 +=================== + * fix(model): ensure $isDeleted is set after calling doc.deleteOne() successfully #15898 + * fix(document): use bitwise OR to accumulate version mode flags #15893 #15888 [AbdelrahmanHafez](https://github.com/AbdelrahmanHafez) + +9.0.2 / 2025-12-17 +================== + * fix(model): trigger error post hook on bulkwrite when pre-hook throws an error #15882 [AbdelrahmanHafez](https://github.com/AbdelrahmanHafez) + * fix(document): use bitwise OR to accumulate version mode flags #15893 [AbdelrahmanHafez](https://github.com/AbdelrahmanHafez) + * types(queries): apply Mongoose casting to default MongoDB driver _id in RootFilterOperators #15891 #15887 #15779 + * types(schema): correctly infer virtuals, methods on hydrated doc type from schema options #15892 + * types: fixed this parameter type detection for methods with arguments #15885 [I-Enderlord-I](https://github.com/I-Enderlord-I) + * types: export InferRawDocTypeWithout_id to replicate Mongoose 8 InferRawDocType behavior #15815 #15814 [JavaScriptBach](https://github.com/JavaScriptBach) + * docs: improve colors on dark mode #15879 [AbdelrahmanHafez](https://github.com/AbdelrahmanHafez) + * docs(model): add overwriteImmutable option #15884 [AbdelrahmanHafez](https://github.com/AbdelrahmanHafez) + * refactor: remove internal callbacks for buffering #15890 + +8.20.3 / 2025-12-15 +=================== + * perf: use Object.hasOwn instead of Object#hasOwnProperty #15875 [AbdelrahmanHafez](https://github.com/AbdelrahmanHafez) + * fix: improve error when calling Document.prototype.init() with null/undefined #15812 [Vegapunk-debug](https://github.com/Vegapunk-debug) + * types(schema): avoid treating paths with default: null as required #15889 + * types(schema): allow partial statics to schema.statics() #15780 + +9.0.1 / 2025-12-05 +================== + * perf: use native Buffer.equals() for buffer comparison #15821 [AbdelrahmanHafez](https://github.com/AbdelrahmanHafez) + * fix(model): fix overwriteImmutable not working with timestamps: true, add overwriteImmutable types re #15781 #15819 [AbdelrahmanHafez](https://github.com/AbdelrahmanHafez) + * fix(bulkWrite): pass overwriteImmutable option to castUpdate fixes #15782 #15781 [jhaayushkumar](https://github.com/jhaayushkumar) + * fix(schema): Add enumValues property to Number enum for consistency with String enum #15824 [AkaHarshit](https://github.com/AkaHarshit) + * fix: incorrect variable bug in double casting #15849 #15848 [lomesh2312](https://github.com/lomesh2312) + * fix: clear timeout in collection operations #15852 [techcodie](https://github.com/techcodie) + * types(query+model): use function overrides instead of | Query to support using Query as filter #15791 #15779 + * docs(migrating_to_9): clarify removing next() from pre middleware #15813 + * docs: add dark mode support and CSS improvements #15753 + * docs: Mongoose compatibility page updates #15797 [alexbevi](https://github.com/alexbevi) + * docs: Add closing backticks to code block in migration guide #15783 [isnifer](https://github.com/isnifer) + * docs: fix documentation link in connection.js #15804 [salittle0](https://github.com/salittle0) + +8.20.2 / 2025-12-05 +=================== + * fix(model): bump version if necessary after successful bulkSave() #15809 #15800 + * fix(bulkWrite): pass overwriteImmutable option to castUpdate fixes #15789 #15782 #15781 + * types(schema): allow calling schema.static() with as TStatics #15794 #15780 + +7.8.8 / 2025-12-04 +================== + * fix(bulkWrite): pass overwriteImmutable option to castUpdate fixes #15789 #15782 #15781 + * fix(model): bump version if necessary after successful bulkSave() #15800 + 9.0.0 / 2025-11-21 ================== * BREAKING CHANGE: drop support for callback-based pre middleware, e.g. `next()` in `pre()` hooks diff --git a/docs/css/api.css b/docs/css/api.css index a2f30f6c76e..997c39ec0e0 100644 --- a/docs/css/api.css +++ b/docs/css/api.css @@ -12,6 +12,28 @@ height: 100%; padding-bottom: 10px; overflow-y: auto; + background-color: var(--menu-bg, #fafafa); + border-left: 1px solid var(--border-color, #ddd); + scrollbar-width: thin; + scrollbar-color: var(--border-color, #ddd) var(--menu-bg, #fafafa); + transition: background-color 0.3s ease, border-color 0.3s ease; +} + +.api-nav::-webkit-scrollbar { + width: 8px; +} + +.api-nav::-webkit-scrollbar-track { + background: var(--menu-bg, #fafafa); +} + +.api-nav::-webkit-scrollbar-thumb { + background-color: var(--border-color, #ddd); + border-radius: 4px; +} + +.api-nav::-webkit-scrollbar-thumb:hover { + background-color: var(--text-muted, #777); } .api-nav .nav-item-title { @@ -24,7 +46,18 @@ } .api-nav a { - color: #777; + color: var(--text-muted, #777); + transition: color 0.2s ease; +} + +.api-nav a:hover { + color: var(--link-color, #0971B2); +} + +.api-nav a:focus-visible { + outline: 3px solid var(--focus-ring, #0971B2); + outline-offset: 2px; + border-radius: 2px; } .api-nav .nav-item-sub { @@ -54,12 +87,47 @@ margin-top: 3em; } +/* Responsive API navigation */ @media (max-width: 1785px) { .api-nav { display: none; } } +/* Tablet: Show API nav as collapsible sidebar */ +@media (min-width: 1400px) and (max-width: 1785px) { + .api-nav { + display: block; + left: auto; + right: 0; + width: 280px; + z-index: 10; + } +} + +/* Mobile: API nav as bottom sheet or hidden */ +@media (max-width: 1400px) { + .api-nav { + display: none; + } + + /* Optionally show as bottom sheet on mobile */ + .api-nav.mobile-open { + display: block; + position: fixed; + bottom: 0; + left: 0; + right: 0; + top: auto; + height: 50vh; + width: 100%; + border-left: none; + border-top: 1px solid var(--border-color, #ddd); + border-radius: 8px 8px 0 0; + box-shadow: 0 -2px 8px var(--shadow, rgba(0, 0, 0, 0.1)); + } +} + ul { margin-top: -10px; } @@ -166,4 +234,54 @@ hr.separate-api { .deprecated { color: #ff0000; + font-weight: 600; +} + +/* Accessibility improvements */ +.method-type:focus-visible, +.api-nav .nav-item-title:focus-visible { + outline: 3px solid var(--focus-ring, #0971B2); + outline-offset: 2px; + border-radius: 2px; +} + +/* Dark mode support for API page */ +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) .api-nav { + background-color: var(--menu-bg); + border-left-color: var(--border-color); + } + :root:not([data-theme="light"]) .api-nav a { + color: var(--text-muted); + } + :root:not([data-theme="light"]) .api-nav a:hover { + color: var(--link-color); + } + :root:not([data-theme="light"]) .native-ad { + background-color: var(--bg-secondary); + } + :root:not([data-theme="light"]) .native-ad a { + color: var(--text-primary); + } +} + +[data-theme="dark"] .api-nav { + background-color: var(--menu-bg); + border-left-color: var(--border-color); +} + +[data-theme="dark"] .api-nav a { + color: var(--text-muted); +} + +[data-theme="dark"] .api-nav a:hover { + color: var(--link-color); +} + +[data-theme="dark"] .native-ad { + background-color: var(--bg-secondary); +} + +[data-theme="dark"] .native-ad a { + color: var(--text-primary); } diff --git a/docs/css/github.css b/docs/css/github.css index f8cec1bdc3f..36e77e2183e 100644 --- a/docs/css/github.css +++ b/docs/css/github.css @@ -1,31 +1,47 @@ code { - background-color: #eee; - padding: 2px 4px; + background-color: var(--code-bg, #eee); + padding: 2px 6px; font-size: 0.9em; - color: #800; + color: var(--link-color, #800); border-radius: 4px; + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; + transition: background-color 0.3s ease, color 0.3s ease; } pre code { background-color: transparent; padding: 0; font-size: 1em; - color: #222; + color: var(--code-text, #222); + line-height: 1.6; + display: block; } pre { display: block; - padding: 9.5px; - margin: 10px 0 10px; + padding: 12px 16px; + margin: 16px 0; 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; + line-height: 1.6; + color: var(--code-text, #333); + word-break: break-word; + overflow-wrap: break-word; + overflow-x: auto; + background-color: var(--code-bg, #f5f5f5); + border: 1px solid var(--border-color, #ccc); + border-radius: 6px; + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; + box-shadow: 0 1px 3px var(--shadow, rgba(0, 0, 0, 0.1)); + transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease; +} + +/* Enable absolute positioning for copy button and improve hover effect */ +pre { + position: relative; +} + +pre:hover { + box-shadow: 0 2px 6px var(--shadow, rgba(0, 0, 0, 0.15)); } /* @@ -38,9 +54,11 @@ github.com style (c) Vasily Polovnyov display: block; overflow-x: auto; padding: 0.5em; - color: #333; - background: #f8f8f8; + color: var(--code-text, #333); + background: var(--code-bg, #f8f8f8); -webkit-text-size-adjust: none; + transition: background-color 0.3s ease, color 0.3s ease; + line-height: 1.6; } .hljs-comment, @@ -152,3 +170,180 @@ github.com style (c) Vasily Polovnyov .hljs-chunk { color: #aaa; } + +/* Dark mode support for syntax highlighting */ +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) code { + background-color: var(--code-bg); + color: var(--link-color); + } + :root:not([data-theme="light"]) pre { + background-color: var(--code-bg); + border-color: var(--border-color); + color: var(--code-text); + } + :root:not([data-theme="light"]) .hljs { + background: var(--code-bg); + color: var(--code-text); + } +} + +[data-theme="dark"] code { + background-color: var(--code-bg); + color: var(--link-color); +} + +[data-theme="dark"] pre { + background-color: var(--code-bg); + border-color: var(--border-color); + color: var(--code-text); +} + +[data-theme="dark"] .hljs { + background: var(--code-bg); + color: var(--code-text); +} + +[data-theme="dark"] .hljs-comment, +[data-theme="dark"] .diff .hljs-header, +[data-theme="dark"] .hljs-javadoc { + color: #6a9955; +} + +[data-theme="dark"] .hljs-keyword, +[data-theme="dark"] .css .rule .hljs-keyword, +[data-theme="dark"] .hljs-winutils, +[data-theme="dark"] .nginx .hljs-title, +[data-theme="dark"] .hljs-subst, +[data-theme="dark"] .hljs-request, +[data-theme="dark"] .hljs-status { + color: #569cd6; + font-weight: bold; +} + +[data-theme="dark"] .hljs-number, +[data-theme="dark"] .hljs-hexcolor, +[data-theme="dark"] .ruby .hljs-constant { + color: #b5cea8; +} + +[data-theme="dark"] .hljs-string, +[data-theme="dark"] .hljs-tag .hljs-value, +[data-theme="dark"] .hljs-phpdoc, +[data-theme="dark"] .hljs-dartdoc, +[data-theme="dark"] .tex .hljs-formula { + color: #ce9178; +} + +[data-theme="dark"] .hljs-title, +[data-theme="dark"] .hljs-id, +[data-theme="dark"] .scss .hljs-preprocessor { + color: #d7ba7d; + font-weight: bold; +} + +[data-theme="dark"] .hljs-class .hljs-title, +[data-theme="dark"] .hljs-type, +[data-theme="dark"] .vhdl .hljs-literal, +[data-theme="dark"] .tex .hljs-command { + color: #4ec9b0; + font-weight: bold; +} + +[data-theme="dark"] .hljs-tag, +[data-theme="dark"] .hljs-tag .hljs-title, +[data-theme="dark"] .hljs-rules .hljs-property, +[data-theme="dark"] .django .hljs-tag .hljs-keyword { + color: #569cd6; + font-weight: normal; +} + +[data-theme="dark"] .hljs-attribute, +[data-theme="dark"] .hljs-variable, +[data-theme="dark"] .lisp .hljs-body { + color: #9cdcfe; +} + +[data-theme="dark"] .hljs-regexp { + color: #d16969; +} + +[data-theme="dark"] .hljs-symbol, +[data-theme="dark"] .ruby .hljs-symbol .hljs-string, +[data-theme="dark"] .lisp .hljs-keyword, +[data-theme="dark"] .clojure .hljs-keyword, +[data-theme="dark"] .scheme .hljs-keyword, +[data-theme="dark"] .tex .hljs-special, +[data-theme="dark"] .hljs-prompt { + color: #c586c0; +} + +[data-theme="dark"] .hljs-built_in { + color: #4fc1ff; +} + +[data-theme="dark"] .hljs-preprocessor, +[data-theme="dark"] .hljs-pragma, +[data-theme="dark"] .hljs-pi, +[data-theme="dark"] .hljs-doctype, +[data-theme="dark"] .hljs-shebang, +[data-theme="dark"] .hljs-cdata { + color: #808080; + font-weight: bold; +} + +[data-theme="dark"] .hljs-deletion { + background: #5a1d1d; + color: #f48771; +} + +[data-theme="dark"] .hljs-addition { + background: #1e3a1e; + color: #b5cea8; +} + +[data-theme="dark"] .diff .hljs-change { + background: #2d4d2d; + color: #4ec9b0; +} + +[data-theme="dark"] .hljs-chunk { + color: #808080; +} + +/* Accessibility: Ensure sufficient contrast for code */ +code, +pre, +.hljs { + /* WCAG AA compliant contrast ratios */ + min-height: 1.5em; +} + +/* Improve code block readability on small screens */ +@media (max-width: 768px) { + pre { + font-size: 12px; + padding: 10px; + margin: 12px 0; + } + + code { + font-size: 0.85em; + padding: 2px 4px; + } +} + +/* Print styles for code blocks */ +@media print { + pre { + border: 1px solid #ccc; + page-break-inside: avoid; + background: #f5f5f5; + color: #000; + } + + code { + background: #f0f0f0; + color: #000; + } +} diff --git a/docs/css/mongoose5.css b/docs/css/mongoose5.css index 4ae64f50ddf..1ea4b374bf8 100644 --- a/docs/css/mongoose5.css +++ b/docs/css/mongoose5.css @@ -1,9 +1,107 @@ +/* CSS Custom Properties for Theming */ +/* Light mode (default) */ +:root, [data-theme="light"] { + --bg-primary: #ffffff; + --bg-secondary: #eee; + --bg-tertiary: #fafafa; + --text-primary: #000000; + --text-secondary: #333333; + --text-muted: #777777; + --link-color: #0971B2; + --link-hover: #065a8f; + --border-color: #ddd; + --code-bg: #f5f5f5; + --code-text: #333; + --menu-bg: #eee; + --menu-hover: rgba(0, 0, 0, 0.1); + --menu-selected: rgba(0, 0, 0, 0.15); + --shadow: rgba(0, 0, 0, 0.1); + --focus-ring: #0971B2; + --focus-ring-width: 3px; + --focus-ring-shadow: rgba(9, 113, 178, 0.1); + /* Brand colors */ + --brand-primary: #800; + --brand-primary-hover: #990000; + --brand-primary-subtle: rgba(136, 0, 0, 0.1); + /* Button colors */ + --btn-bg: #444; + --btn-text-shadow: #222; + /* Tagline text shadow */ + --tagline-text-shadow: #f8f8f8; +} + +/* Dark mode - system preference (no-JS fallback) or manual toggle */ +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) { + --bg-primary: #1a1a1a; + --bg-secondary: #252525; + --bg-tertiary: #252525; + --text-primary: #f0f0f0; + --text-secondary: #d8d8d8; + --text-muted: #a0a0a0; + --link-color: #7BC3FF; + --link-hover: #8AD0FF; + --border-color: #444444; + --code-bg: #252525; + --code-text: #e8e8e8; + --menu-bg: #252525; + --menu-hover: rgba(255, 255, 255, 0.1); + --menu-selected: rgba(255, 255, 255, 0.15); + --shadow: rgba(0, 0, 0, 0.3); + --focus-ring: #FFB4B4; + --focus-ring-shadow: rgba(255, 180, 180, 0.2); + /* Brand colors */ + --brand-primary: #FF5E5E; + --brand-primary-hover: #F55; + --brand-primary-subtle: rgba(255, 180, 180, 0.1); + /* Button colors */ + --btn-bg: #444; + --btn-text-shadow: #222; + /* Tagline text shadow */ + --tagline-text-shadow: #222; + } +} + +/* Dark mode - manual toggle (overrides system preference) */ +[data-theme="dark"] { + --bg-primary: #1a1a1a; + --bg-secondary: #2d2d2d; + --bg-tertiary: #252525; + --text-primary: #f0f0f0; + --text-secondary: #d8d8d8; + --text-muted: #a0a0a0; + --link-color: #7BC3FF; + --link-hover: #8AD0FF; + --border-color: #444444; + --code-bg: #252525; + --code-text: #e8e8e8; + --menu-bg: #252525; + --menu-hover: rgba(255, 255, 255, 0.1); + --menu-selected: rgba(255, 255, 255, 0.15); + --shadow: rgba(0, 0, 0, 0.3); + --focus-ring: #FFB4B4; + --focus-ring-shadow: rgba(255, 180, 180, 0.2); + /* Brand colors */ + --brand-primary: #FF5E5E; + --brand-primary-hover: #F55; + --brand-primary-subtle: rgba(255, 180, 180, 0.1); + /* Button colors */ + --btn-bg: #444; + --btn-text-shadow: #222; + /* Tagline text shadow */ + --tagline-text-shadow: #222; +} + html { font-family: 'Open Sans'; + color-scheme: light dark; } body { margin: 0; + background-color: var(--bg-primary); + color: var(--text-primary); + transition: background-color 0.3s ease, color 0.3s ease; } img { @@ -12,7 +110,19 @@ img { a { text-decoration: none; - color: #0971B2; + color: var(--link-color); + transition: color 0.2s ease, opacity 0.2s ease; +} + +a:hover { + color: var(--link-hover); + opacity: 0.9; +} + +a:focus-visible { + outline: var(--focus-ring-width) solid var(--focus-ring); + outline-offset: 2px; + border-radius: 2px; } p { @@ -51,18 +161,37 @@ h1 a, h2 a, h3 a, h4 a { - color: #000; + color: var(--text-primary); +} + +h1 a:focus-visible, +h2 a:focus-visible, +h3 a:focus-visible, +h4 a:focus-visible { + outline: var(--focus-ring-width) solid var(--focus-ring); + outline-offset: 2px; + border-radius: 2px; } #logo { + display: inline-block; width: 62px; height: 30px; position: relative; - top: 5px; + top: 7px; + background-color: var(--brand-primary); + -webkit-mask-image: url('/docs/images/mongoose.svg'); + mask-image: url('/docs/images/mongoose.svg'); + -webkit-mask-size: 100% 100%; + mask-size: 100% 100%; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-position: center; + mask-position: center; } .logo-text { - color: #800; + color: var(--brand-primary); font-size: 20pt; position: relative; top: 0px; @@ -70,6 +199,7 @@ h4 a { text-transform: none; } + .pure-menu-item { font-size: 12pt; padding-top: 0px; @@ -78,6 +208,11 @@ h4 a { .pure-menu-link { padding-top: 2px; padding-bottom: 2px; + color: var(--text-secondary); +} + +.pure-menu-link:hover { + color: var(--text-primary); } /* change sub-item lists to be more dense */ @@ -86,8 +221,19 @@ h4 a { padding-bottom: 0px; } -.pure-menu-link:hover, .pure-menu-link.selected { - background-color: rgba(0,0,0, 0.1); +.pure-menu-link:hover, +.pure-menu-link.selected { + background-color: var(--menu-hover); +} + +.pure-menu-link:focus-visible { + outline: var(--focus-ring-width) solid var(--focus-ring); + outline-offset: -2px; + border-radius: 2px; +} + +.pure-menu-link.selected { + background-color: var(--menu-selected); } li.sub-item { @@ -96,18 +242,19 @@ li.sub-item { } li.version { - border-bottom: 1px solid #ddd; + border-bottom: 1px solid var(--border-color); padding-bottom: 4px; } li.version ul.pure-menu-children { - border: 1px solid #ddd; + border: 1px solid var(--border-color); + background-color: var(--menu-bg); } #logo-container { padding-top: 0; padding-bottom: 6px; - border-bottom: 1px solid #ddd; + border-bottom: 1px solid var(--border-color); } #logo-container > a { @@ -120,10 +267,29 @@ li.version ul.pure-menu-children { top: 0; left: 0; bottom: 0px; - background-color: #eee; + background-color: var(--menu-bg); width: 250px; - border-right: 1px solid #ddd; + border-right: 1px solid var(--border-color); overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: var(--border-color) var(--menu-bg); +} + +#menu::-webkit-scrollbar { + width: 8px; +} + +#menu::-webkit-scrollbar-track { + background: var(--menu-bg); +} + +#menu::-webkit-scrollbar-thumb { + background-color: var(--border-color); + border-radius: 4px; +} + +#menu::-webkit-scrollbar-thumb:hover { + background-color: var(--text-muted); } .container { @@ -143,11 +309,20 @@ li.version ul.pure-menu-children { } .search input { - border: 1px solid #ddd; + border: 1px solid var(--border-color); padding: 0.25em; width: 170px; border-radius: 3px; flex: 1; + background-color: var(--bg-primary); + color: var(--text-primary); + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.search input:focus { + outline: none; + border-color: var(--focus-ring); + box-shadow: 0 0 0 2px var(--focus-ring-shadow); } #search-input-nav { @@ -155,14 +330,29 @@ li.version ul.pure-menu-children { } .search button { - background-color: #777; - color: white; + background-color: var(--text-muted); + color: var(--bg-primary); border: 1px solid transparent; border-radius: 3px; height: 30px; width: 30px; padding: 0px; flex: 0 0 auto; + cursor: pointer; + transition: background-color 0.2s ease, transform 0.1s ease; +} + +.search button:hover { + background-color: var(--text-secondary); +} + +.search button:active { + transform: scale(0.95); +} + +.search button:focus-visible { + outline: var(--focus-ring-width) solid var(--focus-ring); + outline-offset: 2px; } #search-button-nav { @@ -179,11 +369,21 @@ li.version ul.pure-menu-children { position: absolute; top: 0.125em; right: 0px; - background-color: #fafafa; - border: 1px solid #ddd; + background-color: var(--bg-tertiary); + border: 1px solid var(--border-color); padding: 5px; padding-bottom: 0px; border-radius: 3px; + transition: background-color 0.2s ease, border-color 0.2s ease; +} + +.edit-docs-link:hover { + background-color: var(--bg-secondary); +} + +.edit-docs-link:focus-visible { + outline: var(--focus-ring-width) solid var(--focus-ring); + outline-offset: 2px; } .edit-docs-link img { @@ -204,6 +404,14 @@ li.version ul.pure-menu-children { display: none; } +/* Improved responsive breakpoints */ +@media (max-width: 1400px) { + .container { + width: calc(100% - 300px); + max-width: 900px; + } +} + @media (max-width: 1160px) { h2:hover::before, h3:hover::before { position: static; @@ -226,10 +434,12 @@ li.version ul.pure-menu-children { display: none; position: fixed; top: 45px; - border-top: 1px solid #ddd; - border-right: 1px solid #ddd; - border-bottom: 1px solid #ddd; + border-top: 1px solid var(--border-color); + border-right: 1px solid var(--border-color); + border-bottom: 1px solid var(--border-color); left: 0px; + max-height: calc(100vh - 45px); + box-shadow: 2px 0 8px var(--shadow); } pre { @@ -238,13 +448,15 @@ li.version ul.pure-menu-children { } #mobile-menu { - display: block; + display: flex; + align-items: center; height: 45px; - background-color: #eee; - border-bottom: 1px solid #ddd; + background-color: var(--menu-bg); + border-bottom: 1px solid var(--border-color); position: sticky; top: 0; - z-index: 1; + z-index: 100; + box-shadow: 0 2px 4px var(--shadow); } #logo { @@ -259,6 +471,21 @@ li.version ul.pure-menu-children { width: 215px; margin-left: auto; margin-right: auto; + display: flex; + align-items: center; + justify-content: center; + } + + #mobile-logo-container a { + display: flex; + align-items: center; + text-decoration: none; + } + + #mobile-logo-container a:focus-visible { + outline: var(--focus-ring-width) solid var(--focus-ring); + outline-offset: 2px; + border-radius: 2px; } #logo-container { @@ -267,26 +494,44 @@ li.version ul.pure-menu-children { .menu-link { position: absolute; - display: block; + display: flex; + align-items: center; + justify-content: center; top: 0px; left: 0; - background-color: #eee; + background-color: transparent; z-index: 10; - width: 2em; - height: 3px; - padding: 2.1em 1.6em; + width: 45px; + height: 45px; + padding: 0; + border: none; + cursor: pointer; + transition: background-color 0.2s ease; + } + + .menu-link:hover { + background-color: var(--menu-hover); } - .menu-link:hover, - .menu-link:focus { - background: #ddd; + .menu-link:focus-visible { + outline: var(--focus-ring-width) solid var(--focus-ring); + outline-offset: -2px; + background-color: var(--menu-hover); } #menuLink { width: 40px; height: 40px; - padding: 2.5px; - color: rgb(0,0,0); + padding: 8px; + color: var(--text-primary); + display: flex; + align-items: center; + justify-content: center; + } + + #menuLink svg { + width: 100%; + height: 100%; } .active { @@ -315,8 +560,8 @@ li.version ul.pure-menu-children { right: 0px; width: 190px; text-align: center; - border: 1px solid #ddd; - background-color: #fff; + border: 1px solid var(--border-color); + background-color: var(--bg-primary); } #jobs img { @@ -329,11 +574,11 @@ li.version ul.pure-menu-children { } #jobs .job-listing:hover { - background-color: #ddd; + background-color: var(--menu-hover); } #jobs .company { - color: black; + color: var(--text-primary); } #jobs .title { @@ -341,7 +586,7 @@ li.version ul.pure-menu-children { } #jobs .location { - color: black; + color: var(--text-secondary); margin-top: 0.25em; font-size: 0.75em; } @@ -356,14 +601,14 @@ li.version ul.pure-menu-children { #jobs .jobs-view-more { margin: 1em; font-size: 0.95em; - color: #777777; + color: var(--text-muted); padding: 0.25em; border-radius: 3px; - background-color: #eee; + background-color: var(--bg-secondary); } #jobs .jobs-view-more a { - color: black; + color: var(--text-primary); } @media (max-width: 1360px) { @@ -371,3 +616,194 @@ li.version ul.pure-menu-children { display: none !important; } } + +/* Print stylesheet improvements */ +@media print { + #menu, + #mobile-menu, + .edit-docs-link, + #jobs, + .cpc-ad, + .api-nav { + display: none !important; + } + + .container { + left: 0 !important; + width: 100% !important; + padding: 0 !important; + margin: 0 !important; + } + + body { + background: white; + color: black; + } + + a { + color: #000; + text-decoration: underline; + } + + a[href^="http"]:after { + content: " (" attr(href) ")"; + font-size: 0.8em; + color: #666; + } + + pre { + border: 1px solid #ccc; + page-break-inside: avoid; + } + + h1, h2, h3, h4, h5, h6 { + page-break-after: avoid; + } + + img { + max-width: 100% !important; + page-break-inside: avoid; + } +} + +/* Tablet breakpoint */ +@media (min-width: 768px) and (max-width: 1160px) { + .container { + padding-left: 30px; + padding-right: 30px; + } +} + +/* Small mobile devices */ +@media (max-width: 480px) { + .container { + padding-left: 15px; + padding-right: 15px; + } + + #content { + margin: 5px; + } + + #mobile-logo-container { + width: 180px; + } + + .logo-text { + font-size: 16pt; + } +} + +/* Theme Toggle Button */ +#theme-toggle { + position: fixed; + top: 20px; + right: 20px; + z-index: 1000; + background-color: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 50%; + box-shadow: 0 2px 8px var(--shadow); + transition: background-color 0.3s ease, border-color 0.3s ease; +} + +#theme-toggle-btn { + width: 44px; + height: 44px; + padding: 0; + margin: 0; + border: none; + background: transparent; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + color: var(--text-primary); + transition: background-color 0.2s ease, transform 0.1s ease; +} + +#theme-toggle-btn:hover { + background-color: var(--menu-hover); +} + +#theme-toggle-btn:active { + transform: scale(0.95); +} + +#theme-toggle-btn:focus-visible { + outline: var(--focus-ring-width) solid var(--focus-ring); + outline-offset: 2px; +} + +#theme-icon-light, +#theme-icon-dark { + position: absolute; + transition: opacity 0.3s ease, transform 0.3s ease; + width: 20px; + height: 20px; +} + +/* Default: show moon icon (for light/system mode) */ +#theme-icon-dark { + opacity: 1; + transform: rotate(0deg); +} + +#theme-icon-light { + opacity: 0; + transform: rotate(90deg); + pointer-events: none; +} + +/* In dark mode: show sun icon, hide moon icon */ +[data-theme="dark"] #theme-icon-light { + opacity: 1; + transform: rotate(0deg); + pointer-events: auto; +} + +[data-theme="dark"] #theme-icon-dark { + opacity: 0; + transform: rotate(90deg); + pointer-events: none; +} + +/* On mobile, adjust position */ +@media (max-width: 1160px) { + #theme-toggle { + top: auto; + bottom: 15px; + right: 15px; + } +} + +/* Mongoose brand buttons */ +.mongoose-btn-outline, +.mongoose-btn-solid { + border-radius: 3px; + padding: 3px 30px; + cursor: pointer; + font-size: inherit; + transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease; +} + +.mongoose-btn-outline { + border: 1px solid var(--brand-primary); + color: var(--brand-primary); + background-color: transparent; +} + +.mongoose-btn-outline:hover { + background-color: var(--brand-primary-subtle); +} + +.mongoose-btn-solid { + border: 1px solid transparent; + color: var(--bg-primary); + background-color: var(--brand-primary); +} + +.mongoose-btn-solid:hover { + background-color: var(--brand-primary-hover); +} diff --git a/docs/css/style.css b/docs/css/style.css index 32edaaea473..a9c8a7fc6a1 100644 --- a/docs/css/style.css +++ b/docs/css/style.css @@ -1,16 +1,94 @@ body { font-family: 'Open Sans', Helvetica, Arial, FreeSans; - color: #333; + color: var(--text-secondary); -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: 100%; padding: 0; margin: 0; + background-color: var(--bg-primary); + transition: background-color 0.3s ease, color 0.3s ease; +} + +/* Theme Toggle Button for Homepage */ +#theme-toggle { + position: fixed; + top: 20px; + right: 20px; + bottom: auto; + z-index: 1000; + background-color: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: 50%; + box-shadow: 0 2px 8px var(--shadow); + transition: background-color 0.3s ease, border-color 0.3s ease; +} + +#theme-toggle-btn { + width: 44px; + height: 44px; + padding: 0; + margin: 0; + border: none; + background: transparent; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + color: var(--text-secondary); + transition: background-color 0.2s ease, transform 0.1s ease; +} + +#theme-toggle-btn:hover { + background-color: var(--menu-hover); +} + +#theme-toggle-btn:active { + transform: scale(0.95); +} + +#theme-toggle-btn:focus-visible { + outline: 3px solid var(--focus-ring); + outline-offset: 2px; +} + +#theme-icon-light, +#theme-icon-dark { + position: absolute; + transition: opacity 0.3s ease, transform 0.3s ease; + width: 20px; + height: 20px; +} + +/* Default: show moon icon (for light/system mode) */ +#theme-icon-dark { + opacity: 1; + transform: rotate(0deg); +} + +#theme-icon-light { + opacity: 0; + transform: rotate(90deg); + pointer-events: none; +} + +/* In dark mode: show sun icon, hide moon icon */ +[data-theme="dark"] #theme-icon-light { + opacity: 1; + transform: rotate(0deg); + pointer-events: auto; +} + +[data-theme="dark"] #theme-icon-dark { + opacity: 0; + transform: rotate(90deg); + pointer-events: none; } /* location.hash */ :target::before { content: ">>> "; - color: #1371C9; + color: var(--link-color); font-weight: bold; font-size: 20px; } @@ -21,14 +99,20 @@ body { } a { - color: #800; + color: var(--link-color); -webkit-transition-property: opacity, -webkit-transform, color, background-color, padding, -webkit-box-shadow; -webkit-transition-duration: 0.15s; -webkit-transition-timing-function: ease-out; + transition: opacity 0.15s ease-out, color 0.15s ease-out; } a:hover { opacity: 0.8; } +a:focus-visible { + outline: 3px solid var(--focus-ring); + outline-offset: 2px; + border-radius: 2px; +} #wrap { width: 600px; margin: 0 auto; @@ -43,21 +127,28 @@ h1 { } pre { - background: #eee; - padding: 5px; - border-radius: 3px; + background: var(--code-bg); + padding: 12px 16px; + border-radius: 6px; overflow-x: auto; + border: 1px solid var(--border-color); + transition: background-color 0.3s ease, border-color 0.3s ease; } code { - color: #333; + color: var(--code-text); font-size: 11px; font-family: Consolas, "Liberation Mono", Courier, monospace; + background-color: var(--code-bg); + padding: 2px 6px; + border-radius: 4px; } pre code { border: 0 none; padding: 1.2em; overflow-x: auto; + background-color: transparent; } + #header { text-align: center; padding-top: 20px; @@ -83,14 +174,15 @@ h2 a { font-size: 146px; font-weight: 100; text-indent: -23px; + color: var(--brand-primary); } .load #header .mongoose { letter-spacing: -14px; } .tagline { - color: #333; + color: var(--text-secondary); font-size: 25px; - text-shadow: 1px 1px #f8f8f8; + text-shadow: 1px 1px var(--tagline-text-shadow); text-align: center; margin: 7px 0; } @@ -102,7 +194,7 @@ h2 a { } .tagline a, .blurb a { text-decoration: none; - color: #800; + color: var(--brand-primary); } #links { margin: 50px 10px 20px; @@ -116,10 +208,9 @@ h2 a { #links li { display: inline-block; margin: 0 15px; - background-color: #FEFEFE; } #links a { - background: #444; + background: var(--btn-bg); padding: 9px 0px; border-radius: 3px; color: white; @@ -127,7 +218,7 @@ h2 a { display: inline-block; text-decoration: none; text-transform: lowercase; - text-shadow: 1px 1px 7px #222; + text-shadow: 1px 1px 7px var(--btn-text-shadow); } #follow { margin-bottom: 36px; @@ -148,6 +239,20 @@ h2 a { position: relative; top: -4px; } +/* GitHub buttons iframe - use filter to invert colors in dark mode */ +.github-btn { + background: transparent; +} +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) .github-btn { + color-scheme: light; + filter: invert(0.85) hue-rotate(180deg); + } +} +[data-theme="dark"] .github-btn { + color-scheme: light; + filter: invert(0.85) hue-rotate(180deg); +} #what { margin-bottom: 32px; } @@ -227,7 +332,7 @@ h2 a { margin: 0 12px; } #links a { - background: #444; + background: var(--btn-bg); padding: 7px 34px; font-size: 15px; } @@ -277,7 +382,7 @@ h2 a { #tidelift-button { display: inline-block; - background-color: #444; + background-color: var(--btn-bg); padding-top: 9px; padding-left: 15px; padding-right: 15px; @@ -287,7 +392,7 @@ h2 a { font-size: 20px; display: inline-block; text-decoration: none; - text-shadow: 1px 1px 7px #222; + text-shadow: 1px 1px 7px var(--btn-text-shadow); margin: auto; } #tidelift-button span { diff --git a/docs/enterprise.md b/docs/enterprise.md index bad5366b5e3..6ccaac8c61f 100644 --- a/docs/enterprise.md +++ b/docs/enterprise.md @@ -9,10 +9,10 @@ reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. - + - + ## Enterprise-ready open source software—managed for you @@ -64,8 +64,8 @@ grappling with esoteric open source trivia, and more time building your own applications—and your business. - + - + diff --git a/docs/images/mongoose.svg b/docs/images/mongoose.svg index a230aefd1f9..977463bcf14 100644 --- a/docs/images/mongoose.svg +++ b/docs/images/mongoose.svg @@ -1,35 +1,71 @@ - - + + - + image/svg+xml - + - - - + + + - - - - + + + + - - - + + + + d="m 0,0 c -9.067,-0.676 -18.155,-1.376 -27.24,-1.429 -5.592,-0.032 -11.28,0.698 -16.766,1.839 -6.932,1.443 -12.758,4.878 -16.186,11.582 -0.51,0.993 -2.217,1.565 -3.484,1.943 -4.468,1.328 -8.986,2.485 -13.498,4.154 1.57,0.118 3.141,0.346 4.708,0.33 3.846,-0.038 7.735,0.142 11.522,-0.366 6.921,-0.928 13.787,-2.28 20.673,-3.475 3.924,-0.682 7.869,-1.296 11.761,-2.133 7.385,-1.589 14.744,-3.298 22.106,-4.989 C -1.539,6.339 3.315,5.182 8.168,4.007 M 1.769,-49.161 c -11.308,-3.485 -22.742,-6.62 -33.861,-10.63 -6.961,-2.511 -13.502,-6.23 -20.12,-9.626 -5.77,-2.958 -7.811,-8.009 -7.103,-14.28 0.457,-4.029 1.009,-8.047 1.371,-12.133 -2.957,4.363 -5.141,9.082 -6.424,14.218 -0.264,1.062 -0.515,2.128 -0.737,3.2 -0.874,4.183 1.446,6.787 4.476,9.055 5.897,4.415 12.32,7.892 19.292,10.205 8.696,2.887 17.419,5.768 26.284,8.057 6.245,1.61 12.745,2.223 19.127,3.304 1.178,0.202 2.337,0.52 3.505,0.782 0.039,-0.16 0.079,-0.321 0.12,-0.481 -1.977,-0.554 -3.97,-1.066 -5.93,-1.671 m -159.454,44.744 c -0.186,-0.182 -0.375,-0.362 -0.563,-0.542 -5.042,3.677 -10.514,6.415 -16.51,8.003 -0.168,-0.163 -0.332,-0.326 -0.496,-0.49 5.161,-6.048 10.323,-12.096 15.71,-18.404 -3.607,1.18 -6.964,2.28 -10.322,3.379 -0.143,-0.169 -0.284,-0.339 -0.428,-0.508 4.063,-6.694 9.353,-12.51 14.396,-19.22 -8.768,4.979 -16.507,10.417 -23.731,16.595 -9.916,8.481 -18.566,18.047 -25.046,29.439 -2.669,4.694 -5.025,9.55 -5.529,15.043 -0.113,1.226 -0.015,2.523 0.271,3.72 0.465,1.943 2.309,2.84 4.202,2.252 10.4,-3.233 20.622,-6.896 30.189,-12.211 9.056,-5.031 17.252,-11.153 24.303,-18.769 0.285,-0.308 0.496,-0.684 0.889,-1.236 -5.521,2.516 -10.76,4.902 -16,7.289 -0.177,-0.217 -0.353,-0.435 -0.532,-0.653 3.067,-4.562 6.132,-9.125 9.197,-13.687 m -315.372,-55.318 c -0.13,-0.353 0.276,-0.7 0.414,-1.049 2.156,0.453 4.293,1.045 6.47,1.339 0.568,0.075 1.136,0.147 1.705,0.223 8.034,2.863 16.264,5.112 24.762,6.31 12.011,1.691 24.463,3.936 36.722,3.87 11.648,-0.059 23.431,-1.843 34.828,-3.808 3.197,-0.55 6.336,-1.288 9.43,-2.158 8.326,-1.869 16.15,-5.203 23.182,-9.709 0.507,-0.242 1,-0.509 1.502,-0.763 9.998,-4.71 18.807,-11.639 25.763,-20.128 1.273,-1.409 3.492,-2.878 4.629,-4.42 2.396,-2.827 3.537,-5.839 7.537,-8.996 v 43.126 h 49.88 l 23.323,-43.036 23.392,43.036 h 51.405 v -83.351 l 8.509,5.611 c 0,0 9.33,24.806 52.556,28.289 l 49.544,1.602 c -7.642,15.819 -7.614,15.819 -3.929,20.591 3.649,4.722 31.792,7.818 36.742,10.771 l 72.297,14.587 c 0.17,0.556 0.363,-0.298 0.736,-0.181 1.799,0.567 3.663,0.924 6.233,1.529 l 16.35,3.979 c 1.064,0.599 2.331,0.836 3.504,1.242 7.64,2.652 14.321,6.068 18.018,14.208 3.6,7.93 8.421,15.296 10.169,24.018 0.551,2.759 0.076,5.157 -1.915,7.14 -2.409,2.398 -5.516,3.428 -8.715,4.259 -10.444,4.287 -20.85,8.676 -31.379,12.747 -6.473,2.504 -13.114,4.582 -19.724,6.71 -6.54,2.105 -13.117,4.092 -19.711,6.019 -6.074,1.777 -12.185,3.429 -18.288,5.094 -5.954,1.624 -11.9,3.295 -17.892,4.767 -6.981,1.716 -14.01,3.243 -21.02,4.845 -5.324,1.217 -10.628,2.519 -15.977,3.608 -5.825,1.186 -11.687,2.206 -17.543,3.237 -7.48,1.315 -14.944,2.773 -22.466,3.797 -9.083,1.236 -18.224,2.036 -27.332,3.109 -15.25,1.798 -30.54,2.28 -45.839,1.111 -5.422,-0.414 -10.771,-1.788 -16.152,-2.722 5.7,-2.929 11.34,-5.298 16.903,-7.834 5.596,-2.551 10.813,-5.748 15.604,-9.607 -6.269,2.4 -12.368,5.168 -18.655,7.413 -13.121,4.684 -26.47,8.574 -40.333,10.45 -6.781,0.917 -13.545,1.797 -20.32,-0.115 -6.867,-1.938 -8.809,-4.486 -9.431,-11.377 -1.03,-11.422 2.407,-22.016 6.972,-32.231 6.807,-15.232 16.728,-28.207 29.414,-39.057 9.714,-8.311 20.492,-14.861 32.398,-19.531 3.391,-1.33 6.815,-2.579 10.221,-3.866 -0.077,-0.265 -0.154,-0.527 -0.231,-0.793 -6.528,1.788 -13.221,3.128 -19.544,5.466 -11.183,4.133 -21.183,10.447 -30.332,18.115 -10.212,8.561 -18.699,18.566 -25.007,30.289 -6.694,12.439 -10.525,25.73 -10.663,39.955" + style="fill:currentColor;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path26" /> - - + + - \ No newline at end of file + diff --git a/docs/images/mongoose5_62x30_transparent.png b/docs/images/mongoose5_62x30_transparent.png deleted file mode 100644 index 4df8e77ed1d..00000000000 Binary files a/docs/images/mongoose5_62x30_transparent.png and /dev/null differ diff --git a/docs/js/theme-toggle.js b/docs/js/theme-toggle.js new file mode 100644 index 00000000000..bb8f987d3b2 --- /dev/null +++ b/docs/js/theme-toggle.js @@ -0,0 +1,32 @@ +'use strict'; +(function() { + const STORAGE_KEY = 'mongoose-theme'; + const supportsMatchMedia = typeof window !== 'undefined' && typeof window.matchMedia === 'function'; + const prefersDarkQuery = supportsMatchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null; + + const theme = localStorage.getItem(STORAGE_KEY) || (prefersDarkQuery?.matches ? 'dark' : 'light'); + applyTheme(theme, true); + const toggleBtn = document.getElementById('theme-toggle-btn'); + toggleBtn.addEventListener('click', toggleTheme); + + function toggleTheme() { + const currentTheme = document.documentElement.getAttribute('data-theme'); + if (currentTheme === 'dark') { + applyTheme('light'); + } else { + applyTheme('dark'); + } + } + + function applyTheme(theme, skipSetStorage) { + document.documentElement.setAttribute('data-theme', theme); + if (!skipSetStorage) { + try { + localStorage.setItem(STORAGE_KEY, theme); + // eslint-disable-next-line no-unused-vars + } catch (err) { + // Silently fail - theme will still work for current session + } + } + } +})(); diff --git a/docs/layout.pug b/docs/layout.pug index 34fd4081dbb..42c50652759 100644 --- a/docs/layout.pug +++ b/docs/layout.pug @@ -4,14 +4,22 @@ html(lang='en') meta(charset="utf-8") meta(name="viewport", content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no") title Mongoose v#{package.version}: #{title} + //- Inline script to prevent flash of wrong theme - must run before CSS loads + script. + (function() { + var theme = localStorage.getItem('mongoose-theme'); + if (theme) { + document.documentElement.setAttribute('data-theme', theme); + } + })(); include ./includes/favicon block style link(rel="stylesheet", href="https://unpkg.com/purecss@1.0.1/build/pure-min.css", integrity="sha384-oAOxQR6DkCoMliIh8yFnu25d7Eq/PHS21PClpwjOTeU2jRSq11vu66rf90/cZr47", crossorigin="anonymous") 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/mongoose5.css?v=${Date.now()}`) + link(rel="stylesheet", href=`${versions.versionedPath}/docs/css/github.css?v=${Date.now()}`) link(rel="stylesheet", href=`${versions.versionedPath}/docs/css/carbonads.css`) link(rel="stylesheet", href=`${versions.versionedPath}/docs/css/copy-code.css`) @@ -23,18 +31,22 @@ html(lang='en') body block layout #layout + #theme-toggle + button#theme-toggle-btn(aria-label="Toggle dark mode" title="Toggle dark/light theme") + + #mobile-menu a#menuLink.menu-link(href='#menu') #mobile-logo-container a(href="/") - img#logo(src=`${versions.versionedPath}/docs/images/mongoose5_62x30_transparent.png`) + span#logo(role="img" aria-label="Mongoose logo") span.logo-text mongoose #menu nav.pure-menu #logo-container.pure-menu-heading a(href="/") - img#logo(src=`${versions.versionedPath}/docs/images/mongoose5_62x30_transparent.png`) + span#logo(role="img" aria-label="Mongoose logo") span.logo-text mongoose ul.pure-menu-list#navbar li.pure-menu-horizontal.pure-menu-item.pure-menu-has-children.pure-menu-allow-hover.version @@ -174,5 +186,6 @@ html(lang='en') script(type="text/javascript" src=`${versions.versionedPath}/docs/js/navbar-search.js`) script(type="text/javascript" src=`${versions.versionedPath}/docs/js/mobile-navbar-toggle.js`) + script(type="text/javascript" src=`${versions.versionedPath}/docs/js/theme-toggle.js`) script(type="text/javascript" src=`${versions.versionedPath}/docs/js/copy-code.js`) diff --git a/docs/migrating_to_9.md b/docs/migrating_to_9.md index 808f08e7881..0a6b1510326 100644 --- a/docs/migrating_to_9.md +++ b/docs/migrating_to_9.md @@ -10,6 +10,31 @@ There are several backwards-breaking changes you should be aware of when migrati If you're still on Mongoose 7.x or earlier, please read the [Mongoose 7.x to 8.x migration guide](migrating_to_8.html) and upgrade to Mongoose 8.x first before upgrading to Mongoose 9. +## Pre middleware no longer supports `next()` + +In Mongoose 9, pre middleware no longer receives a `next()` parameter. +Instead, you should use `async` functions or promises to handle async pre middleware. + +```javascript +// Worked in Mongoose 8.x, no longer supported in Mongoose 9! +schema.pre('save', function(next) { + // Do something async + next(); +}); + +// Mongoose 9.x example usage +schema.pre('save', async function() { + // Do something async +}); +// or use promises: +schema.pre('save', function() { + return new Promise((resolve, reject) => { + // Do something async + resolve(); + }); +}); +``` + ## `Schema.prototype.doValidate()` now returns a promise `Schema.prototype.doValidate()` now returns a promise that rejects with a validation error if one occurred. @@ -37,37 +62,6 @@ try { } ``` -## Errors in middleware functions take priority over `next()` calls - -In Mongoose 8.x, if a middleware function threw an error after calling `next()`, that error would be ignored. - -```javascript -schema.pre('save', function(next) { - next(); - // In Mongoose 8, this error will not get reported, because you already called next() - throw new Error('woops!'); -}); -``` - -In Mongoose 9, errors in the middleware function take priority, so the above `save()` would throw an error. - -## `next()` no longer supports passing arguments to the next middleware - -Previously, you could call `next(null, 'new arg')` in a hook and the args to the next middleware would get overwritten by 'new arg'. - -```javascript -schema.pre('save', function(next, options) { - options; // options passed to `save()` - next(null, 'new arg'); -}); - -schema.pre('save', function(next, arg) { - arg; // In Mongoose 8, this would be 'new arg', overwrote the options passed to `save()` -}); -``` - -In Mongoose 9, `next(null, 'new arg')` doesn't overwrite the args to the next middleware. - ## Update pipelines disallowed by default As of MongoDB 4.2, you can pass an array of pipeline stages to `updateOne()`, `updateMany()`, and `findOneAndUpdate()` to modify the document in multiple stages. diff --git a/docs/version-support.md b/docs/version-support.md index 25e82d2a307..9eb200c0ae2 100644 --- a/docs/version-support.md +++ b/docs/version-support.md @@ -5,7 +5,7 @@ Released on **November 21, 2025**, Mongoose 9 is the latest major version. All new features, improvements, and bug fixes are delivered to 8.x—so if you want the best experience, this is the version to use. -* **Latest release:** [9.0.0](https://mongoosejs.com/docs/index.html) +* **Latest release:** [9.0.1](https://mongoosejs.com/docs/index.html) * **Docs:** https://mongoosejs.com/docs ## Mongoose 8 (Prior Version) @@ -13,7 +13,7 @@ All new features, improvements, and bug fixes are delivered to 8.x—so if you w Released on **October 31, 2023**, Mongoose 8 was the latest major version until 9.0.0 was released. All new features, improvements, and bug fixes are still delivered to 8.x until at least February 1, 2026. -* **Latest release:** [8.20.1](https://mongoosejs.com/docs/index.html) +* **Latest release:** [8.20.2](https://mongoosejs.com/docs/index.html) * **Docs:** https://mongoosejs.com/docs/8.x/ ## Mongoose 7 (Legacy) @@ -21,7 +21,7 @@ All new features, improvements, and bug fixes are still delivered to 8.x until a Mongoose 7.x was released on **February 27, 2023** and is now in **legacy support**. It still receives important fixes when needed, but it’s no longer the primary development focus. -* **Latest release:** [7.8.7](https://mongoosejs.com/docs/7.x/docs/guide.html) +* **Latest release:** [7.8.8](https://mongoosejs.com/docs/7.x/docs/guide.html) * **Docs:** https://mongoosejs.com/docs/7.x/ ## Mongoose 6 (Limited Maintenance) diff --git a/index.pug b/index.pug index 14cba69a0ff..b0fd5bffcfa 100644 --- a/index.pug +++ b/index.pug @@ -5,6 +5,7 @@ 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/mongoose5.css", rel="stylesheet") link(href="docs/css/style.css", rel="stylesheet") link(href="docs/css/github.css", rel="stylesheet") link(href="docs/css/carbonads.css", rel="stylesheet") @@ -45,6 +46,10 @@ html(lang='en') } body + #theme-toggle + button#theme-toggle-btn(aria-label="Toggle dark mode" title="Toggle dark/light theme") + + a(class="github-fork-ribbon" href="https://github.com/Automattic/mongoose" data-ribbon="Fork me on GitHub" title="Fork me on GitHub" target="_blank"). Fork me on GitHub #wrap.homepage @@ -153,5 +158,6 @@ html(lang='en') Sponsor [Mongoose on OpenCollective](https://opencollective.com/mongoose) to get your company's logo above! p#footer Licensed under MIT. + script(src="docs/js/theme-toggle.js") script. document.body.className = 'load'; diff --git a/lib/aggregate.js b/lib/aggregate.js index 560c9e228c8..2b72f6ffb61 100644 --- a/lib/aggregate.js +++ b/lib/aggregate.js @@ -103,7 +103,7 @@ Aggregate.prototype._optionsForExec = function() { const options = this.options || {}; const asyncLocalStorage = this.model()?.db?.base.transactionAsyncLocalStorage?.getStore(); - if (!options.hasOwnProperty('session') && asyncLocalStorage?.session != null) { + if (!Object.hasOwn(options, 'session') && asyncLocalStorage?.session != null) { options.session = asyncLocalStorage.session; } diff --git a/lib/cast.js b/lib/cast.js index d5ea75227b9..44f287b3224 100644 --- a/lib/cast.js +++ b/lib/cast.js @@ -107,7 +107,7 @@ 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')) { + } else if (path === '$comment' && !Object.hasOwn(schema.paths, '$comment')) { val = castString(val, path); obj[path] = val; } else { diff --git a/lib/cast/double.js b/lib/cast/double.js index c3887c97b86..2db8c2e32b9 100644 --- a/lib/cast/double.js +++ b/lib/cast/double.js @@ -34,7 +34,7 @@ module.exports = function castDouble(val) { // ex: { a: 'im an object, valueOf: () => 'helloworld' } // throw an error if (typeof tempVal === 'string') { try { - coercedVal = BSON.Double.fromString(val); + coercedVal = BSON.Double.fromString(tempVal); return coercedVal; } catch { assert.ok(false); diff --git a/lib/connection.js b/lib/connection.js index 8d9d27e8ff0..629bab30a0b 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -164,7 +164,7 @@ Object.defineProperty(Connection.prototype, 'readyState', { */ Connection.prototype.get = function getOption(key) { - if (this.config.hasOwnProperty(key)) { + if (Object.hasOwn(this.config, key)) { return this.config[key]; } @@ -192,7 +192,7 @@ Connection.prototype.get = function getOption(key) { */ Connection.prototype.set = function setOption(key, val) { - if (this.config.hasOwnProperty(key)) { + if (Object.hasOwn(this.config, key)) { this.config[key] = val; return val; } @@ -459,7 +459,7 @@ Connection.prototype.bulkWrite = async function bulkWrite(ops, options) { const ordered = options.ordered == null ? true : options.ordered; const asyncLocalStorage = this.base.transactionAsyncLocalStorage?.getStore(); - if ((!options || !options.hasOwnProperty('session')) && asyncLocalStorage?.session != null) { + if ((!options || !Object.hasOwn(options, 'session')) && asyncLocalStorage?.session != null) { options = { ...options, session: asyncLocalStorage.session }; } @@ -477,7 +477,7 @@ Connection.prototype.bulkWrite = async function bulkWrite(ops, options) { if (op.name == null) { throw new MongooseError('Must specify operation name in Connection.prototype.bulkWrite()'); } - if (!castBulkWrite.cast.hasOwnProperty(op.name)) { + if (!Object.hasOwn(castBulkWrite.cast, op.name)) { throw new MongooseError(`Unrecognized bulkWrite() operation name ${op.name}`); } @@ -513,7 +513,7 @@ Connection.prototype.bulkWrite = async function bulkWrite(ops, options) { results[i] = error; continue; } - if (!castBulkWrite.cast.hasOwnProperty(op.name)) { + if (!Object.hasOwn(castBulkWrite.cast, op.name)) { const error = new MongooseError(`Unrecognized bulkWrite() operation name ${op.name}`); validationErrors.push({ index: i, error: error }); results[i] = error; @@ -772,10 +772,10 @@ async function _wrapUserTransaction(fn, session, mongoose) { function _resetSessionDocuments(session) { for (const doc of session[sessionNewDocuments].keys()) { const state = session[sessionNewDocuments].get(doc); - if (state.hasOwnProperty('isNew')) { + if (Object.hasOwn(state, 'isNew')) { doc.$isNew = state.isNew; } - if (state.hasOwnProperty('versionKey')) { + if (Object.hasOwn(state, 'versionKey')) { doc.set(doc.schema.options.versionKey, state.versionKey); } @@ -1013,7 +1013,7 @@ Connection.prototype.onOpen = function() { // avoid having the collection subscribe to our event emitter // to prevent 0.3 warning for (const i in this.collections) { - if (utils.object.hasOwnProperty(this.collections, i)) { + if (Object.hasOwn(this.collections, i)) { this.collections[i].onOpen(); } } @@ -1321,7 +1321,7 @@ Connection.prototype.onClose = function onClose(force) { // avoid having the collection subscribe to our event emitter // to prevent 0.3 warning for (const i in this.collections) { - if (utils.object.hasOwnProperty(this.collections, i)) { + if (Object.hasOwn(this.collections, i)) { this.collections[i].onClose(force); } } diff --git a/lib/document.js b/lib/document.js index 45da5eb3f13..63391b1e94f 100644 --- a/lib/document.js +++ b/lib/document.js @@ -648,6 +648,10 @@ Document.prototype.init = function(doc, opts, fn) { opts = null; } + if (doc == null) { + throw new ObjectParameterError(doc, 'doc', 'init'); + } + this.$__init(doc, opts); if (fn) { @@ -677,6 +681,9 @@ Document.prototype.$init = function() { */ Document.prototype.$__init = function(doc, opts) { + if (doc == null) { + throw new ObjectParameterError(doc, 'doc', 'init'); + } this.$isNew = false; opts = opts || {}; @@ -784,7 +791,7 @@ function init(self, obj, doc, opts, prefix) { } } else { // Retain order when overwriting defaults - if (doc.hasOwnProperty(i) && value !== void 0 && !opts.hydratedPopulatedDocs) { + if (Object.hasOwn(doc, i) && value !== void 0 && !opts.hydratedPopulatedDocs) { delete doc[i]; } if (value === null) { @@ -1167,7 +1174,7 @@ Document.prototype.$set = function $set(path, val, type, options) { const orderedKeys = Object.keys(this.$__schema.tree); for (let i = 0, len = orderedKeys.length; i < len; ++i) { (key = orderedKeys[i]) && - (this._doc.hasOwnProperty(key)) && + (Object.hasOwn(this._doc, key)) && (orderedDoc[key] = undefined); } this._doc = Object.assign(orderedDoc, this._doc); @@ -1217,8 +1224,8 @@ Document.prototype.$set = function $set(path, val, type, options) { 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 hasInitialVal = this.$__.savedState != null && Object.hasOwn(this.$__.savedState, path); + if (this.$__.savedState != null && !this.$isNew && !Object.hasOwn(this.$__.savedState, path)) { const initialVal = this.$__getValue(path); this.$__.savedState[path] = initialVal; @@ -1523,7 +1530,7 @@ Document.prototype.$set = function $set(path, val, type, options) { this.$__.session[sessionNewDocuments].get(this).modifiedPaths && !this.$__.session[sessionNewDocuments].get(this).modifiedPaths.has(savedStatePath); if (savedState != null && - savedState.hasOwnProperty(savedStatePath) && + Object.hasOwn(savedState, savedStatePath) && (!isInTransaction || isModifiedWithinTransaction) && utils.deepEqual(val, savedState[savedStatePath])) { this.unmarkModified(path); @@ -2005,7 +2012,7 @@ Document.prototype.$get = Document.prototype.get; Document.prototype.$__path = function(path) { const adhocs = this.$__.adhocPaths; - const adhocType = adhocs && adhocs.hasOwnProperty(path) ? adhocs[path] : null; + const adhocType = adhocs && Object.hasOwn(adhocs, path) ? adhocs[path] : null; if (adhocType) { return adhocType; @@ -2049,7 +2056,7 @@ Document.prototype.$__saveInitialState = function $__saveInitialState(path) { if (savedState != null) { const firstDot = savedStatePath.indexOf('.'); const topLevelPath = firstDot === -1 ? savedStatePath : savedStatePath.slice(0, firstDot); - if (!savedState.hasOwnProperty(topLevelPath)) { + if (!Object.hasOwn(savedState, topLevelPath)) { savedState[topLevelPath] = clone(this.$__getValue(topLevelPath)); } } @@ -2360,7 +2367,7 @@ Document.prototype.$isDefault = function(path) { } if (typeof path === 'string' && path.indexOf(' ') === -1) { - return this.$__.activePaths.getStatePaths('default').hasOwnProperty(path); + return Object.hasOwn(this.$__.activePaths.getStatePaths('default'), path); } let paths = path; @@ -2368,7 +2375,7 @@ Document.prototype.$isDefault = function(path) { paths = paths.split(' '); } - return paths.some(path => this.$__.activePaths.getStatePaths('default').hasOwnProperty(path)); + return paths.some(path => Object.hasOwn(this.$__.activePaths.getStatePaths('default'), path)); }; /** @@ -2422,7 +2429,7 @@ Document.prototype.isDirectModified = function(path) { } if (typeof path === 'string' && path.indexOf(' ') === -1) { - const res = this.$__.activePaths.getStatePaths('modify').hasOwnProperty(path); + const res = Object.hasOwn(this.$__.activePaths.getStatePaths('modify'), path); if (res || path.indexOf('.') === -1) { return res; } @@ -2461,7 +2468,7 @@ Document.prototype.isInit = function(path) { } if (typeof path === 'string' && path.indexOf(' ') === -1) { - return this.$__.activePaths.getStatePaths('init').hasOwnProperty(path); + return Object.hasOwn(this.$__.activePaths.getStatePaths('init'), path); } let paths = path; @@ -2469,7 +2476,7 @@ Document.prototype.isInit = function(path) { paths = paths.split(' '); } - return paths.some(path => this.$__.activePaths.getStatePaths('init').hasOwnProperty(path)); + return paths.some(path => Object.hasOwn(this.$__.activePaths.getStatePaths('init'), path)); }; /** @@ -2607,7 +2614,7 @@ Document.prototype.isDirectSelected = function isDirectSelected(path) { return true; } - if (this.$__.selected.hasOwnProperty(path)) { + if (Object.hasOwn(this.$__.selected, path)) { return inclusive; } @@ -2779,7 +2786,7 @@ function _getPathsToValidate(doc, pathsToValidate, pathsToSkip, isNestedValidate 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 - !subdocParent.$__.activePaths.getStatePaths('modify').hasOwnProperty(fullPathToSubdoc) && + !Object.hasOwn(subdocParent.$__.activePaths.getStatePaths('modify'), fullPathToSubdoc) && !subdocParent.$isDefault(fullPathToSubdoc)) { paths.add(fullPathToSubdoc); @@ -2850,7 +2857,12 @@ function _getPathsToValidate(doc, pathsToValidate, pathsToSkip, isNestedValidate // 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); + const singleNestedPaths = doc.$__schema.singleNestedPaths; + for (const path of Object.keys(flat)) { + if (!Object.hasOwn(singleNestedPaths, path)) { + addToPaths(path); + } + } } } @@ -4188,7 +4200,7 @@ function applyVirtuals(self, json, options, toObjectOptions) { } // Allow skipping aliases with `toObject({ virtuals: true, aliases: false })` - if (!aliases && schema.aliases.hasOwnProperty(path)) { + if (!aliases && Object.hasOwn(schema.aliases, path)) { continue; } @@ -4457,6 +4469,20 @@ Document.prototype.parent = function() { Document.prototype.$parent = Document.prototype.parent; +/** + * Set the parent of this document. + * + * @param {Document} parent + * @api private + * @method $__setParent + * @memberOf Document + * @instance + */ + +Document.prototype.$__setParent = function $__setParent(parent) { + this.$__.parent = parent; +}; + /** * Helper for console.log * @@ -5101,7 +5127,7 @@ function checkDivergentArray(doc, path, 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 && Object.hasOwn(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 || @@ -5169,7 +5195,7 @@ function operand(self, where, delta, data, val, op) { if (/\.\d+\.|\.\d+$/.test(data.path)) { self.$__.version = VERSION_ALL; } else { - self.$__.version = VERSION_INC; + self.$__.version |= VERSION_INC; } } else if (/^\$p/.test(op)) { // potentially changing array positions @@ -5180,7 +5206,7 @@ function operand(self, where, delta, data, val, op) { } else if (/\.\d+\.|\.\d+$/.test(data.path)) { // now handling $set, $unset // subpath of array - self.$__.version = VERSION_WHERE; + self.$__.version |= VERSION_WHERE; } } @@ -5289,7 +5315,7 @@ Document.prototype.$clone = function() { const clonedDoc = new Model(); clonedDoc.$isNew = this.$isNew; if (this._doc) { - clonedDoc._doc = clone(this._doc, { retainDocuments: true }); + clonedDoc._doc = clone(this._doc, { retainDocuments: true, parentDoc: clonedDoc }); } if (this.$__) { const Cache = this.$__.constructor; @@ -5300,7 +5326,10 @@ Document.prototype.$clone = function() { } clonedCache[key] = clone(this.$__[key]); } - Object.assign(clonedCache.activePaths, clone({ ...this.$__.activePaths })); + Object.assign( + clonedCache.activePaths, + clone({ ...this.$__.activePaths }) + ); clonedDoc.$__ = clonedCache; } return clonedDoc; @@ -5426,6 +5455,38 @@ Document.prototype.$__hasOnlyPrimitiveValues = function $__hasOnlyPrimitiveValue })); }; +/*! + * Increment this document's version if necessary. + */ + +Document.prototype._applyVersionIncrement = function _applyVersionIncrement() { + if (!this.$__.version) return; + const doIncrement = VERSION_INC === (VERSION_INC & this.$__.version); + + this.$__.version = undefined; + if (doIncrement) { + const key = this.$__schema.options.versionKey; + const version = this.$__getValue(key) || 0; + this.$__setValue(key, version + 1); // increment version if was successful + } +}; + +/*! + * Increment this document's version if necessary. + */ + +Document.prototype._applyVersionIncrement = function _applyVersionIncrement() { + if (!this.$__.version) return; + const doIncrement = VERSION_INC === (VERSION_INC & this.$__.version); + + this.$__.version = undefined; + if (doIncrement) { + const key = this.$__schema.options.versionKey; + const version = this.$__getValue(key) || 0; + this.$__setValue(key, version + 1); // increment version if was successful + } +}; + /*! * Module exports. */ diff --git a/lib/drivers/node-mongodb-native/collection.js b/lib/drivers/node-mongodb-native/collection.js index 1e4678ff93f..2feb818169f 100644 --- a/lib/drivers/node-mongodb-native/collection.js +++ b/lib/drivers/node-mongodb-native/collection.js @@ -80,12 +80,6 @@ NativeCollection.prototype._getCollection = function _getCollection() { return null; }; -/*! - * ignore - */ - -const syncCollectionMethods = { watch: true, find: true, aggregate: true }; - /** * Copy the collection methods and make them subject to queues * @param {Number|String} I @@ -107,7 +101,6 @@ function iter(i) { _this.conn.options && _this.conn.options.debug; const debug = connectionDebug == null ? globalDebug : connectionDebug; - const lastArg = arguments[arguments.length - 1]; const opId = new ObjectId(); // If user force closed, queueing will hang forever. See #5664 @@ -122,8 +115,8 @@ function iter(i) { } } - let _args = args; - let callback = null; + let timeout = null; + let waitForBufferPromise = null; if (this._shouldBufferCommands() && this.buffer) { this.conn.emit('buffer', { _id: opId, @@ -133,79 +126,28 @@ function iter(i) { args: args }); - let callback; - let _args = args; - let promise = null; - let timeout = null; - 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) { - clearTimeout(timeout); - } - return lastArg.apply(this, arguments); - }; - _args = args.slice(0, args.length - 1).concat([callback]); - } else { - promise = new 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); - }); - } - const bufferTimeoutMS = this._getBufferTimeoutMS(); - timeout = setTimeout(() => { - const removed = this.removeQueue(i, _args); - if (removed) { - const message = 'Operation `' + this.name + '.' + i + '()` buffering timed out after ' + - bufferTimeoutMS + 'ms'; - const err = new MongooseError(message); - this.conn.emit('buffer-end', { _id: opId, modelName: _this.modelName, collectionName: _this.name, method: i, error: err }); - callback(err); - } - }, bufferTimeoutMS); - - if (!syncCollectionMethods[i] && typeof lastArg === 'function') { - this.addQueue(i, _args); - return; - } + waitForBufferPromise = new Promise((resolve, reject) => { + this.addQueue(resolve); + + timeout = setTimeout(() => { + const removed = this.removeQueue(resolve); + if (removed) { + const message = 'Operation `' + this.name + '.' + i + '()` buffering timed out after ' + + bufferTimeoutMS + 'ms'; + const err = new MongooseError(message); + this.conn.emit('buffer-end', { _id: opId, modelName: _this.modelName, collectionName: _this.name, method: i, error: err }); + reject(err); + } + }, bufferTimeoutMS); + }); - return promise; - } else if (!syncCollectionMethods[i] && typeof lastArg === 'function') { - callback = function collectionOperationCallback(err, res) { - if (err != null) { - _this.conn.emit('operation-end', { _id: opId, modelName: _this.modelName, collectionName: _this.name, method: i, error: err }); - } else { - _this.conn.emit('operation-end', { _id: opId, modelName: _this.modelName, collectionName: _this.name, method: i, result: res }); + return waitForBufferPromise.then(() => { + if (timeout) { + clearTimeout(timeout); } - return lastArg.apply(this, arguments); - }; - _args = args.slice(0, args.length - 1).concat([callback]); + return this[i].apply(this, args); + }); } if (debug) { @@ -227,7 +169,7 @@ function iter(i) { } } - this.conn.emit('operation-start', { _id: opId, modelName: _this.modelName, collectionName: this.name, method: i, params: _args }); + this.conn.emit('operation-start', { _id: opId, modelName: _this.modelName, collectionName: this.name, method: i, params: args }); try { if (collection == null) { @@ -237,45 +179,37 @@ function iter(i) { throw new MongooseError(message); } - if (syncCollectionMethods[i] && typeof lastArg === 'function') { - 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); + const ret = collection[i].apply(collection, args); if (ret != null && typeof ret.then === 'function') { return ret.then( 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 }); + if (timeout != null) { + clearTimeout(timeout); } + this.conn.emit('operation-end', { _id: opId, modelName: _this.modelName, collectionName: this.name, method: i, result }); return result; }, 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 }); + if (timeout != null) { + clearTimeout(timeout); } + this.conn.emit('operation-end', { _id: opId, modelName: _this.modelName, collectionName: this.name, method: i, error }); throw error; } ); } + + this.conn.emit('operation-end', { _id: opId, modelName: _this.modelName, collectionName: this.name, method: i, result: ret }); + if (timeout != null) { + clearTimeout(timeout); + } return ret; } catch (error) { - // Collection operation may throw because of max bson size, catch it here - // See gh-3906 - if (typeof lastArg === 'function') { - return lastArg(error); - } else { - this.conn.emit('operation-end', { _id: opId, modelName: _this.modelName, collectionName: this.name, method: i, error: error }); - - throw error; + if (timeout != null) { + clearTimeout(timeout); } + this.conn.emit('operation-end', { _id: opId, modelName: _this.modelName, collectionName: this.name, method: i, error: error }); + throw error; } }; } diff --git a/lib/drivers/node-mongodb-native/connection.js b/lib/drivers/node-mongodb-native/connection.js index 3f4be863c2e..2f64d83f8e4 100644 --- a/lib/drivers/node-mongodb-native/connection.js +++ b/lib/drivers/node-mongodb-native/connection.js @@ -508,7 +508,7 @@ function _setClient(conn, client, options, dbName) { conn.onOpen(); for (const i in conn.collections) { - if (utils.object.hasOwnProperty(conn.collections, i)) { + if (Object.hasOwn(conn.collections, i)) { conn.collections[i].onOpen(); } } diff --git a/lib/helpers/clone.js b/lib/helpers/clone.js index 575d78ca3cd..e2b8af7ab71 100644 --- a/lib/helpers/clone.js +++ b/lib/helpers/clone.js @@ -53,7 +53,7 @@ function clone(obj, options, isArrayChild) { if (obj.__parentArray != null) { clonedDoc.__parentArray = obj.__parentArray; } - clonedDoc.$__parent = obj.$__parent; + clonedDoc.$__setParent(options.parentDoc ?? obj.$__parent); return clonedDoc; } } diff --git a/lib/helpers/common.js b/lib/helpers/common.js index a9c45d50470..391abf8973e 100644 --- a/lib/helpers/common.js +++ b/lib/helpers/common.js @@ -55,7 +55,7 @@ function flatten(update, path, options, schema) { if (isNested) { const paths = Object.keys(schema.paths); for (const p of paths) { - if (p.startsWith(path + key + '.') && !result.hasOwnProperty(p)) { + if (p.startsWith(path + key + '.') && !Object.hasOwn(result, p)) { result[p] = void 0; } } diff --git a/lib/helpers/indexes/applySchemaCollation.js b/lib/helpers/indexes/applySchemaCollation.js index 93a97a48bda..464210e10cf 100644 --- a/lib/helpers/indexes/applySchemaCollation.js +++ b/lib/helpers/indexes/applySchemaCollation.js @@ -7,7 +7,7 @@ module.exports = function applySchemaCollation(indexKeys, indexOptions, schemaOp return; } - if (schemaOptions.hasOwnProperty('collation') && !indexOptions.hasOwnProperty('collation')) { + if (Object.hasOwn(schemaOptions, 'collation') && !Object.hasOwn(indexOptions, 'collation')) { indexOptions.collation = schemaOptions.collation; } }; diff --git a/lib/helpers/indexes/isDefaultIdIndex.js b/lib/helpers/indexes/isDefaultIdIndex.js index 56d74346c6b..8123dfc7d3f 100644 --- a/lib/helpers/indexes/isDefaultIdIndex.js +++ b/lib/helpers/indexes/isDefaultIdIndex.js @@ -14,5 +14,5 @@ module.exports = function isDefaultIdIndex(index) { } const key = get(index, 'key', {}); - return Object.keys(key).length === 1 && key.hasOwnProperty('_id'); + return Object.keys(key).length === 1 && Object.hasOwn(key, '_id'); }; diff --git a/lib/helpers/model/applyMethods.js b/lib/helpers/model/applyMethods.js index a75beceb218..748315aed57 100644 --- a/lib/helpers/model/applyMethods.js +++ b/lib/helpers/model/applyMethods.js @@ -28,7 +28,7 @@ module.exports = function applyMethods(model, schema) { } for (const method of Object.keys(schema.methods)) { const fn = schema.methods[method]; - if (schema.tree.hasOwnProperty(method)) { + if (Object.hasOwn(schema.tree, method)) { throw new Error('You have a method and a property in your schema both ' + 'named "' + method + '"'); } diff --git a/lib/helpers/model/castBulkWrite.js b/lib/helpers/model/castBulkWrite.js index 3f61c4e6ad3..b2abdd4a0dc 100644 --- a/lib/helpers/model/castBulkWrite.js +++ b/lib/helpers/model/castBulkWrite.js @@ -119,7 +119,8 @@ module.exports.castUpdateOne = function castUpdateOne(originalModel, updateOne, const createdAt = model.schema.$timestamps.createdAt; const updatedAt = model.schema.$timestamps.updatedAt; applyTimestampsToUpdate(now, createdAt, updatedAt, update, { - timestamps: updateOne.timestamps + timestamps: updateOne.timestamps, + overwriteImmutable: updateOne.overwriteImmutable }); } @@ -189,7 +190,8 @@ module.exports.castUpdateMany = function castUpdateMany(originalModel, updateMan const createdAt = model.schema.$timestamps.createdAt; const updatedAt = model.schema.$timestamps.updatedAt; applyTimestampsToUpdate(now, createdAt, updatedAt, updateMany['update'], { - timestamps: updateMany.timestamps + timestamps: updateMany.timestamps, + overwriteImmutable: updateMany.overwriteImmutable }); } if (doInitTimestamps) { @@ -295,13 +297,12 @@ function _addDiscriminatorToObject(schema, obj) { function decideModelByObject(model, object) { const discriminatorKey = model.schema.options.discriminatorKey; - if (object != null && object.hasOwnProperty(discriminatorKey)) { + if (object != null && Object.hasOwn(object, discriminatorKey)) { model = getDiscriminatorByValue(model.discriminators, object[discriminatorKey]) || model; } 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 diff --git a/lib/helpers/populate/getModelsMapForPopulate.js b/lib/helpers/populate/getModelsMapForPopulate.js index 7d1b3f47fde..70bbc5513a5 100644 --- a/lib/helpers/populate/getModelsMapForPopulate.js +++ b/lib/helpers/populate/getModelsMapForPopulate.js @@ -386,13 +386,13 @@ function _virtualPopulate(model, docs, options, _virtualRes) { } data.count = virtual.options.count; - if (virtual.options.skip != null && !options.hasOwnProperty('skip')) { + if (virtual.options.skip != null && !Object.hasOwn(options, 'skip')) { options.skip = virtual.options.skip; } - if (virtual.options.limit != null && !options.hasOwnProperty('limit')) { + if (virtual.options.limit != null && !Object.hasOwn(options, 'limit')) { options.limit = virtual.options.limit; } - if (virtual.options.perDocumentLimit != null && !options.hasOwnProperty('perDocumentLimit')) { + if (virtual.options.perDocumentLimit != null && !Object.hasOwn(options, 'perDocumentLimit')) { options.perDocumentLimit = virtual.options.perDocumentLimit; } let foreignField = virtual.options.foreignField; diff --git a/lib/helpers/populate/modelNamesFromRefPath.js b/lib/helpers/populate/modelNamesFromRefPath.js index a5b02859346..875010dccb1 100644 --- a/lib/helpers/populate/modelNamesFromRefPath.js +++ b/lib/helpers/populate/modelNamesFromRefPath.js @@ -56,7 +56,7 @@ module.exports = function modelNamesFromRefPath(refPath, doc, populatedPath, mod const refValue = mpath.get(refPath, doc, lookupLocalFields); let modelNames; - if (modelSchema != null && modelSchema.virtuals.hasOwnProperty(refPath)) { + if (modelSchema != null && Object.hasOwn(modelSchema.virtuals, refPath)) { modelNames = [modelSchema.virtuals[refPath].applyGetters(void 0, doc)]; } else { modelNames = Array.isArray(refValue) ? refValue : [refValue]; diff --git a/lib/helpers/populate/removeDeselectedForeignField.js b/lib/helpers/populate/removeDeselectedForeignField.js index a86e6e3e9f1..069adec7e81 100644 --- a/lib/helpers/populate/removeDeselectedForeignField.js +++ b/lib/helpers/populate/removeDeselectedForeignField.js @@ -16,7 +16,7 @@ module.exports = function removeDeselectedForeignField(foreignFields, options, d return; } for (const foreignField of foreignFields) { - if (!projection.hasOwnProperty('-' + foreignField)) { + if (!Object.hasOwn(projection, '-' + foreignField)) { continue; } diff --git a/lib/helpers/projection/applyProjection.js b/lib/helpers/projection/applyProjection.js index 7a35b128b24..7b8b1a3d772 100644 --- a/lib/helpers/projection/applyProjection.js +++ b/lib/helpers/projection/applyProjection.js @@ -43,7 +43,7 @@ function applyExclusiveProjection(doc, projection, hasIncludedChildren, projecti for (const key of Object.keys(ret)) { const fullPath = prefix ? prefix + '.' + key : key; - if (projection.hasOwnProperty(fullPath) || projectionLimb.hasOwnProperty(key)) { + if (Object.hasOwn(projection, fullPath) || Object.hasOwn(projectionLimb, key)) { if (isPOJO(projection[fullPath]) || isPOJO(projectionLimb[key])) { ret[key] = applyExclusiveProjection(ret[key], projection, hasIncludedChildren, projectionLimb[key], fullPath); } else { @@ -68,7 +68,7 @@ function applyInclusiveProjection(doc, projection, hasIncludedChildren, projecti for (const key of Object.keys(ret)) { const fullPath = prefix ? prefix + '.' + key : key; - if (projection.hasOwnProperty(fullPath) || projectionLimb.hasOwnProperty(key)) { + if (Object.hasOwn(projection, fullPath) || Object.hasOwn(projectionLimb, key)) { if (isPOJO(projection[fullPath]) || isPOJO(projectionLimb[key])) { ret[key] = applyInclusiveProjection(ret[key], projection, hasIncludedChildren, projectionLimb[key], fullPath); } diff --git a/lib/helpers/query/getEmbeddedDiscriminatorPath.js b/lib/helpers/query/getEmbeddedDiscriminatorPath.js index c8b9be7ffeb..2247ea3b445 100644 --- a/lib/helpers/query/getEmbeddedDiscriminatorPath.js +++ b/lib/helpers/query/getEmbeddedDiscriminatorPath.js @@ -71,7 +71,7 @@ module.exports = function getEmbeddedDiscriminatorPath(schema, update, filter, p const schemaKey = updatedPathsByFilter[filterKey] + '.' + key; const arrayFilterKey = filterKey + '.' + key; if (schemaKey === discriminatorFilterPath) { - const filter = arrayFilters.find(filter => filter.hasOwnProperty(arrayFilterKey)); + const filter = arrayFilters.find(filter => Object.hasOwn(filter, arrayFilterKey)); if (filter != null) { discriminatorKey = filter[arrayFilterKey]; } diff --git a/lib/helpers/setDefaultsOnInsert.js b/lib/helpers/setDefaultsOnInsert.js index 6c963bc9b69..643ba968f99 100644 --- a/lib/helpers/setDefaultsOnInsert.js +++ b/lib/helpers/setDefaultsOnInsert.js @@ -136,14 +136,14 @@ function pathExistsInUpdate(update, targetPath, pathPieces) { } // Check exact match - if (update.hasOwnProperty(targetPath)) { + if (Object.hasOwn(update, targetPath)) { return true; } // Check if any parent path exists let cur = pathPieces[0]; for (let i = 1; i < pathPieces.length; ++i) { - if (update.hasOwnProperty(cur)) { + if (Object.hasOwn(update, cur)) { return true; } cur += '.' + pathPieces[i]; diff --git a/lib/helpers/timestamps/setupTimestamps.js b/lib/helpers/timestamps/setupTimestamps.js index cdeca8a2296..b7327ffac5f 100644 --- a/lib/helpers/timestamps/setupTimestamps.js +++ b/lib/helpers/timestamps/setupTimestamps.js @@ -23,7 +23,7 @@ module.exports = function setupTimestamps(schema, timestamps) { } const createdAt = handleTimestampOption(timestamps, 'createdAt'); const updatedAt = handleTimestampOption(timestamps, 'updatedAt'); - const currentTime = timestamps != null && timestamps.hasOwnProperty('currentTime') ? + const currentTime = timestamps != null && Object.hasOwn(timestamps, 'currentTime') ? timestamps.currentTime : null; const schemaAdditions = {}; diff --git a/lib/helpers/update/applyTimestampsToUpdate.js b/lib/helpers/update/applyTimestampsToUpdate.js index e8d3217fbb9..751f967c72f 100644 --- a/lib/helpers/update/applyTimestampsToUpdate.js +++ b/lib/helpers/update/applyTimestampsToUpdate.js @@ -74,39 +74,52 @@ function applyTimestampsToUpdate(now, createdAt, updatedAt, currentUpdate, optio updates.$set[updatedAt] = now; } - if (updates.hasOwnProperty(updatedAt)) { + if (Object.hasOwn(updates, updatedAt)) { delete updates[updatedAt]; } } if (!skipCreatedAt && createdAt) { - if (currentUpdate[createdAt]) { - delete currentUpdate[createdAt]; - } - if (currentUpdate.$set && currentUpdate.$set[createdAt]) { - delete currentUpdate.$set[createdAt]; - } - let timestampSet = false; - if (createdAt.indexOf('.') !== -1) { - const pieces = createdAt.split('.'); - for (let i = 1; i < pieces.length; ++i) { - const remnant = pieces.slice(-i).join('.'); - const start = pieces.slice(0, -i).join('.'); - if (currentUpdate[start] != null) { - currentUpdate[start][remnant] = now; - timestampSet = true; - break; - } else if (currentUpdate.$set && currentUpdate.$set[start]) { - currentUpdate.$set[start][remnant] = now; - timestampSet = true; - break; + const overwriteImmutable = get(options, 'overwriteImmutable', false); + const hasUserCreatedAt = currentUpdate[createdAt] != null || currentUpdate?.$set[createdAt] != null; + + // If overwriteImmutable is true and user provided createdAt, keep their value + if (overwriteImmutable && hasUserCreatedAt) { + // Move createdAt from top-level to $set if needed + if (currentUpdate[createdAt] != null) { + updates.$set[createdAt] = currentUpdate[createdAt]; + delete currentUpdate[createdAt]; + } + // User's value is already in $set, nothing more to do + } else { + if (currentUpdate[createdAt]) { + delete currentUpdate[createdAt]; + } + if (currentUpdate.$set && currentUpdate.$set[createdAt]) { + delete currentUpdate.$set[createdAt]; + } + let timestampSet = false; + if (createdAt.indexOf('.') !== -1) { + const pieces = createdAt.split('.'); + for (let i = 1; i < pieces.length; ++i) { + const remnant = pieces.slice(-i).join('.'); + const start = pieces.slice(0, -i).join('.'); + if (currentUpdate[start] != null) { + currentUpdate[start][remnant] = now; + timestampSet = true; + break; + } else if (currentUpdate.$set && currentUpdate.$set[start]) { + currentUpdate.$set[start][remnant] = now; + timestampSet = true; + break; + } } } - } - if (!timestampSet) { - updates.$setOnInsert = updates.$setOnInsert || {}; - updates.$setOnInsert[createdAt] = now; + if (!timestampSet) { + updates.$setOnInsert = updates.$setOnInsert || {}; + updates.$setOnInsert[createdAt] = now; + } } } diff --git a/lib/helpers/update/decorateUpdateWithVersionKey.js b/lib/helpers/update/decorateUpdateWithVersionKey.js index ee46d8a699f..a982f730ebf 100644 --- a/lib/helpers/update/decorateUpdateWithVersionKey.js +++ b/lib/helpers/update/decorateUpdateWithVersionKey.js @@ -31,5 +31,5 @@ function hasKey(obj, key) { if (obj == null || typeof obj !== 'object') { return false; } - return Object.prototype.hasOwnProperty.call(obj, key); + return Object.hasOwn(obj, key); } diff --git a/lib/model.js b/lib/model.js index 74d990b68c4..bfd4083e3ca 100644 --- a/lib/model.js +++ b/lib/model.js @@ -352,7 +352,7 @@ function _createSaveOptions(doc, options) { const asyncLocalStorage = doc[modelDbSymbol].base.transactionAsyncLocalStorage?.getStore(); if (session != null) { saveOptions.session = session; - } else if (!options.hasOwnProperty('session') && asyncLocalStorage?.session != null) { + } else if (!Object.hasOwn(options, 'session') && asyncLocalStorage?.session != null) { // Only set session from asyncLocalStorage if `session` option wasn't originally passed in options saveOptions.session = asyncLocalStorage.session; } @@ -586,7 +586,7 @@ Model.prototype.save = async function save(options) { } options = new SaveOptions(options); - if (options.hasOwnProperty('session')) { + if (Object.hasOwn(options, 'session')) { this.$session(options.session); } if (this.$__.timestamps != null) { @@ -748,7 +748,7 @@ Model.prototype.deleteOne = function deleteOne(options) { options = {}; } - if (options.hasOwnProperty('session')) { + if (Object.hasOwn(options, 'session')) { this.$session(options.session); } @@ -790,6 +790,12 @@ Model.prototype.deleteOne = function deleteOne(options) { query.post(function queryPostDeleteOne() { return self.constructor._middleware.execPost('deleteOne', self, [self], {}); }); + query.transform(function setIsDeleted(result) { + if (result?.deletedCount > 0) { + self.$isDeleted(true); + } + return result; + }); return query; }; @@ -2944,7 +2950,7 @@ Model.insertMany = async function insertMany(arr, options) { const lean = !!options.lean; const asyncLocalStorage = this.db.base.transactionAsyncLocalStorage?.getStore(); - if ((!options || !options.hasOwnProperty('session')) && asyncLocalStorage?.session != null) { + if ((!options || !Object.hasOwn(options, 'session')) && asyncLocalStorage?.session != null) { options = { ...options, session: asyncLocalStorage.session }; } @@ -3248,12 +3254,14 @@ function _setIsNew(doc, val) { * @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 * @param {Boolean} [ops.updateOne.timestamps=true] If false, do not apply [timestamps](https://mongoosejs.com/docs/guide.html#timestamps) to the operation + * @param {Boolean} [ops.updateOne.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. * @param {Object} [ops.updateOne.collation] The [MongoDB collation](https://thecodebarbarian.com/a-nodejs-perspective-on-mongodb-34-collations) to use * @param {Array} [ops.updateOne.arrayFilters] The [array filters](https://thecodebarbarian.com/a-nodejs-perspective-on-mongodb-36-array-filters.html) used in `update` * @param {Object} [ops.updateMany.filter] Update all the documents that match this filter * @param {Object} [ops.updateMany.update] An object containing [update operators](https://www.mongodb.com/docs/manual/reference/operator/update/) * @param {Boolean} [ops.updateMany.upsert=false] If true, insert a doc if no documents match `filter` * @param {Boolean} [ops.updateMany.timestamps=true] If false, do not apply [timestamps](https://mongoosejs.com/docs/guide.html#timestamps) to the operation + * @param {Boolean} [ops.updateMany.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. * @param {Object} [ops.updateMany.collation] The [MongoDB collation](https://thecodebarbarian.com/a-nodejs-perspective-on-mongodb-34-collations) to use * @param {Array} [ops.updateMany.arrayFilters] The [array filters](https://thecodebarbarian.com/a-nodejs-perspective-on-mongodb-36-array-filters.html) used in `update` * @param {Object} [ops.deleteOne.filter] Delete the first document that matches this filter @@ -3286,12 +3294,15 @@ Model.bulkWrite = async function bulkWrite(ops, options) { } options = options || {}; - [ops, options] = await this.hooks.execPre('bulkWrite', this, [ops, options]).catch(err => { + try { + [ops, options] = await this.hooks.execPre('bulkWrite', this, [ops, options]); + } catch (err) { if (err instanceof Kareem.skipWrappedFunction) { - return [err]; + ops = err; + } else { + await this.hooks.execPost('bulkWrite', this, [null], { error: err }); } - throw err; - }); + } if (ops instanceof Kareem.skipWrappedFunction) { return ops.args[0]; @@ -3309,7 +3320,7 @@ Model.bulkWrite = async function bulkWrite(ops, options) { const validations = options?._skipCastBulkWrite ? [] : ops.map(op => castBulkWrite(this, op, options)); const asyncLocalStorage = this.db.base.transactionAsyncLocalStorage?.getStore(); - if ((!options || !options.hasOwnProperty('session')) && asyncLocalStorage?.session != null) { + if ((!options || !Object.hasOwn(options, 'session')) && asyncLocalStorage?.session != null) { options = { ...options, session: asyncLocalStorage.session }; } @@ -3517,6 +3528,7 @@ async function handleSuccessfulWrite(document) { } document.$__reset(); + document._applyVersionIncrement(); return document.schema.s.hooks.execPost('save', document, [document]); } @@ -4999,10 +5011,10 @@ Model._applyQueryMiddleware = function _applyQueryMiddleware() { function _getContexts(hook) { const ret = {}; - if (hook.hasOwnProperty('query')) { + if (Object.hasOwn(hook, 'query')) { ret.query = hook.query; } - if (hook.hasOwnProperty('document')) { + if (Object.hasOwn(hook, 'document')) { ret.document = hook.document; } return ret; diff --git a/lib/mongoose.js b/lib/mongoose.js index 864b686dd55..9feaf00f76c 100644 --- a/lib/mongoose.js +++ b/lib/mongoose.js @@ -601,12 +601,11 @@ Mongoose.prototype.model = function model(name, schema, collection, options) { // 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') ? + const overwriteModels = Object.hasOwn(_mongoose.options, 'overwriteModels') ? _mongoose.options.overwriteModels : options.overwriteModels; - if (_mongoose.models.hasOwnProperty(name) && options.cache !== false && overwriteModels !== true) { - if (originalSchema && - originalSchema.instanceOfSchema && + if (Object.hasOwn(_mongoose.models, name) && options.cache !== false && overwriteModels !== true) { + if (originalSchema?.instanceOfSchema && originalSchema !== _mongoose.models[name].schema) { throw new _mongoose.Error.OverwriteModelError(name); } diff --git a/lib/query.js b/lib/query.js index 57bc291340f..01931cf759d 100644 --- a/lib/query.js +++ b/lib/query.js @@ -2100,7 +2100,7 @@ Query.prototype._optionsForExec = function(model) { applyWriteConcern(model.schema, options); const asyncLocalStorage = this.model?.db?.base.transactionAsyncLocalStorage?.getStore(); - if (!this.options.hasOwnProperty('session') && asyncLocalStorage?.session != null) { + if (!Object.hasOwn(this.options, 'session') && asyncLocalStorage?.session != null) { options.session = asyncLocalStorage.session; } @@ -4595,7 +4595,7 @@ Query.prototype.exec = async function exec(op) { throw new MongooseError('Query has invalid `op`: "' + this.op + '"'); } - if (this.options && this.options.sort && typeof this.options.sort === 'object' && this.options.sort.hasOwnProperty('')) { + if (this.options && this.options.sort && typeof this.options.sort === 'object' && Object.hasOwn(this.options.sort, '')) { throw new Error('Invalid field "" passed to sort()'); } @@ -4995,7 +4995,7 @@ Query.prototype.cast = function(model, obj) { model = model || this.model; const discriminatorKey = model.schema.options.discriminatorKey; if (obj != null && - obj.hasOwnProperty(discriminatorKey)) { + Object.hasOwn(obj, discriminatorKey)) { model = getDiscriminatorByValue(model.discriminators, obj[discriminatorKey]) || model; } diff --git a/lib/schema.js b/lib/schema.js index d62fe80cdb6..825ba07a839 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -1485,17 +1485,17 @@ Schema.prototype._gatherChildSchemas = function _gatherChildSchemas() { */ function _getPath(schema, path, cleanPath) { - if (schema.paths.hasOwnProperty(path)) { + if (Object.hasOwn(schema.paths, path)) { return schema.paths[path]; } - if (schema.subpaths.hasOwnProperty(cleanPath)) { + if (Object.hasOwn(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') { + if (Object.hasOwn(schema.singleNestedPaths, cleanPath) && typeof schema.singleNestedPaths[cleanPath] === 'object') { const singleNestedPath = schema.singleNestedPaths[cleanPath]; if (singleNestedPath === 'nested') { return undefined; @@ -1683,20 +1683,20 @@ Schema.prototype.interpretAsType = function(path, obj, options) { childSchemaOptions.typeKey = options.typeKey; } // propagate 'strict' option to child schema - if (options.hasOwnProperty('strict')) { + if (Object.hasOwn(options, 'strict')) { childSchemaOptions.strict = options.strict; } - if (options.hasOwnProperty('strictQuery')) { + if (Object.hasOwn(options, 'strictQuery')) { childSchemaOptions.strictQuery = options.strictQuery; } - if (options.hasOwnProperty('toObject')) { + if (Object.hasOwn(options, 'toObject')) { childSchemaOptions.toObject = utils.omit(options.toObject, ['transform']); } - if (options.hasOwnProperty('toJSON')) { + if (Object.hasOwn(options, 'toJSON')) { childSchemaOptions.toJSON = utils.omit(options.toJSON, ['transform']); } - if (this._userProvidedOptions.hasOwnProperty('_id')) { + if (Object.hasOwn(this._userProvidedOptions, '_id')) { childSchemaOptions._id = this._userProvidedOptions._id; } else if (Schema.Types.DocumentArray.defaultOptions._id != null) { childSchemaOptions._id = Schema.Types.DocumentArray.defaultOptions._id; @@ -1733,7 +1733,7 @@ Schema.prototype.interpretAsType = function(path, obj, options) { `Could not determine the embedded type for array \`${path}\`. ` + 'See https://mongoosejs.com/docs/guide.html#definition for more info on supported schema syntaxes.'); } - if (!MongooseTypes.hasOwnProperty(name)) { + if (!Object.hasOwn(MongooseTypes, name)) { throw new TypeError('Invalid schema configuration: ' + `\`${name}\` is not a valid type within the array \`${path}\`.` + 'See https://bit.ly/mongoose-schematypes for a list of valid schema types.'); @@ -1894,24 +1894,24 @@ Schema.prototype.indexedPaths = function indexedPaths() { */ Schema.prototype.pathType = function(path) { - if (this.paths.hasOwnProperty(path)) { + if (Object.hasOwn(this.paths, path)) { return 'real'; } - if (this.virtuals.hasOwnProperty(path)) { + if (Object.hasOwn(this.virtuals, path)) { return 'virtual'; } - if (this.nested.hasOwnProperty(path)) { + if (Object.hasOwn(this.nested, path)) { return 'nested'; } // Convert to '.$' to check subpaths re: gh-6405 const cleanPath = _pathToPositionalSyntax(path); - if (this.subpaths.hasOwnProperty(cleanPath) || this.subpaths.hasOwnProperty(path)) { + if (Object.hasOwn(this.subpaths, cleanPath) || Object.hasOwn(this.subpaths, path)) { return 'real'; } - const singleNestedPath = this.singleNestedPaths.hasOwnProperty(cleanPath) || this.singleNestedPaths.hasOwnProperty(path); + const singleNestedPath = Object.hasOwn(this.singleNestedPaths, cleanPath) || Object.hasOwn(this.singleNestedPaths, path); if (singleNestedPath) { return singleNestedPath === 'nested' ? 'nested' : 'real'; } @@ -1941,7 +1941,7 @@ Schema.prototype.hasMixedParent = function(path) { path = ''; for (let i = 0; i < subpaths.length; ++i) { path = i > 0 ? path + '.' + subpaths[i] : subpaths[i]; - if (this.paths.hasOwnProperty(path) && + if (Object.hasOwn(this.paths, path) && this.paths[path] instanceof MongooseTypes.Mixed) { return this.paths[path]; } @@ -1970,7 +1970,7 @@ Schema.prototype.setupTimestamp = function(timestamps) { function getPositionalPathType(self, path, cleanPath) { const subpaths = path.split(/\.(\d+)\.|\.(\d+)$/).filter(Boolean); if (subpaths.length < 2) { - return self.paths.hasOwnProperty(subpaths[0]) ? + return Object.hasOwn(self.paths, subpaths[0]) ? self.paths[subpaths[0]] : 'adhocOrUndefined'; } @@ -2675,7 +2675,7 @@ Schema.prototype.virtual = function(name, options) { */ Schema.prototype.virtualpath = function(name) { - return this.virtuals.hasOwnProperty(name) ? this.virtuals[name] : null; + return Object.hasOwn(this.virtuals, name) ? this.virtuals[name] : null; }; /** @@ -2823,8 +2823,8 @@ Schema.prototype.loadClass = function(model, virtualsOnly) { // Stop copying when hit certain base classes if (model === Object.prototype || model === Function.prototype || - model.prototype.hasOwnProperty('$isMongooseModelPrototype') || - model.prototype.hasOwnProperty('$isMongooseDocumentPrototype')) { + Object.hasOwn(model.prototype, '$isMongooseModelPrototype') || + Object.hasOwn(model.prototype, '$isMongooseDocumentPrototype')) { return this; } @@ -2837,7 +2837,7 @@ Schema.prototype.loadClass = function(model, virtualsOnly) { return; } const prop = Object.getOwnPropertyDescriptor(model, name); - if (prop.hasOwnProperty('value')) { + if (Object.hasOwn(prop, 'value')) { this.static(name, prop.value); } }, this); @@ -3063,7 +3063,7 @@ Schema.prototype._transformDuplicateKeyError = function _transformDuplicateKeyEr return error; } const firstKey = keys[0]; - if (!this._duplicateKeyErrorMessagesByPath.hasOwnProperty(firstKey)) { + if (!Object.hasOwn(this._duplicateKeyErrorMessagesByPath, firstKey)) { return error; } return new MongooseError(this._duplicateKeyErrorMessagesByPath[firstKey], { cause: error }); diff --git a/lib/schema/array.js b/lib/schema/array.js index 14b60f22ea2..3d0e325c839 100644 --- a/lib/schema/array.js +++ b/lib/schema/array.js @@ -80,7 +80,7 @@ function SchemaArray(key, cast, options, schemaOptions, parentSchema) { : utils.getFunctionName(cast); const Types = require('./index.js'); - const schemaTypeDefinition = Types.hasOwnProperty(name) ? Types[name] : cast; + const schemaTypeDefinition = Object.hasOwn(Types, name) ? Types[name] : cast; if (typeof schemaTypeDefinition === 'function') { if (schemaTypeDefinition === SchemaArray) { diff --git a/lib/schema/number.js b/lib/schema/number.js index 37cdc246df6..33d9615fb9f 100644 --- a/lib/schema/number.js +++ b/lib/schema/number.js @@ -26,6 +26,7 @@ const CastError = SchemaType.CastError; */ function SchemaNumber(key, options, _schemaOptions, parentSchema) { + this.enumValues = []; SchemaType.call(this, key, options, 'Number', parentSchema); } @@ -316,8 +317,12 @@ SchemaNumber.prototype.enum = function(values, message) { this.validators = this.validators.filter(function(v) { return v.validator !== this.enumValidator; }, this); + this.enumValidator = false; } + if (values === void 0 || values === false) { + return this; + } if (!Array.isArray(values)) { const isObjectSyntax = utils.isPOJO(values) && values.values != null; @@ -337,12 +342,19 @@ SchemaNumber.prototype.enum = function(values, message) { message = message == null ? MongooseError.messages.Number.enum : message; - this.enumValidator = v => v == null || values.indexOf(v) !== -1; + for (const value of values) { + if (value !== undefined) { + this.enumValues.push(this.cast(value)); + } + } + + const vals = this.enumValues; + this.enumValidator = v => v == null || vals.indexOf(v) !== -1; this.validators.push({ validator: this.enumValidator, message: message, type: 'enum', - enumValues: values + enumValues: vals }); return this; diff --git a/lib/schema/operators/text.js b/lib/schema/operators/text.js index 79be4ff7cb6..e8d5340ca8b 100644 --- a/lib/schema/operators/text.js +++ b/lib/schema/operators/text.js @@ -28,7 +28,7 @@ module.exports = function castTextSearch(val, path) { } if (val.$caseSensitive != null) { val.$caseSensitive = castBoolean(val.$caseSensitive, - path + '.$castSensitive'); + path + '.$caseSensitive'); } if (val.$diacriticSensitive != null) { val.$diacriticSensitive = castBoolean(val.$diacriticSensitive, diff --git a/lib/schemaType.js b/lib/schemaType.js index 063c5b73047..9a77e31dad8 100644 --- a/lib/schemaType.js +++ b/lib/schemaType.js @@ -46,10 +46,10 @@ function SchemaType(path, options, instance, parentSchema) { this.instance = instance; this.schemaName = this.constructor.schemaName; this.validators = []; - this.getters = this.constructor.hasOwnProperty('getters') ? + this.getters = Object.hasOwn(this.constructor, 'getters') ? this.constructor.getters.slice() : []; - this.setters = this.constructor.hasOwnProperty('setters') ? + this.setters = Object.hasOwn(this.constructor, 'setters') ? this.constructor.setters.slice() : []; @@ -62,7 +62,7 @@ function SchemaType(path, options, instance, parentSchema) { for (const option of defaultOptionsKeys) { if (option === 'validate') { this.validate(defaultOptions.validate); - } else if (defaultOptions.hasOwnProperty(option) && !Object.prototype.hasOwnProperty.call(options, option)) { + } else if (Object.hasOwn(defaultOptions, option) && !Object.hasOwn(options, option)) { options[option] = defaultOptions[option]; } } @@ -339,7 +339,7 @@ SchemaType.prototype.cast = function cast() { */ SchemaType.set = function set(option, value) { - if (!this.hasOwnProperty('defaultOptions')) { + if (!Object.hasOwn(this, 'defaultOptions')) { this.defaultOptions = Object.assign({}, this.defaultOptions); } this.defaultOptions[option] = value; @@ -362,7 +362,7 @@ SchemaType.set = function set(option, value) { */ SchemaType.get = function(getter) { - this.getters = this.hasOwnProperty('getters') ? this.getters : []; + this.getters = Object.hasOwn(this, 'getters') ? this.getters : []; this.getters.push(getter); }; @@ -496,7 +496,7 @@ SchemaType.prototype.unique = function unique(value, message) { 'false and `unique` set to true'); } - if (!this.options.hasOwnProperty('index') && value === false) { + if (!Object.hasOwn(this.options, 'index') && value === false) { return this; } @@ -535,7 +535,7 @@ SchemaType.prototype.text = function(bool) { 'false and `text` set to true'); } - if (!this.options.hasOwnProperty('index') && bool === false) { + if (!Object.hasOwn(this.options, 'index') && bool === false) { return this; } @@ -572,7 +572,7 @@ SchemaType.prototype.sparse = function(bool) { 'false and `sparse` set to true'); } - if (!this.options.hasOwnProperty('index') && bool === false) { + if (!Object.hasOwn(this.options, 'index') && bool === false) { return this; } diff --git a/lib/types/array/index.js b/lib/types/array/index.js index b21c23bfd12..2466ce5ad51 100644 --- a/lib/types/array/index.js +++ b/lib/types/array/index.js @@ -79,13 +79,13 @@ function MongooseArray(values, path, doc, schematype) { const proxy = new Proxy(__array, { get: function(target, prop) { - if (internals.hasOwnProperty(prop)) { + if (Object.hasOwn(internals, prop)) { return internals[prop]; } - if (mongooseArrayMethods.hasOwnProperty(prop)) { + if (Object.hasOwn(mongooseArrayMethods, prop)) { return mongooseArrayMethods[prop]; } - if (schematype && schematype.virtuals && schematype.virtuals.hasOwnProperty(prop)) { + if (schematype && schematype.virtuals && Object.hasOwn(schematype.virtuals, prop)) { return schematype.virtuals[prop].applyGetters(undefined, target); } if (typeof prop === 'string' && numberRE.test(prop) && schematype?.embeddedSchemaType != null) { @@ -97,9 +97,9 @@ function MongooseArray(values, path, doc, schematype) { set: function(target, prop, value) { if (typeof prop === 'string' && numberRE.test(prop)) { mongooseArrayMethods.set.call(proxy, prop, value, false); - } else if (internals.hasOwnProperty(prop)) { + } else if (Object.hasOwn(internals, prop)) { internals[prop] = value; - } else if (schematype && schematype.virtuals && schematype.virtuals.hasOwnProperty(prop)) { + } else if (schematype?.virtuals && Object.hasOwn(schematype.virtuals, prop)) { schematype.virtuals[prop].applySetters(value, target); } else { __array[prop] = value; diff --git a/lib/types/arraySubdocument.js b/lib/types/arraySubdocument.js index a723bc51fe4..a6f6d27d491 100644 --- a/lib/types/arraySubdocument.js +++ b/lib/types/arraySubdocument.js @@ -169,6 +169,16 @@ ArraySubdocument.prototype.$parent = function() { return this[documentArrayParent]; }; +/*! + * Sets this sub-documents parent document. + * + * @api private + */ + +ArraySubdocument.prototype.$__setParent = function $__setParent(parent) { + this[documentArrayParent] = parent; +}; + /** * Returns this subdocument's parent array. * diff --git a/lib/types/documentArray/index.js b/lib/types/documentArray/index.js index ccc0d230fdb..8d30cc96714 100644 --- a/lib/types/documentArray/index.js +++ b/lib/types/documentArray/index.js @@ -73,16 +73,16 @@ function MongooseDocumentArray(values, path, doc, schematype) { prop === 'isMongooseDocumentArrayProxy') { return true; } - if (internals.hasOwnProperty(prop)) { + if (Object.hasOwn(internals, prop)) { return internals[prop]; } - if (DocumentArrayMethods.hasOwnProperty(prop)) { + if (Object.hasOwn(DocumentArrayMethods, prop)) { return DocumentArrayMethods[prop]; } - if (schematype && schematype.virtuals && schematype.virtuals.hasOwnProperty(prop)) { + if (schematype && schematype.virtuals && Object.hasOwn(schematype.virtuals, prop)) { return schematype.virtuals[prop].applyGetters(undefined, target); } - if (ArrayMethods.hasOwnProperty(prop)) { + if (Object.hasOwn(ArrayMethods, prop)) { return ArrayMethods[prop]; } @@ -91,9 +91,9 @@ function MongooseDocumentArray(values, path, doc, schematype) { set: function(target, prop, value) { if (typeof prop === 'string' && numberRE.test(prop)) { DocumentArrayMethods.set.call(proxy, prop, value, false); - } else if (internals.hasOwnProperty(prop)) { + } else if (Object.hasOwn(internals, prop)) { internals[prop] = value; - } else if (schematype && schematype.virtuals && schematype.virtuals.hasOwnProperty(prop)) { + } else if (schematype?.virtuals && Object.hasOwn(schematype.virtuals, prop)) { schematype.virtuals[prop].applySetters(value, target); } else { __array[prop] = value; diff --git a/lib/types/objectid.js b/lib/types/objectid.js index 5544c243f6e..f0a9b259036 100644 --- a/lib/types/objectid.js +++ b/lib/types/objectid.js @@ -30,7 +30,7 @@ Object.defineProperty(ObjectId.prototype, '_id', { * Convenience `valueOf()` to allow comparing ObjectIds using double equals re: gh-7299 */ -if (!ObjectId.prototype.hasOwnProperty('valueOf')) { +if (!Object.hasOwn(ObjectId.prototype, 'valueOf')) { ObjectId.prototype.valueOf = function objectIdValueOf() { return this.toString(); }; diff --git a/lib/types/subdocument.js b/lib/types/subdocument.js index 5d39e1c214e..7bca17654ff 100644 --- a/lib/types/subdocument.js +++ b/lib/types/subdocument.js @@ -356,6 +356,16 @@ Subdocument.prototype.parent = function() { Subdocument.prototype.$parent = Subdocument.prototype.parent; +/*! + * Sets this sub-documents parent document. + * + * @api private + */ + +Subdocument.prototype.$__setParent = function $__setParent(parent) { + this.$__parent = parent; +}; + /** * ignore * @method $__removeFromParent diff --git a/lib/utils.js b/lib/utils.js index 1cef22784c4..2df06e6af1a 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -711,18 +711,6 @@ exports.object.vals = function vals(o) { return ret; }; -const hop = Object.prototype.hasOwnProperty; - -/** - * Safer helper for hasOwnProperty checks - * - * @param {Object} obj - * @param {String} prop - */ - -exports.object.hasOwnProperty = function(obj, prop) { - return hop.call(obj, prop); -}; /** * Determine if `val` is null or undefined @@ -773,8 +761,6 @@ exports.array.flatten = function flatten(arr, filter, ret) { * ignore */ -const _hasOwnProperty = Object.prototype.hasOwnProperty; - exports.hasUserDefinedProperty = function(obj, key) { if (obj == null) { return false; @@ -789,7 +775,7 @@ exports.hasUserDefinedProperty = function(obj, key) { return false; } - if (_hasOwnProperty.call(obj, key)) { + if (Object.hasOwn(obj, key)) { return true; } if (typeof obj === 'object' && key in obj) { @@ -875,15 +861,7 @@ exports.buffer.areEqual = function(a, b) { if (!Buffer.isBuffer(b)) { return false; } - if (a.length !== b.length) { - return false; - } - for (let i = 0, len = a.length; i < len; ++i) { - if (a[i] !== b[i]) { - return false; - } - } - return true; + return a.equals(b); }; exports.getFunctionName = getFunctionName; diff --git a/lib/virtualType.js b/lib/virtualType.js index 2008ebf8bb4..30cc35d5a8a 100644 --- a/lib/virtualType.js +++ b/lib/virtualType.js @@ -143,7 +143,7 @@ VirtualType.prototype.set = function(fn) { VirtualType.prototype.applyGetters = function(value, doc) { if (utils.hasUserDefinedProperty(this.options, ['ref', 'refPath']) && doc.$$populatedVirtuals && - doc.$$populatedVirtuals.hasOwnProperty(this.path)) { + Object.hasOwn(doc.$$populatedVirtuals, this.path)) { value = doc.$$populatedVirtuals[this.path]; } diff --git a/package.json b/package.json index 83770bb1df6..c60624619c8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "9.0.0", + "version": "9.0.2", "author": "Guillermo Rauch ", "keywords": [ "mongodb", @@ -28,7 +28,7 @@ "sift": "17.1.3" }, "devDependencies": { - "@ark/attest": "0.53.0", + "@ark/attest": "0.55.0", "@mongodb-js/mongodb-downloader": "^1.0.0", "acquit": "1.4.0", "acquit-ignore": "0.2.1", @@ -37,7 +37,7 @@ "broken-link-checker": "^0.7.8", "cheerio": "1.1.2", "dox": "1.0.0", - "eslint": "9.25.1", + "eslint": "9.39.1", "eslint-plugin-markdown": "^5.1.0", "eslint-plugin-mocha-no-only": "1.2.0", "express": "^4.19.2", @@ -45,12 +45,12 @@ "highlight.js": "11.11.1", "lodash.isequal": "4.5.0", "lodash.isequalwith": "4.4.0", - "markdownlint-cli2": "^0.18.1", + "markdownlint-cli2": "^0.19.1", "marked": "15.x", "mkdirp": "^3.0.1", - "mocha": "11.7.4", + "mocha": "11.7.5", "moment": "2.30.1", - "mongodb-memory-server": "10.3.0", + "mongodb-memory-server": "11.0.0", "mongodb-runner": "^6.0.0", "mongodb-client-encryption": "~7.0", "ncp": "^2.0.0", @@ -99,9 +99,10 @@ "test-tsd": "node ./test/types/check-types-filename && tsd --full", "setup-test-encryption": "node scripts/setup-encryption-tests.js", "test-encryption": "mocha --exit ./test/encryption/*.test.js", - "tdd": "mocha ./test/*.test.js --inspect --watch --recursive --watch-files ./**/*.{js,ts}", + "tdd": "mocha --watch --inspect --recursive ./test/*.test.js --watch-files lib/**/*.js test/**/*.js", "test-coverage": "nyc --reporter=html --reporter=text npm test", "ts-benchmark": "cd ./benchmarks/typescript/simple && npm install && npm run benchmark | node ../../../scripts/tsc-diagnostics-check", + "ts-benchmark:local": "node ./scripts/create-tarball && cd ./benchmarks/typescript/simple && rm -rf ./node_modules && npm install && npm run benchmark | node ../../../scripts/tsc-diagnostics-check", "attest-benchmark": "node ./benchmarks/typescript/infer.bench.mts" }, "main": "./index.js", diff --git a/scripts/tsc-diagnostics-check.js b/scripts/tsc-diagnostics-check.js index 79435503cd0..4842682d569 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]) ? 375000 : parseInt(process.argv[2], 10); +const maxInstantiations = isNaN(process.argv[2]) ? 400000 : parseInt(process.argv[2], 10); console.log(stdin); diff --git a/test/collection.test.js b/test/collection.test.js index 7e85135a7f5..343c9b5704d 100644 --- a/test/collection.test.js +++ b/test/collection.test.js @@ -51,17 +51,18 @@ describe('collections:', function() { await db.close(); }); - it('returns a promise if buffering and callback with find() (gh-14184)', function(done) { + it('returns a promise if buffering and callback with find() (gh-14184)', function() { 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(); - }); + return collection.find({ foo: 'bar' }, {}).then( + () => assert.ok(false), + err => { + assert.ok(err); + assert.ok(err.message.includes('buffering timed out after 100ms')); + } + ); }); it('handles bufferTimeoutMS in schemaUserProvidedOptions', async function() { diff --git a/test/document.test.js b/test/document.test.js index beb8c890244..5f234a5edad 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -150,6 +150,41 @@ describe('document', function() { const found = await Test.findOne({ _id: doc._id }); assert.strictEqual(found, null); }); + + it('sets $isDeleted to true after successful delete (gh-15858)', async function() { + const schema = new Schema({ name: String }); + const Product = db.model('Test', schema); + + const product = await Product.create({ name: 'test product' }); + assert.strictEqual(product.$isDeleted(), false); + + const result = await product.deleteOne(); + assert.strictEqual(product.$isDeleted(), true); + assert.strictEqual(result.deletedCount, 1); + + // Verify document was actually deleted + const found = await Product.findById(product._id); + assert.strictEqual(found, null); + + // Verify deleteOne is a no-op when $isDeleted is true + const result2 = await product.deleteOne(); + assert.strictEqual(result2, undefined); + assert.strictEqual(product.$isDeleted(), true); + }); + + it('does not set $isDeleted if delete fails', async function() { + const schema = new Schema({ name: String }); + const Product = db.model('Test', schema); + + const product = await Product.create({ name: 'test product' }); + await Product.deleteOne({ _id: product._id }); // Delete using static method + + assert.strictEqual(product.$isDeleted(), false); + + // Try to delete again - should result in 0 deletedCount + await product.deleteOne(); + assert.strictEqual(product.$isDeleted(), false); + }); }); describe('updateOne', function() { @@ -12348,11 +12383,65 @@ describe('document', function() { assert.ok(clonedDoc.arr.isMongooseArray); assert.ok(!clonedDoc.arr.isMongooseDocumentArray); - assert.deepEqual(doc.subdocArray[0], clonedDoc.subdocArray[0]); - assert.deepEqual(doc.subdoc, clonedDoc.subdoc); + assert.deepEqual(doc.subdocArray[0].toObject(), clonedDoc.subdocArray[0].toObject()); + assert.deepEqual(doc.subdoc.toObject(), clonedDoc.subdoc.toObject()); assert.deepEqual(doc.arr, [99]); }); + it('updates subdocument parents when cloning (gh-15901)', async function() { + const addressSchema = new Schema({ + street: String, + city: String, + image: new Schema({ url: String }) + }); + + const userSchema = new Schema({ + name: String, + addresses: [addressSchema], + bestFriend: { type: Schema.Types.ObjectId, ref: 'User' } + }); + + const User = db.model('User', userSchema); + + const bestFriend = new User({ + name: 'Best Friend' + }); + + const user = new User({ + name: 'Test User', + addresses: [{ street: '123 Main St', city: 'Boston', image: { url: 'google.com' } }], + bestFriend: bestFriend._id + }); + + user.bestFriend = bestFriend; + + assert.ok(user.populated('bestFriend')); + assert.ok(user.bestFriend instanceof User); + + const clonedUser = user.$clone(); + + // Check cloned subdoc parent pointers + assert.ok( + clonedUser.addresses[0].$parent() === clonedUser, + 'cloned subdocument $parent() should return cloned document' + ); + assert.ok( + clonedUser.addresses[0].image.$parent() === clonedUser.addresses[0], + 'cloned nested subdocument $parent() should return cloned subdocument' + ); + + // Check that cloning with populated path works + assert.ok(clonedUser.populated('bestFriend')); + assert.ok(clonedUser.bestFriend instanceof User); + assert.notStrictEqual(clonedUser.bestFriend, bestFriend); // clone should not share instance + assert.deepStrictEqual(clonedUser.bestFriend.toObject(), bestFriend.toObject()); + assert.equal( + clonedUser.bestFriend.$parent(), + clonedUser, + 'populated doc $parent() should return cloned document' + ); + }); + 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 } diff --git a/test/model.insertMany.test.js b/test/model.insertMany.test.js index 181b984b4e2..b12cac2a3a8 100644 --- a/test/model.insertMany.test.js +++ b/test/model.insertMany.test.js @@ -738,4 +738,77 @@ describe('insertMany()', function() { assert.equal(err.message, 'postInsertManyError'); assert.ok(err.stack.includes('postInsertManyError')); }); + + describe('pre-hook errors should propagate', function() { + it('insertMany() should throw when pre-hook throws an error', async function() { + // Arrange + const preHookError = new Error('Pre-hook error - should stop insertMany'); + const { User } = createTestContext({ preHookError }); + + // Act + const error = await User.insertMany([{ name: 'test1' }, { name: 'test2' }]).then(() => null, err => err); + + // Assert + assert.ok(error); + assert.equal(error.message, preHookError.message); + }); + + it('insertMany() should not insert documents when pre-hook throws', async function() { + // Arrange + const preHookError = new Error('Pre-hook error - should stop insertMany'); + const { User } = createTestContext({ preHookError }); + + // Act + await User.insertMany([{ name: 'test1' }, { name: 'test2' }]).catch(() => {}); + + // Assert + const count = await User.countDocuments(); + assert.equal(count, 0); + }); + + it('insertMany() should call error post hook when pre-hook throws', async function() { + // Arrange + let errorPostHookCalled = false; + let normalPostHookCalled = false; + const preHookError = new Error('Pre-hook error - should stop insertMany'); + const { User } = createTestContext({ + preHookError, + postHook: function() { + normalPostHookCalled = true; + }, + errorPostHook: function(error, _docs, next) { + if (error && error.message === preHookError.message) { + errorPostHookCalled = true; + } + next(error); + } + }); + + // Act + await User.insertMany([{ name: 'test1' }]).catch(() => {}); + + // Assert + assert.equal(errorPostHookCalled, true); + assert.equal(normalPostHookCalled, false); + }); + + function createTestContext(options) { + const schema = new Schema({ name: String }); + + schema.pre('insertMany', function() { + throw options.preHookError; + }); + + if (options.postHook) { + schema.post('insertMany', options.postHook); + } + + if (options.errorPostHook) { + schema.post('insertMany', options.errorPostHook); + } + + const User = db.model('User', schema); + return { User }; + } + }); }); diff --git a/test/model.middleware.test.js b/test/model.middleware.test.js index 5c49e70c755..d6bfcb5e118 100644 --- a/test/model.middleware.test.js +++ b/test/model.middleware.test.js @@ -624,5 +624,86 @@ describe('model middleware', function() { }]); assert.strictEqual(res, 'skipMiddlewareFunction test'); }); + + describe('pre-hook errors should propagate (gh-15881)', function() { + for (const ordered of [true, false]) { + it(`bulkWrite() should throw when pre-hook throws an error (ordered: ${ordered})`, async function() { + // Arrange + const preHookError = new Error('Pre-hook error - should stop bulkWrite'); + const { User } = createTestContext({ preHookError }); + + // Act + const error = await User.bulkWrite([{ + insertOne: { document: { name: 'Sam' } } + }], { ordered }).then(() => null, err => err); + + // Assert + assert.ok(error); + assert.equal(error.message, preHookError.message); + }); + } + + it('bulkWrite() should not execute operations when pre-hook throws', async function() { + // Arrange + const preHookError = new Error('Pre-hook error - should stop bulkWrite'); + const { User } = createTestContext({ preHookError }); + + // Act + await User.bulkWrite([{ + insertOne: { document: { name: 'Sam' } } + }]).catch(() => null); + + // Assert + const count = await User.countDocuments(); + assert.equal(count, 0); + }); + + it('bulkWrite() should call error post hook when pre-hook throws', async function() { + // Arrange + let errorPostHookCalled = false; + let normalPostHookCalled = false; + const preHookError = new Error('Pre-hook error - should stop bulkWrite'); + const { User } = createTestContext({ + preHookError, + postHook: function() { + normalPostHookCalled = true; + }, + errorPostHook: function(error, _res, next) { + if (error && error.message === preHookError.message) { + errorPostHookCalled = true; + } + next(error); + } + }); + + // Act + await User.bulkWrite([{ + insertOne: { document: { name: 'Sam' } } + }]).catch(() => {}); + + // Assert + assert.equal(errorPostHookCalled, true); + assert.equal(normalPostHookCalled, false); + }); + + function createTestContext(options) { + const schema = new Schema({ name: String }); + + schema.pre('bulkWrite', function() { + throw options.preHookError; + }); + + if (options.postHook) { + schema.post('bulkWrite', options.postHook); + } + + if (options.errorPostHook) { + schema.post('bulkWrite', options.errorPostHook); + } + + const User = db.model('User', schema); + return { User }; + } + }); }); }); diff --git a/test/model.test.js b/test/model.test.js index 109503249b0..6ad049a408a 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -21,6 +21,7 @@ const ObjectId = Schema.Types.ObjectId; const DocumentObjectId = mongoose.Types.ObjectId; const EmbeddedDocument = mongoose.Types.Subdocument; const MongooseError = mongoose.Error; +const ObjectParameterError = require('../lib/error/objectParameter'); describe('Model', function() { let db; @@ -6376,6 +6377,52 @@ describe('Model', function() { }); + it('bulkWrite can disable timestamps with insertOne and replaceOne (gh-15782)', async function() { + const userSchema = new Schema({ + name: String + }, { timestamps: true }); + + const User = db.model('User', userSchema); + + const user = await User.create({ name: 'Hafez' }); + + await User.bulkWrite([ + { insertOne: { document: { name: 'insertOne-test' }, timestamps: false } }, + { replaceOne: { filter: { _id: user._id }, replacement: { name: 'replaceOne-test' }, timestamps: false } } + ]); + + const insertedDoc = await User.findOne({ name: 'insertOne-test' }); + assert.strictEqual(insertedDoc.createdAt, undefined); + assert.strictEqual(insertedDoc.updatedAt, undefined); + + const replacedDoc = await User.findOne({ name: 'replaceOne-test' }); + assert.strictEqual(replacedDoc.createdAt, undefined); + assert.strictEqual(replacedDoc.updatedAt, undefined); + }); + + it('bulkWrite insertOne and replaceOne respect per-op timestamps: true when global is false (gh-15782)', async function() { + const userSchema = new Schema({ + name: String + }, { timestamps: true }); + + const User = db.model('User', userSchema); + + const user = await User.create({ name: 'Hafez' }); + + await User.bulkWrite([ + { insertOne: { document: { name: 'insertOne-test' }, timestamps: true } }, + { replaceOne: { filter: { _id: user._id }, replacement: { name: 'replaceOne-test' }, timestamps: true } } + ], { timestamps: false }); + + const insertedDoc = await User.findOne({ name: 'insertOne-test' }); + assert.ok(insertedDoc.createdAt instanceof Date); + assert.ok(insertedDoc.updatedAt instanceof Date); + + const replacedDoc = await User.findOne({ name: 'replaceOne-test' }); + assert.ok(replacedDoc.createdAt instanceof Date); + assert.ok(replacedDoc.updatedAt instanceof Date); + }); + it('bulkwrite should not change updatedAt on subdocs when timestamps set to false (gh-13611)', async function() { const postSchema = new Schema({ @@ -7179,6 +7226,53 @@ describe('Model', function() { ); }); + it('increments version key on successful save (gh-15800)', async function() { + // Arrange + const userSchema = new Schema({ + name: [String], + email: { type: String, minLength: 3 } + }); + + const User = db.model('User', userSchema); + const user1 = new User({ name: ['123'], email: '12314' }); + await user1.save(); + + // Act + const user = await User.findOne({ _id: user1._id }); + assert.ok(user); + + // Before, __v should be 0 + assert.equal(user.__v, 0); + + // markModified on array field (triggers $set) + user.markModified('name'); + await User.bulkSave([user]); + + const dbUser1 = await User.findById(user._id); + assert.equal(dbUser1.__v, 1); + assert.equal(user.__v, 1); + + // Update another path and markModified + user.email = '1375'; + await User.bulkSave([user]); + const dbUser2 = await User.findById(user._id); + assert.equal(dbUser2.__v, 1); + assert.equal(user.__v, 1); + + let reloaded = await User.findById(user._id); + assert.equal(reloaded.__v, 1); + + user.email = '1'; + await assert.rejects( + () => User.bulkSave([user]), + /email.*is shorter than the minimum allowed length/ + ); + assert.equal(user.__v, 1); + + reloaded = await User.findById(user._id); + assert.equal(reloaded.__v, 1); + }); + it('saves new documents with ordered: false (gh-15495)', async function() { const userSchema = new Schema({ name: { type: String } @@ -9164,6 +9258,31 @@ describe('Model', function() { assert.strictEqual(doc.name, 'Test2'); }); }); + describe('gh-15812', function() { + it('should throw ObjectParameterError when init is called with null', function() { + const doc = new mongoose.Document({}, new mongoose.Schema({ name: String })); + try { + doc.init(null); + assert.fail('Should have thrown an error'); + } catch (error) { + assert.ok(error instanceof ObjectParameterError); + assert.strictEqual(error.name, 'ObjectParameterError'); + assert.ok(error.message.includes('Parameter "doc" to init() must be an object')); + } + }); + + it('should throw ObjectParameterError when init is called with undefined', function() { + const doc = new mongoose.Document({}, new mongoose.Schema({ name: String })); + try { + doc.init(undefined); + assert.fail('Should have thrown an error'); + } catch (error) { + assert.ok(error instanceof ObjectParameterError); + assert.strictEqual(error.name, 'ObjectParameterError'); + assert.ok(error.message.includes('Parameter "doc" to init() must be an object')); + } + }); + }); }); diff --git a/test/model.updateOne.test.js b/test/model.updateOne.test.js index aef03e3d375..427d3438adf 100644 --- a/test/model.updateOne.test.js +++ b/test/model.updateOne.test.js @@ -2680,34 +2680,270 @@ describe('model: updateOne: ', function() { assert.equal(doc.age, 20); }); - it('overwriting immutable createdAt with bulkWrite (gh-15781)', async function() { - const start = new Date().valueOf(); - const schema = Schema({ - createdAt: { - type: mongoose.Schema.Types.Date, - immutable: true - }, - name: String - }, { timestamps: true }); + describe('bulkWrite overwriteImmutable option (gh-15781)', function() { + it('updateOne can update immutable field with overwriteImmutable: true', async function() { + // Arrange + const { User } = createTestContext(); + const user = await User.create({ name: 'John', ssn: '123-45-6789' }); + const customCreatedAt = new Date('2020-01-01'); + + // Act + await User.bulkWrite([{ + updateOne: { + filter: { _id: user._id }, + update: { createdAt: customCreatedAt, ssn: '999-99-9999' }, + overwriteImmutable: true + } + }]); + + // Assert + const updatedUser = await User.findById(user._id); + assert.strictEqual(updatedUser.ssn, '999-99-9999'); + assert.strictEqual(updatedUser.createdAt.valueOf(), customCreatedAt.valueOf()); + }); + + it('updateMany can update immutable field with overwriteImmutable: true', async function() { + // Arrange + const { User } = createTestContext(); + const user = await User.create({ name: 'Alice', ssn: '111-11-1111' }); + const customCreatedAt = new Date('2020-01-01'); + + // Act + await User.bulkWrite([{ + updateMany: { + filter: { _id: user._id }, + update: { createdAt: customCreatedAt, ssn: '000-00-0000' }, + overwriteImmutable: true + } + }]); - const Model = db.model('Test', schema); + // Assert + const updatedUser = await User.findById(user._id); + assert.strictEqual(updatedUser.ssn, '000-00-0000'); + assert.strictEqual(updatedUser.createdAt.valueOf(), customCreatedAt.valueOf()); + }); - await Model.create({ name: 'gh-15781' }); - let doc = await Model.collection.findOne({ name: 'gh-15781' }); - assert.ok(doc.createdAt.valueOf() >= start); + for (const timestamps of [true, false, null, undefined]) { + it(`overwriting immutable createdAt with bulkWrite (gh-15781) when \`timestamps\` is \`${timestamps}\``, async function() { + // Arrange + const schema = Schema({ name: String }, { timestamps: true }); - const createdAt = new Date('2011-06-01'); - assert.ok(createdAt.valueOf() < start.valueOf()); - await Model.bulkWrite([{ - updateOne: { - filter: { _id: doc._id }, - update: { name: 'gh-15781 update', createdAt }, - overwriteImmutable: true, - timestamps: false - } - }]); - doc = await Model.collection.findOne({ name: 'gh-15781 update' }); - assert.equal(doc.createdAt.valueOf(), createdAt.valueOf()); + const Model = db.model('Test', schema); + + const doc1 = await Model.create({ name: 'gh-15781-1' }); + const doc2 = await Model.create({ name: 'gh-15781-2' }); + + // Act + const createdAt = new Date('2011-06-01'); + + await Model.bulkWrite([ + { + updateOne: { + filter: { _id: doc1._id }, + update: { createdAt }, + overwriteImmutable: true, + timestamps + } + }, + { + updateMany: { + filter: { _id: doc2._id }, + update: { createdAt }, + overwriteImmutable: true, + timestamps + } + } + ]); + + // Assert + const updatesDocs = await Model.find({ _id: { $in: [doc1._id, doc2._id] } }); + + assert.equal(updatesDocs[0].createdAt.valueOf(), createdAt.valueOf()); + assert.equal(updatesDocs[1].createdAt.valueOf(), createdAt.valueOf()); + }); + + it(`can not update immutable fields without overwriteImmutable: true and timestamps: ${timestamps}`, async function() { + // Arrange + const { User } = createTestContext(); + const users = await User.create([ + { name: 'Bob', ssn: '222-22-2222' }, + { name: 'Eve', ssn: '333-33-3333' } + ]); + const newCreatedAt = new Date('2020-01-01'); + + // Act + await User.bulkWrite([ + { + updateOne: { + filter: { _id: users[0]._id }, + update: { ssn: '888-88-8888', createdAt: newCreatedAt } + }, + timestamps + }, + { + updateMany: { + filter: { _id: users[1]._id }, + update: { ssn: '777-77-7777', createdAt: newCreatedAt } + }, + timestamps + } + ]); + + + // Assert + const [updatedUser1, updatedUser2] = await Promise.all([ + User.findById(users[0]._id), + User.findById(users[1]._id) + ]); + assert.strictEqual(updatedUser1.ssn, '222-22-2222'); + assert.notStrictEqual(updatedUser1.createdAt.valueOf(), newCreatedAt.valueOf()); + + assert.strictEqual(updatedUser2.ssn, '333-33-3333'); + assert.notStrictEqual(updatedUser2.createdAt.valueOf(), newCreatedAt.valueOf()); + }); + } + + function createTestContext() { + const userSchema = new Schema({ + name: String, + ssn: { type: String, immutable: true } + }, { timestamps: true }); + const User = db.model('User', userSchema); + return { User }; + } + }); + + describe('bulkWrite overwriteImmutable option (gh-15781)', function() { + it('updateOne can update immutable field with overwriteImmutable: true', async function() { + // Arrange + const { User } = createTestContext(); + const user = await User.create({ name: 'John', ssn: '123-45-6789' }); + const customCreatedAt = new Date('2020-01-01'); + + // Act + await User.bulkWrite([{ + updateOne: { + filter: { _id: user._id }, + update: { createdAt: customCreatedAt, ssn: '999-99-9999' }, + overwriteImmutable: true + } + }]); + + // Assert + const updatedUser = await User.findById(user._id); + assert.strictEqual(updatedUser.ssn, '999-99-9999'); + assert.strictEqual(updatedUser.createdAt.valueOf(), customCreatedAt.valueOf()); + }); + + it('updateMany can update immutable field with overwriteImmutable: true', async function() { + // Arrange + const { User } = createTestContext(); + const user = await User.create({ name: 'Alice', ssn: '111-11-1111' }); + const customCreatedAt = new Date('2020-01-01'); + + // Act + await User.bulkWrite([{ + updateMany: { + filter: { _id: user._id }, + update: { createdAt: customCreatedAt, ssn: '000-00-0000' }, + overwriteImmutable: true + } + }]); + + // Assert + const updatedUser = await User.findById(user._id); + assert.strictEqual(updatedUser.ssn, '000-00-0000'); + assert.strictEqual(updatedUser.createdAt.valueOf(), customCreatedAt.valueOf()); + }); + + for (const timestamps of [true, false, null, undefined]) { + it(`overwriting immutable createdAt with bulkWrite (gh-15781) when \`timestamps\` is \`${timestamps}\``, async function() { + // Arrange + const schema = Schema({ name: String }, { timestamps: true }); + + const Model = db.model('Test', schema); + + const doc1 = await Model.create({ name: 'gh-15781-1' }); + const doc2 = await Model.create({ name: 'gh-15781-2' }); + + // Act + const createdAt = new Date('2011-06-01'); + + await Model.bulkWrite([ + { + updateOne: { + filter: { _id: doc1._id }, + update: { createdAt }, + overwriteImmutable: true, + timestamps + } + }, + { + updateMany: { + filter: { _id: doc2._id }, + update: { createdAt }, + overwriteImmutable: true, + timestamps + } + } + ]); + + // Assert + const updatesDocs = await Model.find({ _id: { $in: [doc1._id, doc2._id] } }); + + assert.equal(updatesDocs[0].createdAt.valueOf(), createdAt.valueOf()); + assert.equal(updatesDocs[1].createdAt.valueOf(), createdAt.valueOf()); + }); + + it(`can not update immutable fields without overwriteImmutable: true and timestamps: ${timestamps}`, async function() { + // Arrange + const { User } = createTestContext(); + const users = await User.create([ + { name: 'Bob', ssn: '222-22-2222' }, + { name: 'Eve', ssn: '333-33-3333' } + ]); + const newCreatedAt = new Date('2020-01-01'); + + // Act + await User.bulkWrite([ + { + updateOne: { + filter: { _id: users[0]._id }, + update: { ssn: '888-88-8888', createdAt: newCreatedAt } + }, + timestamps + }, + { + updateMany: { + filter: { _id: users[1]._id }, + update: { ssn: '777-77-7777', createdAt: newCreatedAt } + }, + timestamps + } + ]); + + + // Assert + const [updatedUser1, updatedUser2] = await Promise.all([ + User.findById(users[0]._id), + User.findById(users[1]._id) + ]); + assert.strictEqual(updatedUser1.ssn, '222-22-2222'); + assert.notStrictEqual(updatedUser1.createdAt.valueOf(), newCreatedAt.valueOf()); + + assert.strictEqual(updatedUser2.ssn, '333-33-3333'); + assert.notStrictEqual(updatedUser2.createdAt.valueOf(), newCreatedAt.valueOf()); + }); + } + + function createTestContext() { + const userSchema = new Schema({ + name: String, + ssn: { type: String, immutable: true } + }, { timestamps: true }); + const User = db.model('User', userSchema); + return { User }; + } }); it('updates buffers with `runValidators` successfully (gh-8580)', async function() { diff --git a/test/query.cursor.test.js b/test/query.cursor.test.js index cb8bc53f8ee..08cc407d5a1 100644 --- a/test/query.cursor.test.js +++ b/test/query.cursor.test.js @@ -539,7 +539,7 @@ describe('QueryCursor', function() { setTimeout(() => { assert.equal(closeEventTriggeredCount, 1); done(); - }, 20); + }, 200); }); it('closing query cursor emits `close` event only once with stream pause/resume (gh-10876)', function(done) { diff --git a/test/schema.validation.test.js b/test/schema.validation.test.js index d246630c4fd..b0e629dfca0 100644 --- a/test/schema.validation.test.js +++ b/test/schema.validation.test.js @@ -97,6 +97,56 @@ describe('schema', function() { await Test.path('state').doValidate('open'); }); + it('number enum', async function() { + const Test = new Schema({ + status: { type: Number, enum: [1, 2, 3, null] }, + priority: { type: Number } + }); + + assert.ok(Test.path('status') instanceof SchemaTypes.Number); + assert.deepEqual(Test.path('status').enumValues, [1, 2, 3, null]); + assert.equal(Test.path('status').validators.length, 1); + + Test.path('status').enum(4, 5); + + assert.deepEqual(Test.path('status').enumValues, [1, 2, 3, null, 4, 5]); + + // with SchemaTypes validate method + Test.path('priority').enum({ + values: [10, 20, 30], + message: 'enum validator failed for path `{PATH}`: test' + }); + + assert.equal(Test.path('priority').validators.length, 1); + assert.deepEqual(Test.path('priority').enumValues, [10, 20, 30]); + + await assert.rejects(Test.path('status').doValidate(6), ValidatorError); + + // allow unsetting enums + await Test.path('status').doValidate(undefined); + + await Test.path('status').doValidate(null); + + await assert.rejects( + Test.path('status').doValidate(99), + ValidatorError + ); + + await assert.rejects( + Test.path('priority').doValidate(40), + err => { + assert.ok(err instanceof ValidatorError); + assert.equal(err.message, + 'enum validator failed for path `priority`: test'); + return true; + } + ); + + await Test.path('status').doValidate(1); + + await Test.path('status').doValidate(2); + }); + it('string regexp', async function() { const Test = new Schema({ simple: { type: String, match: /[a-z]/ } diff --git a/test/types/create.test.ts b/test/types/create.test.ts index 95cab3dd7f9..908467fde4e 100644 --- a/test/types/create.test.ts +++ b/test/types/create.test.ts @@ -1,4 +1,4 @@ -import { Schema, model, Types, HydratedDocument } from 'mongoose'; +import mongoose, { Schema, model, Types, HydratedDocument } from 'mongoose'; import { expectError, expectType } from 'tsd'; const schema = new Schema({ name: { type: 'String' } }); @@ -209,3 +209,16 @@ async function createWithRawDocTypeNo_id() { } createWithAggregateErrors(); + +async function gh15902() { + class ProviderMongoDbMongooseImpl> { + public constructor( + private readonly model: mongoose.Model + ) {} + + public async createOne(resource: K): Promise { + const doc = await this.model.create(resource); + return doc.toObject() as K; + } + } +} diff --git a/test/types/inferrawdoctype.test.ts b/test/types/inferrawdoctype.test.ts index c67ae090fae..a4fb580e634 100644 --- a/test/types/inferrawdoctype.test.ts +++ b/test/types/inferrawdoctype.test.ts @@ -1,6 +1,28 @@ -import { InferRawDocType, type InferSchemaType, type ResolveTimestamps, type Schema, type Types } from 'mongoose'; -import { expectType, expectError } from 'tsd'; +import { InferRawDocType, type InferRawDocTypeWithout_id, type ResolveTimestamps, type Schema, type Types } from 'mongoose'; +import { expectType } from 'tsd'; +function inferPojoType() { + const schemaDefinition = { + email: { + type: String, + trim: true, + required: true, + unique: true, + lowercase: true + }, + password: { + type: String, + required: true + }, + dateOfBirth: { + type: Date, + required: true + } + }; + + type UserType = InferRawDocTypeWithout_id; + expectType<{ email: string, password: string, dateOfBirth: Date }>({} as UserType); +} function gh14839() { const schemaDefinition = { email: { diff --git a/test/types/models.test.ts b/test/types/models.test.ts index 8142f71b848..f5ecd80867a 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -14,14 +14,14 @@ import mongoose, { UpdateQuery, UpdateWriteOpResult, WithLevel1NestedPaths, - createConnection, connection, model, - ObtainSchemaGeneric + UpdateOneModel, + UpdateManyModel } from 'mongoose'; import { expectAssignable, expectError, expectType } from 'tsd'; import { AutoTypedSchemaType, autoTypedSchema } from './schema.test'; -import { ModifyResult, UpdateOneModel, ChangeStreamInsertDocument, ObjectId } from 'mongodb'; +import { ModifyResult, UpdateOneModel as MongoUpdateOneModel, ChangeStreamInsertDocument, ObjectId } from 'mongodb'; function rawDocSyntax(): void { interface ITest { @@ -413,7 +413,7 @@ function gh11911() { const Animal = model('Animal', animalSchema); const changes: UpdateQuery = {}; - expectAssignable({ + expectAssignable({ filter: {}, update: changes }); @@ -507,7 +507,7 @@ function gh12100() { })(); -function modelRemoveOptions() { +async function modelRemoveOptions() { const cmodel = model('Test', new Schema()); const res: DeleteResult = await cmodel.deleteOne({}, {}); @@ -1068,11 +1068,18 @@ async function gh15693() { } interface UserMethods { + printNamePrefixed(this: IUser, prefix: string): void; printName(this: IUser): void; getName(): string; } const schema = new Schema, UserMethods>({ name: { type: String, required: true } }); + schema.method('printNamePrefixed', function printName(this: IUser, prefix: string) { + expectError(this.isModified('name')); + expectError(this.doesNotExist()); + expectType(this.name); + console.log(prefix + this.name); + }); schema.method('printName', function printName(this: IUser) { expectError(this.isModified('name')); expectError(this.doesNotExist()); @@ -1087,5 +1094,38 @@ async function gh15693() { const leanInst = await User.findOne({}).lean().orFail(); User.schema.methods.printName.apply(leanInst); + User.schema.methods.printNamePrefixed.call(leanInst, ''); +} + +async function gh15781() { + const userSchema = new Schema({ + createdAt: { type: Date, immutable: true }, + name: String + }, { timestamps: true }); + + const User = model('User', userSchema); + + await User.bulkWrite([ + { + updateOne: { + filter: { name: 'John' }, + update: { createdAt: new Date() }, + overwriteImmutable: true, + timestamps: false + } + }, + { + updateMany: { + filter: { name: 'Jane' }, + update: { createdAt: new Date() }, + overwriteImmutable: true, + timestamps: false + } + } + ]); + expectType({} as UpdateOneModel['timestamps']); + expectType({} as UpdateOneModel['overwriteImmutable']); + expectType({} as UpdateManyModel['timestamps']); + expectType({} as UpdateManyModel['overwriteImmutable']); } diff --git a/test/types/queries.test.ts b/test/types/queries.test.ts index 23ae0424cb3..ad792580c41 100644 --- a/test/types/queries.test.ts +++ b/test/types/queries.test.ts @@ -863,5 +863,19 @@ async function gh15786() { } const schema = new Schema, {}, {}, {}, DocStatics>({}); - schema.static({ m1() {} } as DocStatics); + schema.static({ m1() {} }); +} + +async function gh15779_2() { + interface Job { + _id: Types.ObjectId; + name: string; + } + + const jobSchema = new Schema({ name: String }); + const JobModel = model('Job', jobSchema); + + const jobs = await JobModel.aggregate([ + { $match: {} as QueryFilter } + ]); } diff --git a/test/types/schema.create.test.ts b/test/types/schema.create.test.ts index 64920468885..72f98477e55 100644 --- a/test/types/schema.create.test.ts +++ b/test/types/schema.create.test.ts @@ -1645,7 +1645,7 @@ function gh13215() { >; type User = { userName: string; - date: Date; + date: NativeDate; } & { _id: Types.ObjectId }; expectType({} as RawDocType); @@ -1654,11 +1654,18 @@ function gh13215() { type SchemaType = InferSchemaType; expectType<{ userName: string; - date: Date; + date: NativeDate; _id: Types.ObjectId; }>({} as SchemaType); type HydratedDoc = ObtainSchemaGeneric; - expectType>({} as HydratedDoc); + expectType + >>({} as HydratedDoc); } function gh14825() { diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index 75e90f50268..966c7ddc0cc 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -2086,3 +2086,48 @@ function gh15751() { const doc = new TestModel(); expectType(doc.myId); } + +function testNewSchemaWithMethodsAndVirtuals() { + const schema = new Schema( + { name: String }, + { + virtuals: { + upperName: { + get(): string | undefined { + return this.name?.toUpperCase(); + } + } + }, + methods: { + greet(greeting: string) { + return `${greeting}, ${this.name}`; + } + } + } + ); + + const TestModel = model('Test', schema); + const doc = new TestModel({ name: 'test' }); + + const greeting = doc.greet('Hello'); + expectType(greeting); + + expectType(doc.upperName); +} + +function gh15878() { + const schema = new Schema({ + name: { + type: String, + default: null + }, + age: { + type: Number, + default: () => null + } + }); + const TestModel = model('Test', schema); + const doc = new TestModel({ name: 'John', age: 30 }); + expectType(doc.name); + expectType(doc.age); +} diff --git a/test/versioning.test.js b/test/versioning.test.js index cbae5b1e928..0582b074ee8 100644 --- a/test/versioning.test.js +++ b/test/versioning.test.js @@ -660,4 +660,76 @@ describe('versioning', function() { const fromDb = await Model.findById(doc); assert.strictEqual(fromDb.meta.versionKey, 0); }); + + describe('version mode accumulation (gh-15888)', function() { + const VERSION_ALL = mongoose.Document.VERSION_ALL; + + it('combines VERSION_INC and VERSION_WHERE when pull and index set happen together', async function() { + // Arrange + const { User, user } = await createTestContext(); + + // Act + user.items.pull(user.items[0]._id); // VERSION_INC ($pull is an array atomic op) + user.tags[0] = 'modified'; // VERSION_WHERE (modifying array element by index) + user.$__delta(); + const versionMode = user.$__.version; + await user.save(); + + // Assert + const afterSave = await User.findById(user._id); + assert.strictEqual(versionMode, VERSION_ALL, 'version mode should be VERSION_ALL (3)'); + assert.strictEqual(user.__v, 1, '__v should be incremented in memory after pull'); + assert.strictEqual(afterSave.__v, 1, '__v should be incremented in db after pull'); + }); + + it('rejects stale update after pull when another client modifies by index', async function() { + // Arrange + const { User, user } = await createTestContext(); + const clientA = await User.findById(user._id); + const clientB = await User.findById(user._id); + + // Act - Client A: pull + set index + clientA.items.pull(clientA.items[0]._id); // VERSION_INC + clientA.tags[0] = 'modified'; // VERSION_WHERE + await clientA.save(); + + // Client B: stale view, updates by index + clientB.items[1].name = 'updated'; // VERSION_WHERE + const err = await clientB.save().then(() => null, err => err); + + // Assert + assert.ok(err, 'save should throw VersionError due to stale __v'); + assert.strictEqual(err.name, 'VersionError'); + }); + + it('combines VERSION_WHERE and VERSION_INC regardless of operation order', async function() { + // Arrange + const { User, user } = await createTestContext(); + + // Act - set index first (VERSION_WHERE) then push (VERSION_INC) + user.tags[0] = 'modified'; // VERSION_WHERE + user.items.push({ name: 'item4' }); // VERSION_INC + user.$__delta(); + const versionMode = user.$__.version; + await user.save(); + + // Assert + const afterSave = await User.findById(user._id); + assert.strictEqual(versionMode, VERSION_ALL, 'version mode should be VERSION_ALL (3)'); + assert.strictEqual(afterSave.__v, 1, '__v should be incremented after push'); + }); + + async function createTestContext() { + const schema = new Schema({ + items: [{ name: String }], + tags: [String] + }); + const User = db.model('Test', schema); + const user = await User.create({ + items: [{ name: 'item1' }, { name: 'item2' }, { name: 'item3' }], + tags: ['tag1', 'tag2'] + }); + return { User, user }; + } + }); }); diff --git a/types/index.d.ts b/types/index.d.ts index a445427e4c7..a33e5a568ce 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -345,7 +345,8 @@ declare module 'mongoose' { TSchemaOptions extends { methods: infer M } ? M : {}, TSchemaOptions extends { query: any } ? TSchemaOptions['query'] : {}, TSchemaOptions extends { virtuals: any } ? TSchemaOptions['virtuals'] : {}, - RawDocType + RawDocType, + ResolveSchemaOptions > >(def: TSchemaDefinition): Schema< RawDocType, @@ -374,7 +375,14 @@ declare module 'mongoose' { InferRawDocType>, ResolveSchemaOptions >, - THydratedDocumentType extends AnyObject = HydratedDocument>> + THydratedDocumentType extends AnyObject = HydratedDocument< + InferHydratedDocType>, + TSchemaOptions extends { methods: infer M } ? M : {}, + TSchemaOptions extends { query: any } ? TSchemaOptions['query'] : {}, + TSchemaOptions extends { virtuals: any } ? TSchemaOptions['virtuals'] : {}, + RawDocType, + ResolveSchemaOptions + > >(def: TSchemaDefinition, options: TSchemaOptions): Schema< RawDocType, Model, @@ -623,8 +631,7 @@ declare module 'mongoose' { /** Adds static "class" methods to Models compiled from this schema. */ static(name: K, fn: TStaticMethods[K]): this; - static(obj: { [F in keyof TStaticMethods]: TStaticMethods[F] }): this; - static(obj: { [F in keyof TStaticMethods]: TStaticMethods[F] } & { [name: string]: (this: TModelType, ...args: any[]) => any }): this; + static(obj: Partial & { [name: string]: (this: TModelType, ...args: any[]) => any }): this; static(name: string, fn: (this: TModelType, ...args: any[]) => any): this; /** Object of currently defined statics on this schema. */ diff --git a/types/inferrawdoctype.d.ts b/types/inferrawdoctype.d.ts index 9e54ef3ea0e..9b44ee6bb8f 100644 --- a/types/inferrawdoctype.d.ts +++ b/types/inferrawdoctype.d.ts @@ -12,18 +12,24 @@ declare module 'mongoose' { ? ObtainSchemaGeneric : FlattenMaps>>; - export type InferRawDocType< + export type InferRawDocTypeWithout_id< SchemaDefinition, TSchemaOptions extends Record = DefaultSchemaOptions, TTransformOptions = { bufferToBinary: false } - > = Require_id = ApplySchemaOptions<{ [ K in keyof (RequiredPaths & OptionalPaths) ]: IsPathRequired extends true ? ObtainRawDocumentPathType : ObtainRawDocumentPathType | null; - }, TSchemaOptions>>; + }, TSchemaOptions>; + + export type InferRawDocType< + SchemaDefinition, + TSchemaOptions extends Record = DefaultSchemaOptions, + TTransformOptions = { bufferToBinary: false } + > = Require_id>; /** * @summary Allows users to optionally choose their own type for a schema field for stronger typing. diff --git a/types/inferschematype.d.ts b/types/inferschematype.d.ts index ed758828054..3af68827fb3 100644 --- a/types/inferschematype.d.ts +++ b/types/inferschematype.d.ts @@ -100,10 +100,10 @@ declare module 'mongoose' { { EnforcedDocType: EnforcedDocType; M: M; - TInstanceMethods: TInstanceMethods; - TQueryHelpers: TQueryHelpers; - TVirtuals: AddDefaultId; - TStaticMethods: TStaticMethods; + TInstanceMethods: IfEquals; + TQueryHelpers: IfEquals; + TVirtuals: AddDefaultId, TSchemaOptions>; + TStaticMethods: IfEquals; TSchemaOptions: TSchemaOptions; DocType: DocType; THydratedDocumentType: THydratedDocumentType; @@ -136,11 +136,6 @@ declare module 'mongoose' { : T; } -type IsPathDefaultUndefined = - PathType extends { default: undefined } ? true - : PathType extends { default: (...args: any[]) => undefined } ? true - : false; - type RequiredPropertyDefinition = | { required: true | string | [true, string | undefined] | { isRequired: true }; @@ -159,16 +154,17 @@ type IsPathRequired = P extends { required: false } ? false : true - : P extends Record ? - IsPathDefaultUndefined

extends true ? - false - : true - : P extends Record ? - P extends { default: any } ? - IfEquals - : false + : P extends { default: undefined | null | ((...args: any[]) => undefined) | ((...args: any[]) => null) } ? false + : P extends { default: any } ? true + : P extends Record ? true : false; +// Internal type used to efficiently check for never or any types +// can be efficiently checked like: +// `[T] extends [neverOrAny] ? T : ...` +// to avoid edge cases +type neverOrAny = ' ~neverOrAny~'; + /** * @summary A Utility to obtain schema's required path keys. * @param {T} T A generic refers to document definition. @@ -251,6 +247,7 @@ type UnionToType = T[number] extends infer U type IsSchemaTypeFromBuiltinClass = T extends typeof String ? true + : unknown extends Buffer ? false : T extends typeof Number ? true : T extends typeof Boolean ? true : T extends typeof Buffer ? true @@ -268,7 +265,6 @@ type IsSchemaTypeFromBuiltinClass = : T extends NativeDate ? true : T extends typeof Schema.Types.Mixed ? true : T extends Types.UUID ? true - : unknown extends Buffer ? false : T extends Buffer ? true : false; @@ -284,12 +280,10 @@ type ResolvePathType< Options extends SchemaTypeOptions = {}, TypeKey extends string = DefaultSchemaOptions['typeKey'], TypeHint = never -> = IfEquals< - TypeHint, - never, - PathValueType extends Schema ? InferSchemaType +> = [TypeHint] extends [never] + ? PathValueType extends Schema ? InferSchemaType : PathValueType extends AnyArray ? - IfEquals extends true + [Item] extends [never] ? any[] : Item extends Schema ? // If Item is a schema, infer its type. @@ -328,7 +322,7 @@ type ResolvePathType< : never : PathValueType extends ArrayConstructor ? any[] : PathValueType extends typeof Schema.Types.Mixed ? any - : IfEquals extends true ? any + : PathValueType extends ObjectConstructor ? any : IfEquals extends true ? any : PathValueType extends typeof SchemaType ? PathValueType['prototype'] : PathValueType extends Record ? @@ -339,6 +333,5 @@ type ResolvePathType< typeKey: TypeKey; } > - : unknown, - TypeHint ->; + : unknown + : TypeHint; diff --git a/types/models.d.ts b/types/models.d.ts index 627666c24e8..876620a839a 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -172,94 +172,47 @@ declare module 'mongoose' { interface RemoveOptions extends SessionOption, Omit {} - const Model: Model; - - export type AnyBulkWriteOperation = { - insertOne: InsertOneModel; - } | { - replaceOne: ReplaceOneModel; - } | { - updateOne: UpdateOneModel; - } | { - updateMany: UpdateManyModel; - } | { - deleteOne: DeleteOneModel; - } | { - deleteMany: DeleteManyModel; - }; - - export interface InsertOneModel { - document: mongodb.OptionalId; + interface MongooseBulkWritePerOperationOptions { + /** Skip validation for this operation. */ + skipValidation?: boolean; /** When false, do not add timestamps. When true, overrides the `timestamps` option set in the `bulkWrite` options. */ timestamps?: boolean; } - export interface ReplaceOneModel { - /** The filter to limit the replaced document. */ - filter: QueryFilter; - /** The document with which to replace the matched document. */ - replacement: mongodb.WithoutId; - /** Specifies a collation. */ - collation?: mongodb.CollationOptions; - /** The index to use. If specified, then the query system will only consider plans using the hinted index. */ - hint?: mongodb.Hint; - /** When true, creates a new document if no document matches the query. */ - upsert?: boolean; - /** When false, do not add timestamps. When true, overrides the `timestamps` option set in the `bulkWrite` options. */ - timestamps?: boolean; + interface MongooseBulkUpdatePerOperationOptions extends MongooseBulkWritePerOperationOptions { + /** When true, allows updating fields that are marked as `immutable` in the schema. */ + overwriteImmutable?: boolean; + /** When false, do not set default values on insert. */ + setDefaultsOnInsert?: boolean; } - export interface UpdateOneModel { - /** The filter to limit the updated documents. */ - filter: QueryFilter; - /** A document or pipeline containing update operators. */ - update: UpdateQuery; - /** A set of filters specifying to which array elements an update should apply. */ - arrayFilters?: AnyObject[]; - /** Specifies a collation. */ - collation?: mongodb.CollationOptions; - /** The index to use. If specified, then the query system will only consider plans using the hinted index. */ - hint?: mongodb.Hint; - /** When true, creates a new document if no document matches the query. */ - upsert?: boolean; - /** When false, do not add timestamps. When true, overrides the `timestamps` option set in the `bulkWrite` options. */ - timestamps?: boolean; - } + export type InsertOneModel = + mongodb.InsertOneModel & MongooseBulkWritePerOperationOptions; - export interface UpdateManyModel { - /** The filter to limit the updated documents. */ - filter: QueryFilter; - /** A document or pipeline containing update operators. */ - update: UpdateQuery; - /** A set of filters specifying to which array elements an update should apply. */ - arrayFilters?: AnyObject[]; - /** Specifies a collation. */ - collation?: mongodb.CollationOptions; - /** The index to use. If specified, then the query system will only consider plans using the hinted index. */ - hint?: mongodb.Hint; - /** When true, creates a new document if no document matches the query. */ - upsert?: boolean; - /** When false, do not add timestamps. When true, overrides the `timestamps` option set in the `bulkWrite` options. */ - timestamps?: boolean; - } + export type ReplaceOneModel = + mongodb.ReplaceOneModel & MongooseBulkWritePerOperationOptions; - export interface DeleteOneModel { - /** The filter to limit the deleted documents. */ - filter: QueryFilter; - /** Specifies a collation. */ - collation?: mongodb.CollationOptions; - /** The index to use. If specified, then the query system will only consider plans using the hinted index. */ - hint?: mongodb.Hint; - } + export type UpdateOneModel = + mongodb.UpdateOneModel & MongooseBulkUpdatePerOperationOptions; - export interface DeleteManyModel { - /** The filter to limit the deleted documents. */ - filter: QueryFilter; - /** Specifies a collation. */ - collation?: mongodb.CollationOptions; - /** The index to use. If specified, then the query system will only consider plans using the hinted index. */ - hint?: mongodb.Hint; - } + export type UpdateManyModel = + mongodb.UpdateManyModel & MongooseBulkUpdatePerOperationOptions; + + export type DeleteOneModel = + mongodb.DeleteOneModel; + + export type DeleteManyModel = + mongodb.DeleteManyModel; + + export type AnyBulkWriteOperation = + | { insertOne: InsertOneModel } + | { replaceOne: ReplaceOneModel } + | { updateOne: UpdateOneModel } + | { updateMany: UpdateManyModel } + | { deleteOne: DeleteOneModel } + | { deleteMany: DeleteManyModel }; + + const Model: Model; /* * Apply common casting logic to the given type, allowing: @@ -338,13 +291,13 @@ declare module 'mongoose' { * round trip to the MongoDB server. */ bulkWrite( - writes: Array>, - options: MongooseBulkWriteOptions & { ordered: false } - ): Promise; + writes: Array>, + options: mongodb.BulkWriteOptions & MongooseBulkWriteOptions & { ordered: false } + ): Promise; bulkWrite( - writes: Array>, - options?: MongooseBulkWriteOptions - ): Promise; + writes: Array>, + options?: mongodb.BulkWriteOptions & MongooseBulkWriteOptions + ): Promise; /** * Sends multiple `save()` calls in a single `bulkWrite()`. This is faster than @@ -382,6 +335,8 @@ declare module 'mongoose' { /** Creates a new document or documents */ create(): Promise; + create(doc: Partial): Promise; + create(docs: Array>): Promise; create(docs: Array>>>, options: CreateOptions & { aggregateErrors: true }): Promise<(THydratedDocumentType | Error)[]>; create(docs: Array>>>, options?: CreateOptions): Promise; create(doc: DeepPartial>>): Promise; diff --git a/types/query.d.ts b/types/query.d.ts index f0e3e860224..9fa158e306d 100644 --- a/types/query.d.ts +++ b/types/query.d.ts @@ -20,7 +20,7 @@ declare module 'mongoose' { export type ApplyBasicQueryCasting = QueryTypeCasting | QueryTypeCasting | (T extends (infer U)[] ? QueryTypeCasting : T) | null; - type _QueryFilter = ({ [P in keyof T]?: mongodb.Condition>; } & mongodb.RootFilterOperators<{ [P in keyof T]?: ApplyBasicQueryCasting; }>); + type _QueryFilter = ({ [P in keyof T]?: mongodb.Condition>; } & mongodb.RootFilterOperators<{ [P in keyof mongodb.WithId]?: ApplyBasicQueryCasting[P]>; }>); type QueryFilter = IsItRecordAndNotAny extends true ? _QueryFilter> : _QueryFilter>; type MongooseBaseQueryOptionKeys = diff --git a/types/utility.d.ts b/types/utility.d.ts index c5d5c2065ec..f295e692ef2 100644 --- a/types/utility.d.ts +++ b/types/utility.d.ts @@ -146,7 +146,7 @@ declare module 'mongoose' { * @param {T} T Function type to extract 'this' parameter from. * @param {F} F Fallback type to return if 'this' parameter does not exist. */ - type ThisParameter = T extends { (this: infer This): void } ? This : F; + type ThisParameter = T extends { (this: infer This, ...args: never): void } ? This : F; /** * @summary Decorates all functions in an object with 'this' parameter. diff --git a/types/virtuals.d.ts b/types/virtuals.d.ts index 2ec48a496f1..71d02297f74 100644 --- a/types/virtuals.d.ts +++ b/types/virtuals.d.ts @@ -8,7 +8,7 @@ declare module 'mongoose' { type TVirtualPathFN = >(this: Document & DocType, value: PathType, virtual: VirtualType, doc: Document & DocType) => TReturn; - type SchemaOptionsVirtualsPropertyType, TInstanceMethods = {}> = { - [K in keyof VirtualPaths]: VirtualPathFunctions extends true ? DocType : any, VirtualPaths[K], TInstanceMethods> - }; + type SchemaOptionsVirtualsPropertyType, TInstanceMethods = {}> = { + [K in keyof VirtualPaths]: VirtualPathFunctions extends true ? DocType : any, VirtualPaths[K], TInstanceMethods> + }; }