From 2c63266f54a7c0cdd4b122b3caf3f6ef6c3be53b Mon Sep 17 00:00:00 2001 From: White-Devil2839 Date: Tue, 18 Nov 2025 10:59:48 +0530 Subject: [PATCH 001/133] feat: Add dark mode support and CSS improvements - Add CSS custom properties for theming - Implement dark mode with prefers-color-scheme and manual toggle - Add theme toggle button in top-left corner with localStorage persistence - Improve accessibility with focus states and contrast ratios - Enhance responsive design with better breakpoints - Improve code block styling with dark mode syntax highlighting - Add print stylesheet optimizations - Improve mobile navigation styling - Enhance API navigation sidebar responsiveness --- docs/api_split.pug | 2 +- docs/css/api.css | 103 ++++++++- docs/css/github.css | 210 +++++++++++++++++-- docs/css/mongoose5.css | 448 +++++++++++++++++++++++++++++++++++++--- docs/css/style.css | 149 ++++++++++++- docs/js/theme-toggle.js | 72 +++++++ docs/layout.pug | 9 +- index.pug | 10 +- 8 files changed, 943 insertions(+), 60 deletions(-) create mode 100644 docs/js/theme-toggle.js diff --git a/docs/api_split.pug b/docs/api_split.pug index 2078d1b28ee..78ff1b1a140 100644 --- a/docs/api_split.pug +++ b/docs/api_split.pug @@ -1,7 +1,7 @@ extends layout append style - link(rel="stylesheet", href=`${versions.versionedPath}/docs/css/api.css`) + link(rel="stylesheet", href=`${versions.versionedPath}/docs/css/api.css?v=${Date.now()}`) script(src=`${versions.versionedPath}/docs/js/api-bold-current-nav.js`) script(src=`${versions.versionedPath}/docs/js/convert-old-anchorid.js`) diff --git a/docs/css/api.css b/docs/css/api.css index a2f30f6c76e..3d9b50fe0df 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,37 @@ 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) { + .api-nav { + background-color: var(--menu-bg, #252525); + border-left-color: var(--border-color, #444); + } + + .api-nav a { + color: var(--text-muted, #888); + } + + .api-nav a:hover { + color: var(--link-color, #4a9eff); + } + + .native-ad { + background-color: var(--bg-secondary, #2d2d2d); + } + + .native-ad a { + color: var(--text-primary, #e0e0e0); + } } diff --git a/docs/css/github.css b/docs/css/github.css index f8cec1bdc3f..f4e134a0dd7 100644 --- a/docs/css/github.css +++ b/docs/css/github.css @@ -1,31 +1,45 @@ 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); } 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; +} + +/* Improve code block copy functionality */ +pre { + position: relative; +} + +pre:hover { + box-shadow: 0 2px 6px var(--shadow, rgba(0, 0, 0, 0.15)); } /* @@ -38,9 +52,10 @@ 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; } .hljs-comment, @@ -152,3 +167,166 @@ github.com style (c) Vasily Polovnyov .hljs-chunk { color: #aaa; } + +/* Dark mode support for syntax highlighting */ +@media (prefers-color-scheme: dark) { + code { + background-color: var(--code-bg, #2d2d2d); + color: var(--link-color, #4a9eff); + } + + pre { + background-color: var(--code-bg, #2d2d2d); + border-color: var(--border-color, #444); + color: var(--code-text, #e0e0e0); + } + + .hljs { + background: var(--code-bg, #2d2d2d); + color: var(--code-text, #e0e0e0); + } + + .hljs-comment, + .diff .hljs-header, + .hljs-javadoc { + color: #6a9955; + } + + .hljs-keyword, + .css .rule .hljs-keyword, + .hljs-winutils, + .nginx .hljs-title, + .hljs-subst, + .hljs-request, + .hljs-status { + color: #569cd6; + font-weight: bold; + } + + .hljs-number, + .hljs-hexcolor, + .ruby .hljs-constant { + color: #b5cea8; + } + + .hljs-string, + .hljs-tag .hljs-value, + .hljs-phpdoc, + .hljs-dartdoc, + .tex .hljs-formula { + color: #ce9178; + } + + .hljs-title, + .hljs-id, + .scss .hljs-preprocessor { + color: #d7ba7d; + font-weight: bold; + } + + .hljs-class .hljs-title, + .hljs-type, + .vhdl .hljs-literal, + .tex .hljs-command { + color: #4ec9b0; + font-weight: bold; + } + + .hljs-tag, + .hljs-tag .hljs-title, + .hljs-rules .hljs-property, + .django .hljs-tag .hljs-keyword { + color: #569cd6; + font-weight: normal; + } + + .hljs-attribute, + .hljs-variable, + .lisp .hljs-body { + color: #9cdcfe; + } + + .hljs-regexp { + color: #d16969; + } + + .hljs-symbol, + .ruby .hljs-symbol .hljs-string, + .lisp .hljs-keyword, + .clojure .hljs-keyword, + .scheme .hljs-keyword, + .tex .hljs-special, + .hljs-prompt { + color: #c586c0; + } + + .hljs-built_in { + color: #4fc1ff; + } + + .hljs-preprocessor, + .hljs-pragma, + .hljs-pi, + .hljs-doctype, + .hljs-shebang, + .hljs-cdata { + color: #808080; + font-weight: bold; + } + + .hljs-deletion { + background: #5a1d1d; + color: #f48771; + } + + .hljs-addition { + background: #1e3a1e; + color: #b5cea8; + } + + .diff .hljs-change { + background: #2d4d2d; + color: #4ec9b0; + } + + .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..141c6a7c89c 100644 --- a/docs/css/mongoose5.css +++ b/docs/css/mongoose5.css @@ -1,9 +1,97 @@ +/* CSS Custom Properties for Theming */ +:root { + /* Light mode colors */ + --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; +} + +/* Dark mode colors */ +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) { + --bg-primary: #1a1a1a; + --bg-secondary: #2d2d2d; + --bg-tertiary: #252525; + --text-primary: #e0e0e0; + --text-secondary: #c0c0c0; + --text-muted: #888888; + --link-color: #4a9eff; + --link-hover: #6bb3ff; + --border-color: #444444; + --code-bg: #2d2d2d; + --code-text: #e0e0e0; + --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: #4a9eff; + } +} + +/* Manual dark mode override */ +[data-theme="dark"] { + --bg-primary: #1a1a1a; + --bg-secondary: #2d2d2d; + --bg-tertiary: #252525; + --text-primary: #e0e0e0; + --text-secondary: #c0c0c0; + --text-muted: #888888; + --link-color: #4a9eff; + --link-hover: #6bb3ff; + --border-color: #444444; + --code-bg: #2d2d2d; + --code-text: #e0e0e0; + --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: #4a9eff; +} + +/* Light mode override */ +[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; +} + 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 +100,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,7 +151,16 @@ 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 { @@ -62,7 +171,7 @@ h4 a { } .logo-text { - color: #800; + color: var(--link-color); font-size: 20pt; position: relative; top: 0px; @@ -86,8 +195,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 +216,18 @@ 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); } #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 +240,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 +282,26 @@ 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 rgba(9, 113, 178, 0.1); +} + +@media (prefers-color-scheme: dark) { + .search input:focus { + box-shadow: 0 0 0 2px rgba(74, 158, 255, 0.2); + } } #search-input-nav { @@ -155,14 +309,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 +348,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 +383,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 +413,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 +427,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 +450,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 +473,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 { @@ -371,3 +595,163 @@ 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: 10px; + left: 10px; + 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: 5px; + left: 50px; + } +} diff --git a/docs/css/style.css b/docs/css/style.css index 32edaaea473..095d187f27b 100644 --- a/docs/css/style.css +++ b/docs/css/style.css @@ -1,10 +1,123 @@ body { font-family: 'Open Sans', Helvetica, Arial, FreeSans; - color: #333; + color: var(--text-secondary, #333); -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: 100%; padding: 0; margin: 0; + background-color: var(--bg-primary, #ffffff); + transition: background-color 0.3s ease, color 0.3s ease; +} + +/* Dark mode support for homepage */ +@media (prefers-color-scheme: dark) { + body:not([data-theme="light"]) { + background-color: var(--bg-primary, #1a1a1a); + color: var(--text-secondary, #c0c0c0); + } +} + +/* Manual theme overrides */ +body[data-theme="dark"] { + background-color: var(--bg-primary, #1a1a1a); + color: var(--text-secondary, #c0c0c0); +} + +body[data-theme="light"] { + background-color: var(--bg-primary, #ffffff); + color: var(--text-secondary, #333); +} + +/* Theme Toggle Button for Homepage */ +#theme-toggle { + position: fixed; + top: 10px; + left: 10px; + z-index: 1000; + background-color: rgba(255, 255, 255, 0.9); + border: 1px solid #ddd; + border-radius: 50%; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + transition: background-color 0.3s ease, border-color 0.3s ease; +} + +body[data-theme="dark"] #theme-toggle { + background-color: rgba(26, 26, 26, 0.9); + border-color: #444; +} + +#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: #333; + transition: background-color 0.2s ease, transform 0.1s ease; +} + +body[data-theme="dark"] #theme-toggle-btn { + color: #e0e0e0; +} + +#theme-toggle-btn:hover { + background-color: rgba(0, 0, 0, 0.1); +} + +body[data-theme="dark"] #theme-toggle-btn:hover { + background-color: rgba(255, 255, 255, 0.1); +} + +#theme-toggle-btn:active { + transform: scale(0.95); +} + +#theme-toggle-btn:focus-visible { + outline: 3px solid #0971B2; + outline-offset: 2px; +} + +body[data-theme="dark"] #theme-toggle-btn:focus-visible { + outline-color: #4a9eff; +} + +#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 */ @@ -21,14 +134,20 @@ body { } a { - color: #800; + color: var(--link-color, #800); -webkit-transition-property: opacity, -webkit-transform, color, background-color, padding, -webkit-box-shadow; -webkit-transition-duration: 0.15s; -webkit-transition-timing-function: ease-out; + 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, #0971B2); + outline-offset: 2px; + border-radius: 2px; +} #wrap { width: 600px; margin: 0 auto; @@ -43,20 +162,38 @@ h1 { } pre { - background: #eee; - padding: 5px; - border-radius: 3px; + background: var(--code-bg, #eee); + padding: 12px 16px; + border-radius: 6px; overflow-x: auto; + border: 1px solid var(--border-color, #ddd); + transition: background-color 0.3s ease, border-color 0.3s ease; } code { - color: #333; + color: var(--code-text, #333); font-size: 11px; font-family: Consolas, "Liberation Mono", Courier, monospace; + background-color: var(--code-bg, #eee); + padding: 2px 6px; + border-radius: 4px; } pre code { border: 0 none; padding: 1.2em; overflow-x: auto; + background-color: transparent; +} + +/* Dark mode for homepage code blocks */ +@media (prefers-color-scheme: dark) { + pre { + background: var(--code-bg, #2d2d2d); + border-color: var(--border-color, #444); + } + code { + color: var(--code-text, #e0e0e0); + background-color: var(--code-bg, #2d2d2d); + } } #header { text-align: center; diff --git a/docs/js/theme-toggle.js b/docs/js/theme-toggle.js new file mode 100644 index 00000000000..df57c22c08e --- /dev/null +++ b/docs/js/theme-toggle.js @@ -0,0 +1,72 @@ +(function() { + 'use strict'; + + // Get saved theme preference or default to system preference + function getInitialTheme() { + const savedTheme = localStorage.getItem('mongoose-theme'); + if (savedTheme === 'light' || savedTheme === 'dark') { + return savedTheme; + } + // Default to system preference + return null; + } + + // Apply theme to document + function applyTheme(theme) { + if (theme === 'light' || theme === 'dark') { + document.documentElement.setAttribute('data-theme', theme); + localStorage.setItem('mongoose-theme', theme); + } else { + document.documentElement.removeAttribute('data-theme'); + localStorage.removeItem('mongoose-theme'); + } + updateThemeIcon(); + } + + // Update theme icon visibility + function updateThemeIcon() { + // CSS handles the icon visibility, but we can ensure proper state + const theme = document.documentElement.getAttribute('data-theme'); + const lightIcon = document.getElementById('theme-icon-light'); // Sun icon + const darkIcon = document.getElementById('theme-icon-dark'); // Moon icon + + if (lightIcon && darkIcon) { + // In dark mode: show sun icon (to switch to light) + // In light mode: show moon icon (to switch to dark) + // CSS handles the actual visibility via opacity + } + } + + // Toggle between light and dark + function toggleTheme() { + const currentTheme = document.documentElement.getAttribute('data-theme'); + if (currentTheme === 'dark') { + applyTheme('light'); + } else if (currentTheme === 'light') { + applyTheme(null); // Reset to system preference + } else { + // Currently using system preference, switch to dark + applyTheme('dark'); + } + } + + // Initialize theme on page load + function initTheme() { + const initialTheme = getInitialTheme(); + applyTheme(initialTheme); + + // Set up toggle button + const toggleBtn = document.getElementById('theme-toggle-btn'); + if (toggleBtn) { + toggleBtn.addEventListener('click', toggleTheme); + } + } + + // Run when DOM is ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initTheme); + } else { + initTheme(); + } +})(); + diff --git a/docs/layout.pug b/docs/layout.pug index c8e5ac01d4d..10e7c29d981 100644 --- a/docs/layout.pug +++ b/docs/layout.pug @@ -10,8 +10,8 @@ html(lang='en') 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`) meta(name='msapplication-TileColor', content='#ffffff') @@ -21,6 +21,10 @@ 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') @@ -172,3 +176,4 @@ 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`) diff --git a/index.pug b/index.pug index 14cba69a0ff..f219d428acd 100644 --- a/index.pug +++ b/index.pug @@ -5,8 +5,9 @@ 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/style.css", rel="stylesheet") - link(href="docs/css/github.css", rel="stylesheet") + link(href="docs/css/mongoose5.css?v=" + Date.now(), rel="stylesheet") + link(href="docs/css/style.css?v=" + Date.now(), rel="stylesheet") + link(href="docs/css/github.css?v=" + Date.now(), rel="stylesheet") link(href="docs/css/carbonads.css", rel="stylesheet") include ./docs/includes/favicon @@ -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'; From 35d6c70c72e6516c16f96e8d79d7e8e9b56832b2 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 25 Nov 2025 10:50:55 -0500 Subject: [PATCH 002/133] fix(bulkWrite): pass overwriteImmutable option to castUpdate fixes Fix #15781 Backport #15782 to 7.x --- lib/helpers/model/castBulkWrite.js | 14 +++++++++---- lib/helpers/query/castUpdate.js | 4 ++-- lib/helpers/query/handleImmutable.js | 6 +++++- test/model.updateOne.test.js | 30 ++++++++++++++++++++++++++++ 4 files changed, 47 insertions(+), 7 deletions(-) diff --git a/lib/helpers/model/castBulkWrite.js b/lib/helpers/model/castBulkWrite.js index 71d9150f848..56ee9548062 100644 --- a/lib/helpers/model/castBulkWrite.js +++ b/lib/helpers/model/castBulkWrite.js @@ -69,7 +69,9 @@ module.exports = function castBulkWrite(originalModel, op, options) { if (model.schema.$timestamps != null && op['updateOne'].timestamps !== false) { const createdAt = model.schema.$timestamps.createdAt; const updatedAt = model.schema.$timestamps.updatedAt; - applyTimestampsToUpdate(now, createdAt, updatedAt, op['updateOne']['update'], {}); + applyTimestampsToUpdate(now, createdAt, updatedAt, op['updateOne']['update'], { + timestamps: op['updateOne'].timestamps + }); } if (op['updateOne'].timestamps !== false) { @@ -100,7 +102,8 @@ module.exports = function castBulkWrite(originalModel, op, options) { op['updateOne']['update'] = castUpdate(model.schema, op['updateOne']['update'], { strict: strict, overwrite: false, - upsert: op['updateOne'].upsert + upsert: op['updateOne'].upsert, + overwriteImmutable: op['updateOne'].overwriteImmutable }, model, op['updateOne']['filter']); } catch (error) { return callback(error, null); @@ -136,7 +139,9 @@ module.exports = function castBulkWrite(originalModel, op, options) { if (model.schema.$timestamps != null && op['updateMany'].timestamps !== false) { const createdAt = model.schema.$timestamps.createdAt; const updatedAt = model.schema.$timestamps.updatedAt; - applyTimestampsToUpdate(now, createdAt, updatedAt, op['updateMany']['update'], {}); + applyTimestampsToUpdate(now, createdAt, updatedAt, op['updateMany']['update'], { + timestamps: op['updateMany'].timestamps + }); } if (op['updateMany'].timestamps !== false) { applyTimestampsToChildren(now, op['updateMany']['update'], model.schema); @@ -158,7 +163,8 @@ module.exports = function castBulkWrite(originalModel, op, options) { op['updateMany']['update'] = castUpdate(model.schema, op['updateMany']['update'], { strict: strict, overwrite: false, - upsert: op['updateMany'].upsert + upsert: op['updateMany'].upsert, + overwriteImmutable: op['updateMany'].overwriteImmutable }, model, op['updateMany']['filter']); } catch (error) { return callback(error, null); diff --git a/lib/helpers/query/castUpdate.js b/lib/helpers/query/castUpdate.js index 25fbb456ea1..2e097443c48 100644 --- a/lib/helpers/query/castUpdate.js +++ b/lib/helpers/query/castUpdate.js @@ -240,7 +240,7 @@ function walkUpdatePath(schema, obj, op, options, context, filter, pref) { if (op !== '$setOnInsert' && !options.overwrite && - handleImmutable(schematype, strict, obj, key, prefix + key, context)) { + handleImmutable(schematype, strict, obj, key, prefix + key, options, context)) { continue; } @@ -335,7 +335,7 @@ function walkUpdatePath(schema, obj, op, options, context, filter, pref) { // You can use `$setOnInsert` with immutable keys if (op !== '$setOnInsert' && !options.overwrite && - handleImmutable(schematype, strict, obj, key, prefix + key, context)) { + handleImmutable(schematype, strict, obj, key, prefix + key, options, context)) { continue; } diff --git a/lib/helpers/query/handleImmutable.js b/lib/helpers/query/handleImmutable.js index 22adb3c50de..dc26adbb3e6 100644 --- a/lib/helpers/query/handleImmutable.js +++ b/lib/helpers/query/handleImmutable.js @@ -2,7 +2,7 @@ const StrictModeError = require('../../error/strict'); -module.exports = function handleImmutable(schematype, strict, obj, key, fullPath, ctx) { +module.exports = function handleImmutable(schematype, strict, obj, key, fullPath, options, ctx) { if (schematype == null || !schematype.options || !schematype.options.immutable) { return false; } @@ -15,6 +15,10 @@ module.exports = function handleImmutable(schematype, strict, obj, key, fullPath return false; } + if (options && options.overwriteImmutable) { + return false; + } + if (strict === false) { return false; } diff --git a/test/model.updateOne.test.js b/test/model.updateOne.test.js index 8eab40a6a6b..5e5954c6c44 100644 --- a/test/model.updateOne.test.js +++ b/test/model.updateOne.test.js @@ -2707,6 +2707,36 @@ 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 }); + + const Model = db.model('Test', schema); + + await Model.create({ name: 'gh-15781' }); + let doc = await Model.collection.findOne({ name: 'gh-15781' }); + assert.ok(doc.createdAt.valueOf() >= start); + + 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()); + }); + it('updates buffers with `runValidators` successfully (gh-8580)', async function() { const Test = db.model('Test', Schema({ data: { type: Buffer, required: true } From 49200a18c9d9f7e2735c2597a768fd95bb256b43 Mon Sep 17 00:00:00 2001 From: White-Devil2839 Date: Wed, 26 Nov 2025 12:56:40 +0530 Subject: [PATCH 003/133] Fix: Requested changes --- docs/css/github.css | 123 ++++++++++++++++++++++++++++++++++++++++ docs/css/mongoose5.css | 8 +-- docs/css/style.css | 4 +- docs/js/theme-toggle.js | 67 ++++++++++++++-------- 4 files changed, 172 insertions(+), 30 deletions(-) diff --git a/docs/css/github.css b/docs/css/github.css index f4e134a0dd7..227688e7b51 100644 --- a/docs/css/github.css +++ b/docs/css/github.css @@ -169,6 +169,129 @@ github.com style (c) Vasily Polovnyov } /* Dark mode support for syntax highlighting */ +body.code-theme-dark code { + background-color: var(--code-bg, #2d2d2d); + color: var(--link-color, #4a9eff); +} + +body.code-theme-dark pre { + background-color: var(--code-bg, #2d2d2d); + border-color: var(--border-color, #444); + color: var(--code-text, #e0e0e0); +} + +body.code-theme-dark .hljs { + background: var(--code-bg, #2d2d2d); + color: var(--code-text, #e0e0e0); +} + +body.code-theme-dark .hljs-comment, +body.code-theme-dark .diff .hljs-header, +body.code-theme-dark .hljs-javadoc { + color: #6a9955; +} + +body.code-theme-dark .hljs-keyword, +body.code-theme-dark .css .rule .hljs-keyword, +body.code-theme-dark .hljs-winutils, +body.code-theme-dark .nginx .hljs-title, +body.code-theme-dark .hljs-subst, +body.code-theme-dark .hljs-request, +body.code-theme-dark .hljs-status { + color: #569cd6; + font-weight: bold; +} + +body.code-theme-dark .hljs-number, +body.code-theme-dark .hljs-hexcolor, +body.code-theme-dark .ruby .hljs-constant { + color: #b5cea8; +} + +body.code-theme-dark .hljs-string, +body.code-theme-dark .hljs-tag .hljs-value, +body.code-theme-dark .hljs-phpdoc, +body.code-theme-dark .hljs-dartdoc, +body.code-theme-dark .tex .hljs-formula { + color: #ce9178; +} + +body.code-theme-dark .hljs-title, +body.code-theme-dark .hljs-id, +body.code-theme-dark .scss .hljs-preprocessor { + color: #d7ba7d; + font-weight: bold; +} + +body.code-theme-dark .hljs-class .hljs-title, +body.code-theme-dark .hljs-type, +body.code-theme-dark .vhdl .hljs-literal, +body.code-theme-dark .tex .hljs-command { + color: #4ec9b0; + font-weight: bold; +} + +body.code-theme-dark .hljs-tag, +body.code-theme-dark .hljs-tag .hljs-title, +body.code-theme-dark .hljs-rules .hljs-property, +body.code-theme-dark .django .hljs-tag .hljs-keyword { + color: #569cd6; + font-weight: normal; +} + +body.code-theme-dark .hljs-attribute, +body.code-theme-dark .hljs-variable, +body.code-theme-dark .lisp .hljs-body { + color: #9cdcfe; +} + +body.code-theme-dark .hljs-regexp { + color: #d16969; +} + +body.code-theme-dark .hljs-symbol, +body.code-theme-dark .ruby .hljs-symbol .hljs-string, +body.code-theme-dark .lisp .hljs-keyword, +body.code-theme-dark .clojure .hljs-keyword, +body.code-theme-dark .scheme .hljs-keyword, +body.code-theme-dark .tex .hljs-special, +body.code-theme-dark .hljs-prompt { + color: #c586c0; +} + +body.code-theme-dark .hljs-built_in { + color: #4fc1ff; +} + +body.code-theme-dark .hljs-preprocessor, +body.code-theme-dark .hljs-pragma, +body.code-theme-dark .hljs-pi, +body.code-theme-dark .hljs-doctype, +body.code-theme-dark .hljs-shebang, +body.code-theme-dark .hljs-cdata { + color: #808080; + font-weight: bold; +} + +body.code-theme-dark .hljs-deletion { + background: #5a1d1d; + color: #f48771; +} + +body.code-theme-dark .hljs-addition { + background: #1e3a1e; + color: #b5cea8; +} + +body.code-theme-dark .diff .hljs-change { + background: #2d4d2d; + color: #4ec9b0; +} + +body.code-theme-dark .hljs-chunk { + color: #808080; +} + @media (prefers-color-scheme: dark) { code { background-color: var(--code-bg, #2d2d2d); diff --git a/docs/css/mongoose5.css b/docs/css/mongoose5.css index 141c6a7c89c..a6cd2e5c892 100644 --- a/docs/css/mongoose5.css +++ b/docs/css/mongoose5.css @@ -676,8 +676,8 @@ li.version ul.pure-menu-children { /* Theme Toggle Button */ #theme-toggle { position: fixed; - top: 10px; - left: 10px; + bottom: 20px; + right: 20px; z-index: 1000; background-color: var(--bg-secondary); border: 1px solid var(--border-color); @@ -751,7 +751,7 @@ li.version ul.pure-menu-children { /* On mobile, adjust position */ @media (max-width: 1160px) { #theme-toggle { - top: 5px; - left: 50px; + bottom: 15px; + right: 15px; } } diff --git a/docs/css/style.css b/docs/css/style.css index 095d187f27b..2f4a2ca6194 100644 --- a/docs/css/style.css +++ b/docs/css/style.css @@ -31,8 +31,8 @@ body[data-theme="light"] { /* Theme Toggle Button for Homepage */ #theme-toggle { position: fixed; - top: 10px; - left: 10px; + bottom: 20px; + right: 20px; z-index: 1000; background-color: rgba(255, 255, 255, 0.9); border: 1px solid #ddd; diff --git a/docs/js/theme-toggle.js b/docs/js/theme-toggle.js index df57c22c08e..44ae26f967d 100644 --- a/docs/js/theme-toggle.js +++ b/docs/js/theme-toggle.js @@ -1,43 +1,52 @@ (function() { 'use strict'; - // Get saved theme preference or default to system preference + const STORAGE_KEY = 'mongoose-theme'; + const CODE_THEME_CLASS = 'code-theme-dark'; + const supportsMatchMedia = typeof window !== 'undefined' && typeof window.matchMedia === 'function'; + const prefersDarkQuery = supportsMatchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null; + function getInitialTheme() { - const savedTheme = localStorage.getItem('mongoose-theme'); + const savedTheme = localStorage.getItem(STORAGE_KEY); if (savedTheme === 'light' || savedTheme === 'dark') { return savedTheme; } - // Default to system preference - return null; + return null; // Follow system preference + } + + function getEffectiveTheme(theme) { + if (theme === 'light' || theme === 'dark') { + return theme; + } + return prefersDarkQuery && prefersDarkQuery.matches ? 'dark' : 'light'; + } + + function syncCodeTheme(theme) { + const effectiveTheme = getEffectiveTheme(theme); + const isDark = effectiveTheme === 'dark'; + document.documentElement.classList.toggle(CODE_THEME_CLASS, isDark); + if (document.body) { + document.body.classList.toggle(CODE_THEME_CLASS, isDark); + } } - // Apply theme to document function applyTheme(theme) { if (theme === 'light' || theme === 'dark') { document.documentElement.setAttribute('data-theme', theme); - localStorage.setItem('mongoose-theme', theme); + localStorage.setItem(STORAGE_KEY, theme); } else { document.documentElement.removeAttribute('data-theme'); - localStorage.removeItem('mongoose-theme'); + localStorage.removeItem(STORAGE_KEY); } + syncCodeTheme(theme); updateThemeIcon(); } - // Update theme icon visibility function updateThemeIcon() { - // CSS handles the icon visibility, but we can ensure proper state - const theme = document.documentElement.getAttribute('data-theme'); - const lightIcon = document.getElementById('theme-icon-light'); // Sun icon - const darkIcon = document.getElementById('theme-icon-dark'); // Moon icon - - if (lightIcon && darkIcon) { - // In dark mode: show sun icon (to switch to light) - // In light mode: show moon icon (to switch to dark) - // CSS handles the actual visibility via opacity - } + // CSS handles the icon visibility. This function exists for future enhancements. + void document.documentElement.getAttribute('data-theme'); } - // Toggle between light and dark function toggleTheme() { const currentTheme = document.documentElement.getAttribute('data-theme'); if (currentTheme === 'dark') { @@ -45,24 +54,34 @@ } else if (currentTheme === 'light') { applyTheme(null); // Reset to system preference } else { - // Currently using system preference, switch to dark applyTheme('dark'); } } - // Initialize theme on page load + function handleSystemThemeChange() { + if (!localStorage.getItem(STORAGE_KEY)) { + syncCodeTheme(null); + } + } + function initTheme() { const initialTheme = getInitialTheme(); applyTheme(initialTheme); - - // Set up toggle button + const toggleBtn = document.getElementById('theme-toggle-btn'); if (toggleBtn) { toggleBtn.addEventListener('click', toggleTheme); } + + if (prefersDarkQuery) { + if (typeof prefersDarkQuery.addEventListener === 'function') { + prefersDarkQuery.addEventListener('change', handleSystemThemeChange); + } else if (typeof prefersDarkQuery.addListener === 'function') { + prefersDarkQuery.addListener(handleSystemThemeChange); + } + } } - // Run when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initTheme); } else { From b635f4e98918f7e8d669c48ce129e53251c48a6d Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 25 Nov 2025 16:43:27 -0500 Subject: [PATCH 004/133] types(schema): allow calling schema.static() with `as TStatics` Re: #15780 --- test/types/queries.test.ts | 13 +++++++++++++ types/index.d.ts | 1 + 2 files changed, 14 insertions(+) diff --git a/test/types/queries.test.ts b/test/types/queries.test.ts index 2f729f4e5cd..64c7384b7bc 100644 --- a/test/types/queries.test.ts +++ b/test/types/queries.test.ts @@ -798,3 +798,16 @@ function gh15671() { }; }; } + +async function gh15786() { + interface IDoc { + } + + interface DocStatics { + m1(): void; + m2(): void; + } + + const schema = new Schema, {}, {}, {}, DocStatics>({}); + schema.static({ m1() {} } as DocStatics); +} diff --git a/types/index.d.ts b/types/index.d.ts index 8e8b5ad67f9..30fb89a9abd 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -542,6 +542,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(name: string, fn: (this: TModelType, ...args: any[]) => any): this; From d193bd9580e7b6c5f20cfb263fddf8fcdb1bfe88 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 26 Nov 2025 16:46:55 -0500 Subject: [PATCH 005/133] Update test/types/queries.test.ts Co-authored-by: hasezoey --- test/types/queries.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/types/queries.test.ts b/test/types/queries.test.ts index 64c7384b7bc..090e2c2bdc3 100644 --- a/test/types/queries.test.ts +++ b/test/types/queries.test.ts @@ -804,8 +804,8 @@ async function gh15786() { } interface DocStatics { - m1(): void; - m2(): void; + m1(): void; + m2(): void; } const schema = new Schema, {}, {}, {}, DocStatics>({}); From 1405e4f36cda0c3f0b6afd2e05b8b3800613dc70 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 26 Nov 2025 16:48:07 -0500 Subject: [PATCH 006/133] Fix typo in IDoc interface property name --- test/types/queries.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/types/queries.test.ts b/test/types/queries.test.ts index 090e2c2bdc3..16e85e51484 100644 --- a/test/types/queries.test.ts +++ b/test/types/queries.test.ts @@ -801,6 +801,7 @@ function gh15671() { async function gh15786() { interface IDoc { + nmae: string; } interface DocStatics { From 344d8e72986148c54296ceb9e826e4edee5a08f8 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 28 Nov 2025 12:57:28 -0500 Subject: [PATCH 007/133] fix(model): bump version if necessary after successful bulkSave() Fix #15800 --- lib/document.js | 16 ++++++++++++++++ lib/model.js | 13 +++++-------- test/model.test.js | 47 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 8 deletions(-) diff --git a/lib/document.js b/lib/document.js index 15764c75687..cd0ca2d326f 100644 --- a/lib/document.js +++ b/lib/document.js @@ -5422,6 +5422,22 @@ 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;// increment version if was successful + this.$__setValue(key, version + 1); + } +}; + /*! * Module exports. */ diff --git a/lib/model.js b/lib/model.js index 67f017b6a49..e7247344a55 100644 --- a/lib/model.js +++ b/lib/model.js @@ -523,11 +523,9 @@ Model.prototype.$__save = function(options, callback) { const versionBump = this.$__.version; // was this an update that required a version bump? if (versionBump && !this.$__.inserting) { - const doIncrement = VERSION_INC === (VERSION_INC & this.$__.version); - this.$__.version = undefined; - const key = this.$__schema.options.versionKey; - const version = this.$__getValue(key) || 0; if (numAffected <= 0) { + const key = this.$__schema.options.versionKey; + const version = this.$__getValue(key) || 0; // the update failed. pass an error back this.$__undoReset(); const err = this.$__.$versionError || @@ -535,10 +533,7 @@ Model.prototype.$__save = function(options, callback) { return callback(err, this); } - // increment version if was successful - if (doIncrement) { - this.$__setValue(key, version + 1); - } + this._applyVersionIncrement(); } if (result != null && numAffected <= 0) { this.$__undoReset(); @@ -3666,6 +3661,8 @@ function handleSuccessfulWrite(document) { } document.$__reset(); + document._applyVersionIncrement(); + document.schema.s.hooks.execPost('save', document, [document], {}, (err) => { if (err) { reject(err); diff --git a/test/model.test.js b/test/model.test.js index cb28be62aaa..264e59f90a8 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -7015,6 +7015,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 } From 505d6a08afe221707aa61f4e3a0a589f9c802ae9 Mon Sep 17 00:00:00 2001 From: Phillip Huang Date: Fri, 28 Nov 2025 19:29:07 -0600 Subject: [PATCH 008/133] Export InferPojoType to replicate Mongoose 8 InferRawDocType behavior --- test/types/inferrawdoctype.test.ts | 26 ++++++++++++++++++++++++-- types/inferrawdoctype.d.ts | 8 +++++++- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/test/types/inferrawdoctype.test.ts b/test/types/inferrawdoctype.test.ts index c67ae090fae..743bb85be73 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 InferPojoType, 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 = InferPojoType< typeof schemaDefinition>; + expectType<{ email: string, password: string, dateOfBirth: Date }>({} as UserType); +} function gh14839() { const schemaDefinition = { email: { diff --git a/types/inferrawdoctype.d.ts b/types/inferrawdoctype.d.ts index 9e54ef3ea0e..6c49ccc257a 100644 --- a/types/inferrawdoctype.d.ts +++ b/types/inferrawdoctype.d.ts @@ -12,7 +12,7 @@ declare module 'mongoose' { ? ObtainSchemaGeneric : FlattenMaps>>; - export type InferRawDocType< + export type InferPojoType< SchemaDefinition, TSchemaOptions extends Record = DefaultSchemaOptions, TTransformOptions = { bufferToBinary: false } @@ -25,6 +25,12 @@ declare module 'mongoose' { : ObtainRawDocumentPathType | null; }, 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. * Make sure to check for `any` because `T extends { __rawDocTypeHint: infer U }` will infer `unknown` if T is `any`. From 95a2be74a9736a97416412d75bed49af2445192e Mon Sep 17 00:00:00 2001 From: Phillip Huang Date: Fri, 28 Nov 2025 19:35:53 -0600 Subject: [PATCH 009/133] actually make the change --- types/inferrawdoctype.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/types/inferrawdoctype.d.ts b/types/inferrawdoctype.d.ts index 6c49ccc257a..906e4571581 100644 --- a/types/inferrawdoctype.d.ts +++ b/types/inferrawdoctype.d.ts @@ -16,14 +16,14 @@ declare module 'mongoose' { 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, From 8efa7908485525795f728677d61902ac9f18af57 Mon Sep 17 00:00:00 2001 From: Hafez Date: Sun, 30 Nov 2025 01:22:34 +0100 Subject: [PATCH 010/133] fix(types): add `overwriteImmutable` types re #15781 --- package.json | 4 +-- test/model.updateOne.test.js | 69 +++++++++++++++++++++++------------- test/types/models.test.ts | 39 ++++++++++++++++++-- types/models.d.ts | 4 +++ 4 files changed, 87 insertions(+), 29 deletions(-) diff --git a/package.json b/package.json index 83770bb1df6..179838c6fe2 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "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": "npx mocha ./test/*.test.js --inspect --watch --recursive --watch-files 'test/**/*.js' --watch-files 'lib/**/*.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", "attest-benchmark": "node ./benchmarks/typescript/infer.bench.mts" @@ -139,4 +139,4 @@ "target": "ES2022" } } -} +} \ No newline at end of file diff --git a/test/model.updateOne.test.js b/test/model.updateOne.test.js index aef03e3d375..dd71b88ec38 100644 --- a/test/model.updateOne.test.js +++ b/test/model.updateOne.test.js @@ -2680,34 +2680,53 @@ 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' }); + + // Act + await User.bulkWrite([{ + updateOne: { + filter: { _id: user._id }, + update: { ssn: '999-99-9999' }, + overwriteImmutable: true + } + }]); - const Model = db.model('Test', schema); + // Assert + const updatedUser = await User.findById(user._id); + assert.strictEqual(updatedUser.ssn, '999-99-9999'); + }); - await Model.create({ name: 'gh-15781' }); - let doc = await Model.collection.findOne({ name: 'gh-15781' }); - assert.ok(doc.createdAt.valueOf() >= start); + 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 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()); + // Act + await User.bulkWrite([{ + updateMany: { + filter: { _id: user._id }, + update: { ssn: '000-00-0000' }, + overwriteImmutable: true + } + }]); + + // Assert + const updatedUser = await User.findById(user._id); + assert.strictEqual(updatedUser.ssn, '000-00-0000'); + }); + + function createTestContext() { + const userSchema = new Schema({ + name: String, + ssn: { type: String, immutable: true } + }); + const User = db.model('User', userSchema); + return { User }; + } }); it('updates buffers with `runValidators` successfully (gh-8580)', async function() { diff --git a/test/types/models.test.ts b/test/types/models.test.ts index 8142f71b848..b44c6c975e2 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -17,11 +17,13 @@ import mongoose, { createConnection, connection, model, - ObtainSchemaGeneric + 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 { @@ -1089,3 +1091,36 @@ async function gh15693() { User.schema.methods.printName.apply(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/types/models.d.ts b/types/models.d.ts index 627666c24e8..a049861f63e 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -224,6 +224,8 @@ declare module 'mongoose' { upsert?: boolean; /** When false, do not add timestamps. When true, overrides the `timestamps` option set in the `bulkWrite` options. */ timestamps?: boolean; + /** When true, allows updating fields that are marked as `immutable` in the schema. */ + overwriteImmutable?: boolean; } export interface UpdateManyModel { @@ -241,6 +243,8 @@ declare module 'mongoose' { upsert?: boolean; /** When false, do not add timestamps. When true, overrides the `timestamps` option set in the `bulkWrite` options. */ timestamps?: boolean; + /** When true, allows updating fields that are marked as `immutable` in the schema. */ + overwriteImmutable?: boolean; } export interface DeleteOneModel { From cc5fe1f3221e41dfca06288bbc00bcefb292fa21 Mon Sep 17 00:00:00 2001 From: Hafez Date: Sun, 30 Nov 2025 01:26:17 +0100 Subject: [PATCH 011/133] fix(types): update types for MongoUpdateOneModel and modelRemoveOptions function --- test/types/models.test.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/types/models.test.ts b/test/types/models.test.ts index b44c6c975e2..0f3c14b7c7f 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -14,10 +14,8 @@ import mongoose, { UpdateQuery, UpdateWriteOpResult, WithLevel1NestedPaths, - createConnection, connection, model, - ObtainSchemaGeneric, UpdateOneModel, UpdateManyModel } from 'mongoose'; @@ -415,7 +413,7 @@ function gh11911() { const Animal = model('Animal', animalSchema); const changes: UpdateQuery = {}; - expectAssignable({ + expectAssignable({ filter: {}, update: changes }); @@ -509,7 +507,7 @@ function gh12100() { })(); -function modelRemoveOptions() { +async function modelRemoveOptions() { const cmodel = model('Test', new Schema()); const res: DeleteResult = await cmodel.deleteOne({}, {}); From cc306f605805eac14aa2e7f4b04fbe6105fb3e86 Mon Sep 17 00:00:00 2001 From: Hafez Date: Sun, 30 Nov 2025 01:52:08 +0100 Subject: [PATCH 012/133] fix: add overwriteImmutable in bulkWrite, port bulkwrite enriched option types to 7.x --- lib/helpers/model/castBulkWrite.js | 22 +++++- test/model.test.js | 46 +++++++++++++ test/types/models.test.ts | 41 +++++++++++- types/models.d.ts | 103 ++++++++++++++++++++++++++++- 4 files changed, 205 insertions(+), 7 deletions(-) diff --git a/lib/helpers/model/castBulkWrite.js b/lib/helpers/model/castBulkWrite.js index 56ee9548062..0120117f337 100644 --- a/lib/helpers/model/castBulkWrite.js +++ b/lib/helpers/model/castBulkWrite.js @@ -28,7 +28,7 @@ module.exports = function castBulkWrite(originalModel, op, options) { const model = decideModelByObject(originalModel, op['insertOne']['document']); const doc = new model(op['insertOne']['document']); - if (model.schema.options.timestamps && options.timestamps !== false) { + if (model.schema.options.timestamps && getTimestampsOpt(op['insertOne'], options)) { doc.initializeTimestamps(); } if (options.session != null) { @@ -190,7 +190,7 @@ module.exports = function castBulkWrite(originalModel, op, options) { // set `skipId`, otherwise we get "_id field cannot be changed" const doc = new model(op['replaceOne']['replacement'], strict, true); - if (model.schema.options.timestamps) { + if (model.schema.options.timestamps && getTimestampsOpt(op['replaceOne'], options)) { doc.initializeTimestamps(); } if (options.session != null) { @@ -279,3 +279,21 @@ function decideModelByObject(model, object) { } return model; } + +/** + * Gets timestamps option for a given operation. If the option is set within an individual + * operation, use it. Otherwise, use the global timestamps option configured in the `bulkWrite` + * options. Overall default is `true`. + * @api private + */ + +function getTimestampsOpt(opCommand, options) { + const opLevelOpt = opCommand.timestamps; + const bulkLevelOpt = options.timestamps; + if (opLevelOpt != null) { + return opLevelOpt; + } else if (bulkLevelOpt != null) { + return bulkLevelOpt; + } + return true; +} diff --git a/test/model.test.js b/test/model.test.js index 04516afa3be..d660960a154 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -5926,6 +5926,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({ diff --git a/test/types/models.test.ts b/test/types/models.test.ts index 0e00f16fc74..08fc0824b9b 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -13,11 +13,13 @@ import mongoose, { Query, UpdateWriteOpResult, AggregateOptions, - StringSchemaDefinition + StringSchemaDefinition, + UpdateOneModel, + UpdateManyModel } from 'mongoose'; import { expectAssignable, expectError, expectType } from 'tsd'; import { AutoTypedSchemaType, autoTypedSchema } from './schema.test'; -import { UpdateOneModel, ChangeStreamInsertDocument, ObjectId } from 'mongodb'; +import { UpdateOneModel as MongoUpdateOneModel, ChangeStreamInsertDocument, ObjectId } from 'mongodb'; function rawDocSyntax(): void { interface ITest { @@ -415,7 +417,7 @@ function gh11911() { const Animal = model('Animal', animalSchema); const changes: UpdateQuery = {}; - expectAssignable({ + expectAssignable({ filter: {}, update: changes }); @@ -766,3 +768,36 @@ async function gh14003() { await TestModel.validate({ name: 'foo' }, ['name']); await TestModel.validate({ name: 'foo' }, { pathsToSkip: ['name'] }); } + +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/types/models.d.ts b/types/models.d.ts index 7cb082bc372..7c40f30d52d 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -142,6 +142,105 @@ declare module 'mongoose' { interface RemoveOptions extends SessionOption, Omit {} + export type AnyBulkWriteOperation = { + insertOne: InsertOneModel; + } | { + replaceOne: ReplaceOneModel; + } | { + updateOne: UpdateOneModel; + } | { + updateMany: UpdateManyModel; + } | { + deleteOne: DeleteOneModel; + } | { + deleteMany: DeleteManyModel; + }; + + export interface InsertOneModel { + document: mongodb.OptionalId; + /** 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: FilterQuery; + /** 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; + /** 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 UpdateOneModel { + /** The filter to limit the updated documents. */ + filter: FilterQuery; + /** A document or pipeline containing update operators. */ + update: UpdateQuery | 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; + /** 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 UpdateManyModel { + /** The filter to limit the updated documents. */ + filter: FilterQuery; + /** A document or pipeline containing update operators. */ + update: UpdateQuery | 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; + /** 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 DeleteOneModel { + /** The filter to limit the deleted documents. */ + filter: FilterQuery; + /** 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 interface DeleteManyModel { + /** The filter to limit the deleted documents. */ + filter: FilterQuery; + /** 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; + } + const Model: Model; /** @@ -185,11 +284,11 @@ declare module 'mongoose' { * round trip to the MongoDB server. */ bulkWrite( - writes: Array>, + writes: Array>, options: mongodb.BulkWriteOptions & MongooseBulkWriteOptions & { ordered: false } ): Promise; bulkWrite( - writes: Array>, + writes: Array>, options?: mongodb.BulkWriteOptions & MongooseBulkWriteOptions ): Promise; From 542c8072a43dbae140394270a5b0def10d3301dc Mon Sep 17 00:00:00 2001 From: Hafez Date: Sun, 30 Nov 2025 02:17:54 +0100 Subject: [PATCH 013/133] perf: use native Buffer.equals() for buffer comparison --- lib/utils.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/lib/utils.js b/lib/utils.js index 1cef22784c4..2b896dce28b 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -875,15 +875,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; From 29ab2236808a1370b65e0a7b114540c4d29d69b0 Mon Sep 17 00:00:00 2001 From: Harshit Date: Sun, 30 Nov 2025 21:00:45 +0530 Subject: [PATCH 014/133] fix(schema): Add enumValues property to Number enum for consistency with String enum This change makes Number enum behavior consistent with String enum by: - Initializing enumValues array in SchemaNumber constructor - Maintaining enumValues property when enum() is called - Supporting clearing enum when called with false/undefined - Using enumValues in validator closure for consistency Fixes inconsistency where schema.path('field').enumValues was available for String enums but not for Number enums. Includes comprehensive test coverage matching String enum test patterns. --- lib/schema/number.js | 16 +++++++++-- test/schema.validation.test.js | 50 ++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) 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/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]/ } From 215c7fa29313e05b52e798173e5242d7f8dd85a2 Mon Sep 17 00:00:00 2001 From: Arnav kumar Date: Sun, 30 Nov 2025 23:25:12 +0530 Subject: [PATCH 015/133] fix: increase timeout in flaky aggregation cursor test (gh-8835) The test 'closing aggregation cursor emits close event only once' was failing in CI environments due to a 20ms timeout being too short. Increased timeout to 200ms to allow sufficient time for the close event to be emitted, consistent with other tests in the file. --- test/query.cursor.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) { From caf00ef7f6f935154fdc8563ad104f29177d3a73 Mon Sep 17 00:00:00 2001 From: Devendra Date: Sun, 30 Nov 2025 23:23:19 +0530 Subject: [PATCH 016/133] fix(types): apply versionKey option in ApplySchemaOptions Fixes #15798 --- test/types/schema.test.ts | 8 ++++++++ types/inferschematype.d.ts | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index 75e90f50268..0fccb59a2be 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -2086,3 +2086,11 @@ function gh15751() { const doc = new TestModel(); expectType(doc.myId); } + +function gh15798() { + const schema1 = new Schema({ name: String }, { statics: { testMe() { } }, versionKey: false }); + model("M1", schema1).testMe(); + + const schema2 = new Schema({ name: String }, { statics: { testMe() { } }, timestamps: true }); + model("M2", schema2).testMe(); +} diff --git a/types/inferschematype.d.ts b/types/inferschematype.d.ts index ed758828054..9b9b30da16a 100644 --- a/types/inferschematype.d.ts +++ b/types/inferschematype.d.ts @@ -114,7 +114,7 @@ declare module 'mongoose' { type ResolveSchemaOptions = MergeType; - type ApplySchemaOptions = ResolveTimestamps; + type ApplySchemaOptions = Default__v, O>; type DefaultTimestampProps = { createdAt: NativeDate; From 3b545e842d660ada55c8ae67ad751ed706bbc74d Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 1 Dec 2025 17:32:03 -0500 Subject: [PATCH 017/133] bump instantiations --- scripts/tsc-diagnostics-check.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/tsc-diagnostics-check.js b/scripts/tsc-diagnostics-check.js index 79435503cd0..fcebf5415f5 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]) ? 390000 : parseInt(process.argv[2], 10); console.log(stdin); From 7a305a8b5b60437714a5110fae8ddb34e6ed679c Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 1 Dec 2025 17:37:36 -0500 Subject: [PATCH 018/133] Update lib/document.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- lib/document.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/document.js b/lib/document.js index cd0ca2d326f..e9b1e63e90b 100644 --- a/lib/document.js +++ b/lib/document.js @@ -5433,8 +5433,8 @@ Document.prototype._applyVersionIncrement = function _applyVersionIncrement() { this.$__.version = undefined; if (doIncrement) { const key = this.$__schema.options.versionKey; - const version = this.$__getValue(key) || 0;// increment version if was successful - this.$__setValue(key, version + 1); + const version = this.$__getValue(key) || 0; + this.$__setValue(key, version + 1); // increment version if was successful } }; From a5e89da341b612f1f0c110a7595be77e1b1d4b48 Mon Sep 17 00:00:00 2001 From: Hafez Date: Tue, 2 Dec 2025 01:14:10 +0100 Subject: [PATCH 019/133] fix: address PR feedback --- lib/helpers/model/castBulkWrite.js | 21 +----- types/models.d.ts | 109 +++++++---------------------- 2 files changed, 27 insertions(+), 103 deletions(-) diff --git a/lib/helpers/model/castBulkWrite.js b/lib/helpers/model/castBulkWrite.js index 0120117f337..7a503fcee81 100644 --- a/lib/helpers/model/castBulkWrite.js +++ b/lib/helpers/model/castBulkWrite.js @@ -28,7 +28,7 @@ module.exports = function castBulkWrite(originalModel, op, options) { const model = decideModelByObject(originalModel, op['insertOne']['document']); const doc = new model(op['insertOne']['document']); - if (model.schema.options.timestamps && getTimestampsOpt(op['insertOne'], options)) { + if (model.schema.options.timestamps && (op['insertOne'].timestamps ?? options.timestamps ?? true)) { doc.initializeTimestamps(); } if (options.session != null) { @@ -190,7 +190,7 @@ module.exports = function castBulkWrite(originalModel, op, options) { // set `skipId`, otherwise we get "_id field cannot be changed" const doc = new model(op['replaceOne']['replacement'], strict, true); - if (model.schema.options.timestamps && getTimestampsOpt(op['replaceOne'], options)) { + if (model.schema.options.timestamps && (op['replaceOne'].timestamps ?? options.timestamps ?? true)) { doc.initializeTimestamps(); } if (options.session != null) { @@ -280,20 +280,3 @@ function decideModelByObject(model, object) { return model; } -/** - * Gets timestamps option for a given operation. If the option is set within an individual - * operation, use it. Otherwise, use the global timestamps option configured in the `bulkWrite` - * options. Overall default is `true`. - * @api private - */ - -function getTimestampsOpt(opCommand, options) { - const opLevelOpt = opCommand.timestamps; - const bulkLevelOpt = options.timestamps; - if (opLevelOpt != null) { - return opLevelOpt; - } else if (bulkLevelOpt != null) { - return bulkLevelOpt; - } - return true; -} diff --git a/types/models.d.ts b/types/models.d.ts index 7c40f30d52d..90e7e1456e7 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -142,104 +142,45 @@ declare module 'mongoose' { interface RemoveOptions extends SessionOption, Omit {} - export type AnyBulkWriteOperation = { - insertOne: InsertOneModel; - } | { - replaceOne: ReplaceOneModel; - } | { - updateOne: UpdateOneModel; - } | { - updateMany: UpdateManyModel; - } | { - deleteOne: DeleteOneModel; - } | { - deleteMany: DeleteManyModel; - }; - - export interface InsertOneModel { - document: mongodb.OptionalId; - /** 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: FilterQuery; - /** 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; + 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 UpdateOneModel { - /** The filter to limit the updated documents. */ - filter: FilterQuery; - /** A document or pipeline containing update operators. */ - update: UpdateQuery | 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; + 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 UpdateManyModel { - /** The filter to limit the updated documents. */ - filter: FilterQuery; - /** A document or pipeline containing update operators. */ - update: UpdateQuery | 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; - /** 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 type InsertOneModel = + mongodb.InsertOneModel & MongooseBulkWritePerOperationOptions; - export interface DeleteOneModel { - /** The filter to limit the deleted documents. */ - filter: FilterQuery; - /** 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 ReplaceOneModel = + mongodb.ReplaceOneModel & MongooseBulkWritePerOperationOptions; - export interface DeleteManyModel { - /** The filter to limit the deleted documents. */ - filter: FilterQuery; - /** 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 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; From 7f2255283c90f463c864544fe214bd6fe49bbfbb Mon Sep 17 00:00:00 2001 From: Hafez Date: Tue, 2 Dec 2025 01:38:57 +0100 Subject: [PATCH 020/133] fix: address PR feedback --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 179838c6fe2..11c8706bbe7 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "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": "npx mocha ./test/*.test.js --inspect --watch --recursive --watch-files 'test/**/*.js' --watch-files 'lib/**/*.js'", + "tdd": "mocha ./test/*.test.js --inspect --watch --recursive --watch-files 'test/**/*.js' --watch-files 'lib/**/*.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", "attest-benchmark": "node ./benchmarks/typescript/infer.bench.mts" From 03855f4afebef025d707316220a83f9fea6574a4 Mon Sep 17 00:00:00 2001 From: Hafez Date: Tue, 2 Dec 2025 01:39:45 +0100 Subject: [PATCH 021/133] fix: add --inspect flag to tdd script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 11c8706bbe7..f7c634ea437 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "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 'test/**/*.js' --watch-files 'lib/**/*.js'", + "tdd": "mocha --watch --inspect --recursive ./test/*.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", "attest-benchmark": "node ./benchmarks/typescript/infer.bench.mts" From 88187c545fa6885f750233d64afdfd921dca4da1 Mon Sep 17 00:00:00 2001 From: Hafez Date: Tue, 2 Dec 2025 01:45:01 +0100 Subject: [PATCH 022/133] chore: fix lint --- test/types/schema.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index 0fccb59a2be..e42c867241c 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -2089,8 +2089,8 @@ function gh15751() { function gh15798() { const schema1 = new Schema({ name: String }, { statics: { testMe() { } }, versionKey: false }); - model("M1", schema1).testMe(); + model('M1', schema1).testMe(); const schema2 = new Schema({ name: String }, { statics: { testMe() { } }, timestamps: true }); - model("M2", schema2).testMe(); + model('M2', schema2).testMe(); } From 3e1e886baae00783118adab89e1f4268a9aa7567 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Dec 2025 00:57:38 +0000 Subject: [PATCH 023/133] chore(deps): bump mongodb/atlas-github-action from 0.2.1 to 0.2.2 Bumps [mongodb/atlas-github-action](https://github.com/mongodb/atlas-github-action) from 0.2.1 to 0.2.2. - [Release notes](https://github.com/mongodb/atlas-github-action/releases) - [Commits](https://github.com/mongodb/atlas-github-action/compare/v0.2.1...v0.2.2) --- updated-dependencies: - dependency-name: mongodb/atlas-github-action dependency-version: 0.2.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c3e923ac4e2..c05939444cb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -114,7 +114,7 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.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 From 335e68779b95792b95f1c54afe218dd4b4fc4195 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Dec 2025 00:57:48 +0000 Subject: [PATCH 024/133] chore(deps): bump actions/checkout from 5.0.0 to 6.0.0 Bumps [actions/checkout](https://github.com/actions/checkout) from 5.0.0 to 6.0.0. - [Release notes](https://github.com/actions/checkout/releases) - [Commits](https://github.com/actions/checkout/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 6.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/benchmark.yml | 2 +- .github/workflows/codeql.yml | 2 +- .github/workflows/documentation.yml | 4 ++-- .github/workflows/encryption-tests.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/test.yml | 12 ++++++------ .github/workflows/tidelift-alignment.yml | 2 +- .github/workflows/tsd.yml | 4 ++-- 8 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 8f45a0266f3..a040e4f7e3c 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-22.04 name: Benchmark TypeScript Types steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: fetch-depth: 0 - name: Setup node 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..5ceefa667a2 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 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c3e923ac4e2..0c4ca6ba996 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 @@ -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,7 +112,7 @@ 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 - name: Setup local Atlas deployment @@ -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 From 3fef3ff784d9b154d6c1492cb28f65d09ae18838 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Dec 2025 01:59:39 +0000 Subject: [PATCH 025/133] chore(deps-dev): bump markdownlint-cli2 from 0.18.1 to 0.19.1 Bumps [markdownlint-cli2](https://github.com/DavidAnson/markdownlint-cli2) from 0.18.1 to 0.19.1. - [Changelog](https://github.com/DavidAnson/markdownlint-cli2/blob/main/CHANGELOG.md) - [Commits](https://github.com/DavidAnson/markdownlint-cli2/compare/v0.18.1...v0.19.1) --- updated-dependencies: - dependency-name: markdownlint-cli2 dependency-version: 0.19.1 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 83770bb1df6..dccf10e349f 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "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", From 44aa0e0b088c85223b9884cc832b5918bffbf546 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Dec 2025 01:59:42 +0000 Subject: [PATCH 026/133] chore(deps-dev): bump mocha from 11.7.4 to 11.7.5 Bumps [mocha](https://github.com/mochajs/mocha) from 11.7.4 to 11.7.5. - [Release notes](https://github.com/mochajs/mocha/releases) - [Changelog](https://github.com/mochajs/mocha/blob/v11.7.5/CHANGELOG.md) - [Commits](https://github.com/mochajs/mocha/compare/v11.7.4...v11.7.5) --- updated-dependencies: - dependency-name: mocha dependency-version: 11.7.5 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 83770bb1df6..7eb447257f0 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "markdownlint-cli2": "^0.18.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-runner": "^6.0.0", From d8d0efe94a462e1981e61c1df36e3157bc867cd7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Dec 2025 01:59:50 +0000 Subject: [PATCH 027/133] chore(deps-dev): bump @ark/attest from 0.53.0 to 0.55.0 Bumps [@ark/attest](https://github.com/arktypeio/arktype/tree/HEAD/ark/attest) from 0.53.0 to 0.55.0. - [Release notes](https://github.com/arktypeio/arktype/releases) - [Changelog](https://github.com/arktypeio/arktype/blob/main/ark/attest/CHANGELOG.md) - [Commits](https://github.com/arktypeio/arktype/commits/@ark/attest@0.55.0/ark/attest) --- updated-dependencies: - dependency-name: "@ark/attest" dependency-version: 0.55.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 83770bb1df6..4fac1505908 100644 --- a/package.json +++ b/package.json @@ -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", From d7729efe621feef32cb455877ce19e7fa116f6db Mon Sep 17 00:00:00 2001 From: Lomesh Sonkeshriya Date: Tue, 2 Dec 2025 12:10:45 +0530 Subject: [PATCH 028/133] Fix typo in text search error path and variable bug in double casting - Fix typo in lib/schema/operators/text.js: '' -> '' This ensures error messages correctly reference the field - Fix variable reference bug in lib/cast/double.js: use 'tempVal' instead of 'val' When casting objects with valueOf()/toString() methods, the extracted string value should be used instead of the original object --- lib/cast/double.js | 2 +- lib/schema/operators/text.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/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, From ba08ce07aadbf3930e42807fc786bf5157c739c5 Mon Sep 17 00:00:00 2001 From: Ansh Date: Wed, 3 Dec 2025 00:29:58 +0530 Subject: [PATCH 029/133] fix: clear timeout in collection operations to prevent memory leak - Add clearTimeout() calls in success and error paths - Prevents setTimeout references from accumulating - Fixes memory leak in high-throughput applications - Add test to verify timeout cleanup behavior Fixes memory leak where buffered collection operations would create setTimeout timers that were never cleared when operations completed successfully, causing continuous memory growth. --- lib/connection.js | 7 +-- lib/drivers/node-mongodb-native/collection.js | 15 +++++ lib/drivers/node-mongodb-native/connection.js | 45 ++++++++------- test/collection.timeout.test.js | 57 +++++++++++++++++++ 4 files changed, 100 insertions(+), 24 deletions(-) create mode 100644 test/collection.timeout.test.js diff --git a/lib/connection.js b/lib/connection.js index 8d9d27e8ff0..5d23e61d56f 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -69,7 +69,6 @@ function Connection(base) { this.config = {}; this.replica = false; this.options = null; - this.otherDbs = []; // FIXME: To be replaced with relatedDbs this.relatedDbs = {}; // Hashmap of other dbs that share underlying connection this.states = STATES; this._readyState = STATES.disconnected; @@ -137,8 +136,8 @@ Object.defineProperty(Connection.prototype, 'readyState', { if (this._readyState !== val) { this._readyState = val; - // [legacy] loop over the otherDbs on this connection and change their state - for (const db of this.otherDbs) { + // loop over the relatedDbs on this connection and change their state + for (const db of Object.values(this.relatedDbs)) { db.readyState = val; } @@ -1330,7 +1329,7 @@ Connection.prototype.onClose = function onClose(force) { const wasForceClosed = typeof force === 'object' && force !== null ? force.force : force; - for (const db of this.otherDbs) { + for (const db of Object.values(this.relatedDbs)) { this._destroyCalled ? db.destroy({ force: wasForceClosed, skipCloseClient: true }) : db.close({ force: wasForceClosed, skipCloseClient: true }); } }; diff --git a/lib/drivers/node-mongodb-native/collection.js b/lib/drivers/node-mongodb-native/collection.js index 1e4678ff93f..838d601310e 100644 --- a/lib/drivers/node-mongodb-native/collection.js +++ b/lib/drivers/node-mongodb-native/collection.js @@ -239,6 +239,9 @@ function iter(i) { if (syncCollectionMethods[i] && typeof lastArg === 'function') { const result = collection[i].apply(collection, _args.slice(0, _args.length - 1)); + if (timeout != null) { + clearTimeout(timeout); + } this.conn.emit('operation-end', { _id: opId, modelName: _this.modelName, collectionName: this.name, method: i, result }); return lastArg.call(this, null, result); } @@ -247,6 +250,9 @@ function iter(i) { if (ret != null && typeof ret.then === 'function') { return ret.then( result => { + if (timeout != null) { + clearTimeout(timeout); + } if (typeof lastArg === 'function') { lastArg(null, result); } else { @@ -255,6 +261,9 @@ function iter(i) { return result; }, error => { + if (timeout != null) { + clearTimeout(timeout); + } if (typeof lastArg === 'function') { lastArg(error); return; @@ -265,10 +274,16 @@ function iter(i) { } ); } + 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 (timeout != null) { + clearTimeout(timeout); + } if (typeof lastArg === 'function') { return lastArg(error); } else { diff --git a/lib/drivers/node-mongodb-native/connection.js b/lib/drivers/node-mongodb-native/connection.js index 3f4be863c2e..1adb924e714 100644 --- a/lib/drivers/node-mongodb-native/connection.js +++ b/lib/drivers/node-mongodb-native/connection.js @@ -113,14 +113,13 @@ NativeConnection.prototype.useDb = function(name, options) { newConn.name = name; - // push onto the otherDbs stack, this is used when state changes - this.otherDbs.push(newConn); - newConn.otherDbs.push(this); - - // push onto the relatedDbs cache, this is used when state changes - if (options && options.useCache) { - this.relatedDbs[newConn.name] = newConn; - newConn.relatedDbs = this.relatedDbs; + // Add to relatedDbs for state synchronization and caching + this.relatedDbs[newConn.name] = newConn; + newConn.relatedDbs = this.relatedDbs; + + // Ensure the parent connection is also tracked in the new connection's relatedDbs + if (this.name && !newConn.relatedDbs[this.name]) { + newConn.relatedDbs[this.name] = this; } return newConn; @@ -160,19 +159,21 @@ NativeConnection.prototype.aggregate = function aggregate(pipeline, options) { */ NativeConnection.prototype.removeDb = function removeDb(name) { - const dbs = this.otherDbs.filter(db => db.name === name); - if (!dbs.length) { + const db = this.relatedDbs[name]; + if (!db) { throw new MongooseError(`No connections to database "${name}" found`); } - for (const db of dbs) { - db._closeCalled = true; - db._destroyCalled = true; - db._readyState = STATES.disconnected; - db.$wasForceClosed = true; + db._closeCalled = true; + db._destroyCalled = true; + db._readyState = STATES.disconnected; + db.$wasForceClosed = true; + + // Remove from all related connections' relatedDbs + for (const relatedDb of Object.values(this.relatedDbs)) { + delete relatedDb.relatedDbs[name]; } delete this.relatedDbs[name]; - this.otherDbs = this.otherDbs.filter(db => db.name !== name); }; /** @@ -345,8 +346,10 @@ NativeConnection.prototype.createClient = async function createClient(uri, optio _setClient(this, client, options, dbName); - for (const db of this.otherDbs) { - _setClient(db, client, {}, db.name); + for (const db of Object.values(this.relatedDbs)) { + if (db !== this) { + _setClient(db, client, {}, db.name); + } } return this; }; @@ -494,8 +497,10 @@ function _setClient(conn, client, options, dbName) { client.on('serverHeartbeatSucceeded', () => { conn._lastHeartbeatAt = Date.now(); - for (const otherDb of conn.otherDbs) { - otherDb._lastHeartbeatAt = conn._lastHeartbeatAt; + for (const otherDb of Object.values(conn.relatedDbs)) { + if (otherDb !== conn) { + otherDb._lastHeartbeatAt = conn._lastHeartbeatAt; + } } }); diff --git a/test/collection.timeout.test.js b/test/collection.timeout.test.js new file mode 100644 index 00000000000..5e539f72a93 --- /dev/null +++ b/test/collection.timeout.test.js @@ -0,0 +1,57 @@ +'use strict'; + +const assert = require('assert'); +const mongoose = require('../index'); + +describe('Collection timeout cleanup', function() { + let db; + + before(async function() { + db = await mongoose.createConnection('mongodb://127.0.0.1:27017/mongoose_test').asPromise(); + }); + + after(async function() { + await db.close(); + }); + + it('should clear timeout on successful operations', async function() { + const TestModel = db.model('Test', { name: String }); + + // Track active handles before operations + const initialHandles = process._getActiveHandles().length; + + // Execute multiple operations that should complete successfully + for (let i = 0; i < 10; i++) { + await TestModel.find({}).exec(); + } + + // Allow some time for any lingering timeouts + await new Promise(resolve => setTimeout(resolve, 100)); + + // Active handles should not have grown significantly + const finalHandles = process._getActiveHandles().length; + assert.ok(finalHandles <= initialHandles + 2, + `Expected handles to remain stable, but grew from ${initialHandles} to ${finalHandles}`); + }); + + it('should clear timeout on operation errors', async function() { + const TestModel = db.model('Test2', { name: String }); + + const initialHandles = process._getActiveHandles().length; + + // Execute operations that will fail + for (let i = 0; i < 5; i++) { + try { + await TestModel.collection.findOne({ $invalidOperator: true }); + } catch (err) { + // Expected to fail + } + } + + await new Promise(resolve => setTimeout(resolve, 100)); + + const finalHandles = process._getActiveHandles().length; + assert.ok(finalHandles <= initialHandles + 2, + `Expected handles to remain stable after errors, but grew from ${initialHandles} to ${finalHandles}`); + }); +}); \ No newline at end of file From ba491cad935f136f315e91aff764901df45596b4 Mon Sep 17 00:00:00 2001 From: Ansh Date: Wed, 3 Dec 2025 02:30:11 +0530 Subject: [PATCH 030/133] revert: remove unrelated otherDbs changes Keep only the setTimeout memory leak fix as requested by maintainers. Revert changes to connection.js that were unrelated to the timeout issue. --- lib/connection.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/connection.js b/lib/connection.js index 5d23e61d56f..8d9d27e8ff0 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -69,6 +69,7 @@ function Connection(base) { this.config = {}; this.replica = false; this.options = null; + this.otherDbs = []; // FIXME: To be replaced with relatedDbs this.relatedDbs = {}; // Hashmap of other dbs that share underlying connection this.states = STATES; this._readyState = STATES.disconnected; @@ -136,8 +137,8 @@ Object.defineProperty(Connection.prototype, 'readyState', { if (this._readyState !== val) { this._readyState = val; - // loop over the relatedDbs on this connection and change their state - for (const db of Object.values(this.relatedDbs)) { + // [legacy] loop over the otherDbs on this connection and change their state + for (const db of this.otherDbs) { db.readyState = val; } @@ -1329,7 +1330,7 @@ Connection.prototype.onClose = function onClose(force) { const wasForceClosed = typeof force === 'object' && force !== null ? force.force : force; - for (const db of Object.values(this.relatedDbs)) { + for (const db of this.otherDbs) { this._destroyCalled ? db.destroy({ force: wasForceClosed, skipCloseClient: true }) : db.close({ force: wasForceClosed, skipCloseClient: true }); } }; From 728e0399c9d0fdad757b7da6b9bfe9f7138c5c1b Mon Sep 17 00:00:00 2001 From: Ansh Date: Wed, 3 Dec 2025 02:40:44 +0530 Subject: [PATCH 031/133] fix: resolve timeout variable scope issue Move timeout variable declaration to function scope to ensure it's accessible in all success/error paths for proper cleanup. --- lib/drivers/node-mongodb-native/collection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/drivers/node-mongodb-native/collection.js b/lib/drivers/node-mongodb-native/collection.js index 838d601310e..b4a61fe075a 100644 --- a/lib/drivers/node-mongodb-native/collection.js +++ b/lib/drivers/node-mongodb-native/collection.js @@ -124,6 +124,7 @@ function iter(i) { let _args = args; let callback = null; + let timeout = null; if (this._shouldBufferCommands() && this.buffer) { this.conn.emit('buffer', { _id: opId, @@ -136,7 +137,6 @@ function iter(i) { let callback; let _args = args; let promise = null; - let timeout = null; if (syncCollectionMethods[i] && typeof lastArg === 'function') { this.addQueue(i, _args); callback = lastArg; From 615876aacdd39d04d095af3b38c7ecc391bca7be Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 2 Dec 2025 18:45:18 -0500 Subject: [PATCH 032/133] style: fix lint --- test/types/schema.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index 0fccb59a2be..e42c867241c 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -2089,8 +2089,8 @@ function gh15751() { function gh15798() { const schema1 = new Schema({ name: String }, { statics: { testMe() { } }, versionKey: false }); - model("M1", schema1).testMe(); + model('M1', schema1).testMe(); const schema2 = new Schema({ name: String }, { statics: { testMe() { } }, timestamps: true }); - model("M2", schema2).testMe(); + model('M2', schema2).testMe(); } From 787e084c6feca866a046d93ea3add80d8265777b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Dec 2025 23:51:43 +0000 Subject: [PATCH 033/133] chore(deps-dev): bump eslint from 9.25.1 to 9.39.1 Bumps [eslint](https://github.com/eslint/eslint) from 9.25.1 to 9.39.1. - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](https://github.com/eslint/eslint/compare/v9.25.1...v9.39.1) --- updated-dependencies: - dependency-name: eslint dependency-version: 9.39.1 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 66bcc6e68cd..b5603234480 100644 --- a/package.json +++ b/package.json @@ -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", From c1063ace0504899c8b0c23464a4875d25ebe4567 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 2 Dec 2025 18:56:47 -0500 Subject: [PATCH 034/133] revert #15825 --- test/types/schema.test.ts | 8 -------- types/inferschematype.d.ts | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index e42c867241c..75e90f50268 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -2086,11 +2086,3 @@ function gh15751() { const doc = new TestModel(); expectType(doc.myId); } - -function gh15798() { - const schema1 = new Schema({ name: String }, { statics: { testMe() { } }, versionKey: false }); - model('M1', schema1).testMe(); - - const schema2 = new Schema({ name: String }, { statics: { testMe() { } }, timestamps: true }); - model('M2', schema2).testMe(); -} diff --git a/types/inferschematype.d.ts b/types/inferschematype.d.ts index 9b9b30da16a..ed758828054 100644 --- a/types/inferschematype.d.ts +++ b/types/inferschematype.d.ts @@ -114,7 +114,7 @@ declare module 'mongoose' { type ResolveSchemaOptions = MergeType; - type ApplySchemaOptions = Default__v, O>; + type ApplySchemaOptions = ResolveTimestamps; type DefaultTimestampProps = { createdAt: NativeDate; From 3f5e6e91f079b67c5abdde46c4a5f3dc0307ce4b Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 2 Dec 2025 20:46:29 -0500 Subject: [PATCH 035/133] Update docs/api_split.pug Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/api_split.pug | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api_split.pug b/docs/api_split.pug index 78ff1b1a140..56b9d496596 100644 --- a/docs/api_split.pug +++ b/docs/api_split.pug @@ -1,7 +1,7 @@ extends layout append style - link(rel="stylesheet", href=`${versions.versionedPath}/docs/css/api.css?v=${Date.now()}`) + link(rel="stylesheet", href=`${versions.versionedPath}/docs/css/api.css?v=${versions.version}`) script(src=`${versions.versionedPath}/docs/js/api-bold-current-nav.js`) script(src=`${versions.versionedPath}/docs/js/convert-old-anchorid.js`) From dc35cfad22d68731fed5b8638d6554f662f03d34 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 2 Dec 2025 20:49:44 -0500 Subject: [PATCH 036/133] Update API CSS link in api_split.pug Removed version query parameter from API CSS link. --- docs/api_split.pug | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api_split.pug b/docs/api_split.pug index 56b9d496596..2078d1b28ee 100644 --- a/docs/api_split.pug +++ b/docs/api_split.pug @@ -1,7 +1,7 @@ extends layout append style - link(rel="stylesheet", href=`${versions.versionedPath}/docs/css/api.css?v=${versions.version}`) + link(rel="stylesheet", href=`${versions.versionedPath}/docs/css/api.css`) script(src=`${versions.versionedPath}/docs/js/api-bold-current-nav.js`) script(src=`${versions.versionedPath}/docs/js/convert-old-anchorid.js`) From fddbf21f0c6222ddb2562f57886df89452d0a48b Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 2 Dec 2025 20:54:56 -0500 Subject: [PATCH 037/133] Change logo text color and adjust theme toggle position --- docs/css/mongoose5.css | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/css/mongoose5.css b/docs/css/mongoose5.css index a6cd2e5c892..8cae0fda911 100644 --- a/docs/css/mongoose5.css +++ b/docs/css/mongoose5.css @@ -171,7 +171,7 @@ h4 a:focus-visible { } .logo-text { - color: var(--link-color); + color: #800; font-size: 20pt; position: relative; top: 0px; @@ -676,7 +676,7 @@ li.version ul.pure-menu-children { /* Theme Toggle Button */ #theme-toggle { position: fixed; - bottom: 20px; + top: 20px; right: 20px; z-index: 1000; background-color: var(--bg-secondary); @@ -751,6 +751,7 @@ li.version ul.pure-menu-children { /* On mobile, adjust position */ @media (max-width: 1160px) { #theme-toggle { + top: auto; bottom: 15px; right: 15px; } From 8af7b0b03473eef390aa23253df9c4076da1b80c Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 2 Dec 2025 20:57:54 -0500 Subject: [PATCH 038/133] Remove cache-busting query parameters from CSS links --- index.pug | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/index.pug b/index.pug index f219d428acd..b0fd5bffcfa 100644 --- a/index.pug +++ b/index.pug @@ -5,9 +5,9 @@ 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?v=" + Date.now(), rel="stylesheet") - link(href="docs/css/style.css?v=" + Date.now(), rel="stylesheet") - link(href="docs/css/github.css?v=" + Date.now(), rel="stylesheet") + 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") include ./docs/includes/favicon From 72150d346b67c3da8a9361bce58d2dcf608c1962 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 2 Dec 2025 21:04:01 -0500 Subject: [PATCH 039/133] Adjust theme toggle button position --- docs/css/style.css | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/css/style.css b/docs/css/style.css index 2f4a2ca6194..2e404c97b0d 100644 --- a/docs/css/style.css +++ b/docs/css/style.css @@ -31,6 +31,7 @@ body[data-theme="light"] { /* Theme Toggle Button for Homepage */ #theme-toggle { position: fixed; + top: auto; bottom: 20px; right: 20px; z-index: 1000; From 6145cca8187f786bc7bbf0e24c9272e6f572f9f9 Mon Sep 17 00:00:00 2001 From: Hafez Date: Wed, 3 Dec 2025 03:45:21 +0100 Subject: [PATCH 040/133] fix(model): make `overwriteImmutable` work on `createdAt` regardless of `timestamps` value --- lib/helpers/model/castBulkWrite.js | 6 +- lib/helpers/update/applyTimestampsToUpdate.js | 61 +++++++++++-------- test/model.updateOne.test.js | 49 ++++++++++++++- 3 files changed, 87 insertions(+), 29 deletions(-) diff --git a/lib/helpers/model/castBulkWrite.js b/lib/helpers/model/castBulkWrite.js index 3f61c4e6ad3..d8c8bda78ac 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) { diff --git a/lib/helpers/update/applyTimestampsToUpdate.js b/lib/helpers/update/applyTimestampsToUpdate.js index e8d3217fbb9..fd0828b5a00 100644 --- a/lib/helpers/update/applyTimestampsToUpdate.js +++ b/lib/helpers/update/applyTimestampsToUpdate.js @@ -80,33 +80,46 @@ function applyTimestampsToUpdate(now, createdAt, updatedAt, currentUpdate, optio } 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/test/model.updateOne.test.js b/test/model.updateOne.test.js index dd71b88ec38..d3f909f405c 100644 --- a/test/model.updateOne.test.js +++ b/test/model.updateOne.test.js @@ -2685,12 +2685,13 @@ describe('model: updateOne: ', 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: { ssn: '999-99-9999' }, + update: { createdAt: customCreatedAt, ssn: '999-99-9999' }, overwriteImmutable: true } }]); @@ -2698,18 +2699,20 @@ describe('model: updateOne: ', function() { // 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: { ssn: '000-00-0000' }, + update: { createdAt: customCreatedAt, ssn: '000-00-0000' }, overwriteImmutable: true } }]); @@ -2717,13 +2720,53 @@ describe('model: updateOne: ', function() { // 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()); + }); + function createTestContext() { const userSchema = new Schema({ name: String, ssn: { type: String, immutable: true } - }); + }, { timestamps: true }); const User = db.model('User', userSchema); return { User }; } From 27cbe27d4f9c2bc93f9015e2766fb79f642051a6 Mon Sep 17 00:00:00 2001 From: Hafez Date: Wed, 3 Dec 2025 03:46:31 +0100 Subject: [PATCH 041/133] chore: add `--watch-files` to `tdd` script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3ae5d82c3b1..1e0f0c6ac86 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "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 --watch --inspect --recursive ./test/*.test.js", + "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", "attest-benchmark": "node ./benchmarks/typescript/infer.bench.mts" From ff6831ef1eb0d87bc3609af8137eade5875288aa Mon Sep 17 00:00:00 2001 From: Hafez Date: Wed, 3 Dec 2025 03:55:46 +0100 Subject: [PATCH 042/133] test(model): add more tests for overwriteImmutable with bulkWrite --- test/model.updateOne.test.js | 112 +++++++++++++++++++++++++++-------- 1 file changed, 87 insertions(+), 25 deletions(-) diff --git a/test/model.updateOne.test.js b/test/model.updateOne.test.js index 5e5954c6c44..b6ad030a218 100644 --- a/test/model.updateOne.test.js +++ b/test/model.updateOne.test.js @@ -2707,34 +2707,96 @@ 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.only('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()); + }); + + 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() { From 706b9e46b4f349f01037ac2d320061951647d224 Mon Sep 17 00:00:00 2001 From: Hafez Date: Wed, 3 Dec 2025 03:56:24 +0100 Subject: [PATCH 043/133] fix(model): update createdAt when overwriteImmutable is true regardless of `timestamps` option --- lib/helpers/model/castBulkWrite.js | 6 +- lib/helpers/update/applyTimestampsToUpdate.js | 61 +++++++++++-------- 2 files changed, 41 insertions(+), 26 deletions(-) diff --git a/lib/helpers/model/castBulkWrite.js b/lib/helpers/model/castBulkWrite.js index 7a503fcee81..49b0ded06df 100644 --- a/lib/helpers/model/castBulkWrite.js +++ b/lib/helpers/model/castBulkWrite.js @@ -70,7 +70,8 @@ module.exports = function castBulkWrite(originalModel, op, options) { const createdAt = model.schema.$timestamps.createdAt; const updatedAt = model.schema.$timestamps.updatedAt; applyTimestampsToUpdate(now, createdAt, updatedAt, op['updateOne']['update'], { - timestamps: op['updateOne'].timestamps + timestamps: op['updateOne'].timestamps, + overwriteImmutable: op['updateOne'].overwriteImmutable }); } @@ -140,7 +141,8 @@ module.exports = function castBulkWrite(originalModel, op, options) { const createdAt = model.schema.$timestamps.createdAt; const updatedAt = model.schema.$timestamps.updatedAt; applyTimestampsToUpdate(now, createdAt, updatedAt, op['updateMany']['update'], { - timestamps: op['updateMany'].timestamps + timestamps: op['updateMany'].timestamps, + overwriteImmutable: op['updateMany'].overwriteImmutable }); } if (op['updateMany'].timestamps !== false) { diff --git a/lib/helpers/update/applyTimestampsToUpdate.js b/lib/helpers/update/applyTimestampsToUpdate.js index b48febafb69..ae83b403117 100644 --- a/lib/helpers/update/applyTimestampsToUpdate.js +++ b/lib/helpers/update/applyTimestampsToUpdate.js @@ -81,33 +81,46 @@ function applyTimestampsToUpdate(now, createdAt, updatedAt, currentUpdate, optio } 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; + } } } From 35ee075a2adafcd4456945b7ff4c0cdaa6a82810 Mon Sep 17 00:00:00 2001 From: Hafez Date: Wed, 3 Dec 2025 03:58:47 +0100 Subject: [PATCH 044/133] test: remove .only --- test/model.updateOne.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/model.updateOne.test.js b/test/model.updateOne.test.js index b6ad030a218..b1f78d9baaf 100644 --- a/test/model.updateOne.test.js +++ b/test/model.updateOne.test.js @@ -2707,7 +2707,7 @@ describe('model: updateOne: ', function() { assert.equal(doc.age, 20); }); - describe.only('bulkWrite overwriteImmutable option (gh-15781)', function() { + describe('bulkWrite overwriteImmutable option (gh-15781)', function() { it('updateOne can update immutable field with overwriteImmutable: true', async function() { // Arrange const { User } = createTestContext(); From f826b66d6c7019488f3c4a40f2eb917409b126ca Mon Sep 17 00:00:00 2001 From: Ansh Date: Wed, 3 Dec 2025 09:20:46 +0530 Subject: [PATCH 045/133] Fix setTimeout memory leak in buffered collection operations - Add clearTimeout() calls in success/error paths to prevent memory leaks - Move timeout variable to function scope for proper cleanup access - Revert unrelated otherDbs/relatedDbs changes as requested by maintainer - Move tests to collection.test.js as requested --- lib/drivers/node-mongodb-native/connection.js | 45 +++++++-------- test/collection.timeout.test.js | 57 ------------------- 2 files changed, 20 insertions(+), 82 deletions(-) delete mode 100644 test/collection.timeout.test.js diff --git a/lib/drivers/node-mongodb-native/connection.js b/lib/drivers/node-mongodb-native/connection.js index 1adb924e714..3f4be863c2e 100644 --- a/lib/drivers/node-mongodb-native/connection.js +++ b/lib/drivers/node-mongodb-native/connection.js @@ -113,13 +113,14 @@ NativeConnection.prototype.useDb = function(name, options) { newConn.name = name; - // Add to relatedDbs for state synchronization and caching - this.relatedDbs[newConn.name] = newConn; - newConn.relatedDbs = this.relatedDbs; - - // Ensure the parent connection is also tracked in the new connection's relatedDbs - if (this.name && !newConn.relatedDbs[this.name]) { - newConn.relatedDbs[this.name] = this; + // push onto the otherDbs stack, this is used when state changes + this.otherDbs.push(newConn); + newConn.otherDbs.push(this); + + // push onto the relatedDbs cache, this is used when state changes + if (options && options.useCache) { + this.relatedDbs[newConn.name] = newConn; + newConn.relatedDbs = this.relatedDbs; } return newConn; @@ -159,21 +160,19 @@ NativeConnection.prototype.aggregate = function aggregate(pipeline, options) { */ NativeConnection.prototype.removeDb = function removeDb(name) { - const db = this.relatedDbs[name]; - if (!db) { + const dbs = this.otherDbs.filter(db => db.name === name); + if (!dbs.length) { throw new MongooseError(`No connections to database "${name}" found`); } - db._closeCalled = true; - db._destroyCalled = true; - db._readyState = STATES.disconnected; - db.$wasForceClosed = true; - - // Remove from all related connections' relatedDbs - for (const relatedDb of Object.values(this.relatedDbs)) { - delete relatedDb.relatedDbs[name]; + for (const db of dbs) { + db._closeCalled = true; + db._destroyCalled = true; + db._readyState = STATES.disconnected; + db.$wasForceClosed = true; } delete this.relatedDbs[name]; + this.otherDbs = this.otherDbs.filter(db => db.name !== name); }; /** @@ -346,10 +345,8 @@ NativeConnection.prototype.createClient = async function createClient(uri, optio _setClient(this, client, options, dbName); - for (const db of Object.values(this.relatedDbs)) { - if (db !== this) { - _setClient(db, client, {}, db.name); - } + for (const db of this.otherDbs) { + _setClient(db, client, {}, db.name); } return this; }; @@ -497,10 +494,8 @@ function _setClient(conn, client, options, dbName) { client.on('serverHeartbeatSucceeded', () => { conn._lastHeartbeatAt = Date.now(); - for (const otherDb of Object.values(conn.relatedDbs)) { - if (otherDb !== conn) { - otherDb._lastHeartbeatAt = conn._lastHeartbeatAt; - } + for (const otherDb of conn.otherDbs) { + otherDb._lastHeartbeatAt = conn._lastHeartbeatAt; } }); diff --git a/test/collection.timeout.test.js b/test/collection.timeout.test.js deleted file mode 100644 index 5e539f72a93..00000000000 --- a/test/collection.timeout.test.js +++ /dev/null @@ -1,57 +0,0 @@ -'use strict'; - -const assert = require('assert'); -const mongoose = require('../index'); - -describe('Collection timeout cleanup', function() { - let db; - - before(async function() { - db = await mongoose.createConnection('mongodb://127.0.0.1:27017/mongoose_test').asPromise(); - }); - - after(async function() { - await db.close(); - }); - - it('should clear timeout on successful operations', async function() { - const TestModel = db.model('Test', { name: String }); - - // Track active handles before operations - const initialHandles = process._getActiveHandles().length; - - // Execute multiple operations that should complete successfully - for (let i = 0; i < 10; i++) { - await TestModel.find({}).exec(); - } - - // Allow some time for any lingering timeouts - await new Promise(resolve => setTimeout(resolve, 100)); - - // Active handles should not have grown significantly - const finalHandles = process._getActiveHandles().length; - assert.ok(finalHandles <= initialHandles + 2, - `Expected handles to remain stable, but grew from ${initialHandles} to ${finalHandles}`); - }); - - it('should clear timeout on operation errors', async function() { - const TestModel = db.model('Test2', { name: String }); - - const initialHandles = process._getActiveHandles().length; - - // Execute operations that will fail - for (let i = 0; i < 5; i++) { - try { - await TestModel.collection.findOne({ $invalidOperator: true }); - } catch (err) { - // Expected to fail - } - } - - await new Promise(resolve => setTimeout(resolve, 100)); - - const finalHandles = process._getActiveHandles().length; - assert.ok(finalHandles <= initialHandles + 2, - `Expected handles to remain stable after errors, but grew from ${initialHandles} to ${finalHandles}`); - }); -}); \ No newline at end of file From 13d27bc9df9e3580e44fcbc0ef25c45f5022c391 Mon Sep 17 00:00:00 2001 From: Hafez Date: Wed, 3 Dec 2025 05:12:52 +0100 Subject: [PATCH 046/133] test: add tests for behavior when overwriteImmutable: false --- test/model.updateOne.test.js | 42 +++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/test/model.updateOne.test.js b/test/model.updateOne.test.js index d3f909f405c..d045f40d077 100644 --- a/test/model.updateOne.test.js +++ b/test/model.updateOne.test.js @@ -2723,7 +2723,7 @@ describe('model: updateOne: ', function() { assert.strictEqual(updatedUser.createdAt.valueOf(), customCreatedAt.valueOf()); }); - for (const timestamps of [true, false, null, undefined]) + 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 }); @@ -2761,6 +2761,46 @@ describe('model: updateOne: ', function() { 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', 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 } + } + + }, + { + updateMany: { + filter: { _id: users[1]._id }, + update: { ssn: '777-77-7777', createdAt: newCreatedAt } + } + } + ]); + + + // 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({ From 7cab3ca56e15fdebe23b69e04fc1fe044827576d Mon Sep 17 00:00:00 2001 From: Hafez Date: Wed, 3 Dec 2025 05:17:20 +0100 Subject: [PATCH 047/133] test(model): add test to assert inability to update immutable fields without overwriteImmutable: true --- test/model.updateOne.test.js | 42 +++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/test/model.updateOne.test.js b/test/model.updateOne.test.js index b1f78d9baaf..5a3ab96ff3a 100644 --- a/test/model.updateOne.test.js +++ b/test/model.updateOne.test.js @@ -2750,7 +2750,7 @@ describe('model: updateOne: ', function() { assert.strictEqual(updatedUser.createdAt.valueOf(), customCreatedAt.valueOf()); }); - for (const timestamps of [true, false, null, undefined]) + 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 }); @@ -2788,6 +2788,46 @@ describe('model: updateOne: ', function() { 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', 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 } + } + + }, + { + updateMany: { + filter: { _id: users[1]._id }, + update: { ssn: '777-77-7777', createdAt: newCreatedAt } + } + } + ]); + + + // 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({ From e9307b7043937c35390de70f3639a95d4a931d76 Mon Sep 17 00:00:00 2001 From: hasezoey Date: Wed, 3 Dec 2025 16:26:02 +0100 Subject: [PATCH 048/133] chore(workflows/test): drop nodejs 18 As mongoose already requires 20.19 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dcf2752565b..8c4c1e53bea 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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: From 724174c4d4bbcd441d84acc619514da2b83e5a70 Mon Sep 17 00:00:00 2001 From: Hafez Date: Wed, 3 Dec 2025 20:34:13 +0100 Subject: [PATCH 049/133] test(model): make tests more strict by asserting `timestamps` option doesn't matter when passing `overwriteImmutable` and updating `createdAt` --- test/model.updateOne.test.js | 69 ++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/test/model.updateOne.test.js b/test/model.updateOne.test.js index d045f40d077..9841ebcd9ab 100644 --- a/test/model.updateOne.test.js +++ b/test/model.updateOne.test.js @@ -2680,7 +2680,7 @@ describe('model: updateOne: ', function() { assert.equal(doc.age, 20); }); - describe('bulkWrite overwriteImmutable option (gh-15781)', function() { + describe.only('bulkWrite overwriteImmutable option (gh-15781)', function() { it('updateOne can update immutable field with overwriteImmutable: true', async function() { // Arrange const { User } = createTestContext(); @@ -2761,46 +2761,47 @@ describe('model: updateOne: ', function() { 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', async function() { + 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 } - } + 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'); - }, - { - updateMany: { - filter: { _id: users[1]._id }, - update: { ssn: '777-77-7777', createdAt: newCreatedAt } + // 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 + 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()); - }); + assert.strictEqual(updatedUser2.ssn, '333-33-3333'); + assert.notStrictEqual(updatedUser2.createdAt.valueOf(), newCreatedAt.valueOf()); + }); + } function createTestContext() { const userSchema = new Schema({ From f64ad6da7c3f35e0d4c6b19eb3792035acaaa683 Mon Sep 17 00:00:00 2001 From: Hafez Date: Wed, 3 Dec 2025 20:35:27 +0100 Subject: [PATCH 050/133] test: remove `.only` --- test/model.updateOne.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/model.updateOne.test.js b/test/model.updateOne.test.js index 9841ebcd9ab..b567a255da9 100644 --- a/test/model.updateOne.test.js +++ b/test/model.updateOne.test.js @@ -2680,7 +2680,7 @@ describe('model: updateOne: ', function() { assert.equal(doc.age, 20); }); - describe.only('bulkWrite overwriteImmutable option (gh-15781)', function() { + describe('bulkWrite overwriteImmutable option (gh-15781)', function() { it('updateOne can update immutable field with overwriteImmutable: true', async function() { // Arrange const { User } = createTestContext(); From d775807e3a1f43490c38b4f6dc40636191c5f29f Mon Sep 17 00:00:00 2001 From: Hafez Date: Wed, 3 Dec 2025 20:36:32 +0100 Subject: [PATCH 051/133] test(model): make tests more strict by asserting `timestamps` does not matter when updating `createdAt` with `overwriteImmutable: true` --- test/model.updateOne.test.js | 67 ++++++++++++++++++------------------ 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/test/model.updateOne.test.js b/test/model.updateOne.test.js index 5a3ab96ff3a..3d1ca408ac3 100644 --- a/test/model.updateOne.test.js +++ b/test/model.updateOne.test.js @@ -2788,46 +2788,47 @@ describe('model: updateOne: ', function() { 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', async function() { + 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 } - } + 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'); - }, - { - updateMany: { - filter: { _id: users[1]._id }, - update: { ssn: '777-77-7777', createdAt: newCreatedAt } + // 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 + 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()); - }); + assert.strictEqual(updatedUser2.ssn, '333-33-3333'); + assert.notStrictEqual(updatedUser2.createdAt.valueOf(), newCreatedAt.valueOf()); + }); + } function createTestContext() { const userSchema = new Schema({ From d043ae128cd7d522dd6542c427979abd8b735677 Mon Sep 17 00:00:00 2001 From: Hafez Date: Thu, 4 Dec 2025 04:52:38 +0100 Subject: [PATCH 052/133] refactor: use Object.hasOwn instead of Object.prototype.hasOwnProperty(...) --- lib/aggregate.js | 2 +- lib/cast.js | 2 +- lib/connection.js | 18 ++++---- lib/document.js | 39 +++++++++-------- lib/drivers/node-mongodb-native/connection.js | 2 +- lib/helpers/common.js | 2 +- lib/helpers/indexes/applySchemaCollation.js | 2 +- lib/helpers/indexes/isDefaultIdIndex.js | 2 +- lib/helpers/model/applyMethods.js | 2 +- lib/helpers/model/castBulkWrite.js | 2 +- .../populate/getModelsMapForPopulate.js | 6 +-- lib/helpers/populate/modelNamesFromRefPath.js | 2 +- .../populate/removeDeselectedForeignField.js | 2 +- lib/helpers/projection/applyProjection.js | 2 +- .../query/getEmbeddedDiscriminatorPath.js | 2 +- lib/helpers/setDefaultsOnInsert.js | 4 +- lib/helpers/timestamps/setupTimestamps.js | 2 +- lib/helpers/update/applyTimestampsToUpdate.js | 2 +- lib/model.js | 14 +++---- lib/mongoose.js | 7 ++-- lib/query.js | 6 +-- lib/schema.js | 42 +++++++++---------- lib/schema/array.js | 2 +- lib/schemaType.js | 16 +++---- lib/types/array/index.js | 4 +- lib/types/documentArray/index.js | 4 +- lib/types/objectid.js | 2 +- lib/virtualType.js | 2 +- 28 files changed, 99 insertions(+), 95 deletions(-) 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/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..dba0cd34086 100644 --- a/lib/document.js +++ b/lib/document.js @@ -784,7 +784,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 +1167,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 +1217,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 +1523,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 +2005,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 +2049,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 +2360,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 +2368,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 +2422,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 +2461,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 +2469,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 +2607,7 @@ Document.prototype.isDirectSelected = function isDirectSelected(path) { return true; } - if (this.$__.selected.hasOwnProperty(path)) { + if (Object.hasOwn(this.$__.selected, path)) { return inclusive; } @@ -2779,7 +2779,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 +2850,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 +4193,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; } @@ -5101,7 +5106,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 || 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/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 d8c8bda78ac..62ad2153177 100644 --- a/lib/helpers/model/castBulkWrite.js +++ b/lib/helpers/model/castBulkWrite.js @@ -297,7 +297,7 @@ 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; 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..6ac27c73629 100644 --- a/lib/helpers/projection/applyProjection.js +++ b/lib/helpers/projection/applyProjection.js @@ -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 fd0828b5a00..751f967c72f 100644 --- a/lib/helpers/update/applyTimestampsToUpdate.js +++ b/lib/helpers/update/applyTimestampsToUpdate.js @@ -74,7 +74,7 @@ function applyTimestampsToUpdate(now, createdAt, updatedAt, currentUpdate, optio updates.$set[updatedAt] = now; } - if (updates.hasOwnProperty(updatedAt)) { + if (Object.hasOwn(updates, updatedAt)) { delete updates[updatedAt]; } } diff --git a/lib/model.js b/lib/model.js index 74d990b68c4..66b30e493cd 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); } @@ -2944,7 +2944,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 }; } @@ -3309,7 +3309,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 }; } @@ -4999,10 +4999,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/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..6dd47fa60e4 100644 --- a/lib/types/array/index.js +++ b/lib/types/array/index.js @@ -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 (Object.hasOwn(schematype?.virtuals, prop)) { schematype.virtuals[prop].applySetters(value, target); } else { __array[prop] = value; diff --git a/lib/types/documentArray/index.js b/lib/types/documentArray/index.js index ccc0d230fdb..4b495f008fc 100644 --- a/lib/types/documentArray/index.js +++ b/lib/types/documentArray/index.js @@ -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 (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/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]; } From 712aecceaad35710f04c0cbb643b0006b2046b76 Mon Sep 17 00:00:00 2001 From: Hafez Date: Thu, 4 Dec 2025 05:01:25 +0100 Subject: [PATCH 053/133] refactor: replace hasOwnProperty with Object.hasOwn for safer property checks --- lib/helpers/projection/applyProjection.js | 2 +- .../update/decorateUpdateWithVersionKey.js | 2 +- lib/types/array/index.js | 6 +++--- lib/types/documentArray/index.js | 8 ++++---- lib/utils.js | 16 +--------------- 5 files changed, 10 insertions(+), 24 deletions(-) diff --git a/lib/helpers/projection/applyProjection.js b/lib/helpers/projection/applyProjection.js index 6ac27c73629..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 { 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/types/array/index.js b/lib/types/array/index.js index 6dd47fa60e4..00137a7d3c7 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) { diff --git a/lib/types/documentArray/index.js b/lib/types/documentArray/index.js index 4b495f008fc..eb17e9e941a 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]; } diff --git a/lib/utils.js b/lib/utils.js index 2b896dce28b..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) { From 6757eb7540bebc3eec174ee615732509cfb3de60 Mon Sep 17 00:00:00 2001 From: Hafez Date: Thu, 4 Dec 2025 05:05:28 +0100 Subject: [PATCH 054/133] Update lib/types/array/index.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- lib/types/array/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/types/array/index.js b/lib/types/array/index.js index 00137a7d3c7..2466ce5ad51 100644 --- a/lib/types/array/index.js +++ b/lib/types/array/index.js @@ -99,7 +99,7 @@ function MongooseArray(values, path, doc, schematype) { mongooseArrayMethods.set.call(proxy, prop, value, false); } else if (Object.hasOwn(internals, prop)) { internals[prop] = value; - } else if (Object.hasOwn(schematype?.virtuals, prop)) { + } else if (schematype?.virtuals && Object.hasOwn(schematype.virtuals, prop)) { schematype.virtuals[prop].applySetters(value, target); } else { __array[prop] = value; From f0ee1d351756737622746e6c712f22fa6632eb57 Mon Sep 17 00:00:00 2001 From: Hafez Date: Thu, 4 Dec 2025 05:05:56 +0100 Subject: [PATCH 055/133] Update lib/types/documentArray/index.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- lib/types/documentArray/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/types/documentArray/index.js b/lib/types/documentArray/index.js index eb17e9e941a..8d30cc96714 100644 --- a/lib/types/documentArray/index.js +++ b/lib/types/documentArray/index.js @@ -93,7 +93,7 @@ function MongooseDocumentArray(values, path, doc, schematype) { DocumentArrayMethods.set.call(proxy, prop, value, false); } else if (Object.hasOwn(internals, prop)) { internals[prop] = value; - } else if (Object.hasOwn(schematype?.virtuals, prop)) { + } else if (schematype?.virtuals && Object.hasOwn(schematype.virtuals, prop)) { schematype.virtuals[prop].applySetters(value, target); } else { __array[prop] = value; From 17d3adfef1f5e1a683a18de399ef4bf771002803 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 4 Dec 2025 15:47:42 -0500 Subject: [PATCH 056/133] Refactor theme toggle functions for improved clarity --- docs/js/theme-toggle.js | 81 ++++++----------------------------------- 1 file changed, 12 insertions(+), 69 deletions(-) diff --git a/docs/js/theme-toggle.js b/docs/js/theme-toggle.js index 44ae26f967d..3fc019d4227 100644 --- a/docs/js/theme-toggle.js +++ b/docs/js/theme-toggle.js @@ -6,86 +6,29 @@ const supportsMatchMedia = typeof window !== 'undefined' && typeof window.matchMedia === 'function'; const prefersDarkQuery = supportsMatchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null; - function getInitialTheme() { - const savedTheme = localStorage.getItem(STORAGE_KEY); - if (savedTheme === 'light' || savedTheme === 'dark') { - return savedTheme; - } - return null; // Follow system preference - } - - function getEffectiveTheme(theme) { - if (theme === 'light' || theme === 'dark') { - return theme; - } - return prefersDarkQuery && prefersDarkQuery.matches ? 'dark' : 'light'; - } - - function syncCodeTheme(theme) { - const effectiveTheme = getEffectiveTheme(theme); - const isDark = effectiveTheme === 'dark'; - document.documentElement.classList.toggle(CODE_THEME_CLASS, isDark); - if (document.body) { - document.body.classList.toggle(CODE_THEME_CLASS, isDark); - } - } - - function applyTheme(theme) { - if (theme === 'light' || theme === 'dark') { - document.documentElement.setAttribute('data-theme', theme); - localStorage.setItem(STORAGE_KEY, theme); - } else { - document.documentElement.removeAttribute('data-theme'); - localStorage.removeItem(STORAGE_KEY); - } - syncCodeTheme(theme); - updateThemeIcon(); - } - - function updateThemeIcon() { - // CSS handles the icon visibility. This function exists for future enhancements. - void document.documentElement.getAttribute('data-theme'); - } - function toggleTheme() { const currentTheme = document.documentElement.getAttribute('data-theme'); if (currentTheme === 'dark') { applyTheme('light'); - } else if (currentTheme === 'light') { - applyTheme(null); // Reset to system preference } else { applyTheme('dark'); } } - function handleSystemThemeChange() { - if (!localStorage.getItem(STORAGE_KEY)) { - syncCodeTheme(null); - } - } - - function initTheme() { - const initialTheme = getInitialTheme(); - applyTheme(initialTheme); - - const toggleBtn = document.getElementById('theme-toggle-btn'); - if (toggleBtn) { - toggleBtn.addEventListener('click', toggleTheme); + function applyTheme(theme, skipSetStorage) { + document.documentElement.setAttribute('data-theme', theme); + if (!skipSetStorage) { + localStorage.setItem(STORAGE_KEY, theme); } - - if (prefersDarkQuery) { - if (typeof prefersDarkQuery.addEventListener === 'function') { - prefersDarkQuery.addEventListener('change', handleSystemThemeChange); - } else if (typeof prefersDarkQuery.addListener === 'function') { - prefersDarkQuery.addListener(handleSystemThemeChange); - } + const isDark = theme === 'dark'; + document.documentElement.classList.toggle(CODE_THEME_CLASS, isDark); + if (document.body) { + document.body.classList.toggle(CODE_THEME_CLASS, isDark); } } - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initTheme); - } else { - initTheme(); - } + const theme = localStorage.getItem(STORAGE_KEY) || (prefersDarkQuery?.matches ? 'dark' : 'light'); + applyTheme(theme, true); + const toggleBtn = document.getElementById('theme-toggle-btn'); + toggleBtn.addEventListener('click', toggleTheme); })(); - From 8144b3f88e2804a41233d78f063d3954286e66d2 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 4 Dec 2025 16:21:38 -0500 Subject: [PATCH 057/133] Refactor theme styles for dark and light modes --- docs/css/style.css | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/docs/css/style.css b/docs/css/style.css index 2e404c97b0d..ae06f764eaa 100644 --- a/docs/css/style.css +++ b/docs/css/style.css @@ -18,12 +18,12 @@ body { } /* Manual theme overrides */ -body[data-theme="dark"] { +[data-theme="dark"] { background-color: var(--bg-primary, #1a1a1a); color: var(--text-secondary, #c0c0c0); } -body[data-theme="light"] { +[data-theme="light"] { background-color: var(--bg-primary, #ffffff); color: var(--text-secondary, #333); } @@ -31,7 +31,6 @@ body[data-theme="light"] { /* Theme Toggle Button for Homepage */ #theme-toggle { position: fixed; - top: auto; bottom: 20px; right: 20px; z-index: 1000; @@ -42,7 +41,7 @@ body[data-theme="light"] { transition: background-color 0.3s ease, border-color 0.3s ease; } -body[data-theme="dark"] #theme-toggle { +[data-theme="dark"] #theme-toggle { background-color: rgba(26, 26, 26, 0.9); border-color: #444; } @@ -186,15 +185,13 @@ pre code { } /* Dark mode for homepage code blocks */ -@media (prefers-color-scheme: dark) { - pre { - background: var(--code-bg, #2d2d2d); - border-color: var(--border-color, #444); - } - code { - color: var(--code-text, #e0e0e0); - background-color: var(--code-bg, #2d2d2d); - } +[data-theme="dark"] pre { + background: var(--code-bg, #2d2d2d); + border-color: var(--border-color, #444); +} +[data-theme="dark"] code { + color: var(--code-text, #e0e0e0); + background-color: var(--code-bg, #2d2d2d); } #header { text-align: center; @@ -221,6 +218,7 @@ h2 a { font-size: 146px; font-weight: 100; text-indent: -23px; + color: #800; } .load #header .mongoose { letter-spacing: -14px; @@ -232,6 +230,10 @@ h2 a { text-align: center; margin: 7px 0; } +[data-theme="dark"] .tagline { + color: #f8f8f8; + text-shadow: 1px 1px #222; +} .blurb { text-align: center; font-style: oblique; @@ -254,7 +256,6 @@ h2 a { #links li { display: inline-block; margin: 0 15px; - background-color: #FEFEFE; } #links a { background: #444; From 5bbe4adfd103b5a843ec4c9616f2b1d9ede94a7f Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 4 Dec 2025 16:22:37 -0500 Subject: [PATCH 058/133] Refactor dark mode styles for syntax highlighting --- docs/css/github.css | 233 ++++++++++---------------------------------- 1 file changed, 54 insertions(+), 179 deletions(-) diff --git a/docs/css/github.css b/docs/css/github.css index 227688e7b51..f7aa548e0b7 100644 --- a/docs/css/github.css +++ b/docs/css/github.css @@ -169,254 +169,129 @@ github.com style (c) Vasily Polovnyov } /* Dark mode support for syntax highlighting */ -body.code-theme-dark code { +.code-theme-dark code { background-color: var(--code-bg, #2d2d2d); color: var(--link-color, #4a9eff); } -body.code-theme-dark pre { +.code-theme-dark pre { background-color: var(--code-bg, #2d2d2d); border-color: var(--border-color, #444); color: var(--code-text, #e0e0e0); } -body.code-theme-dark .hljs { +.code-theme-dark .hljs { background: var(--code-bg, #2d2d2d); color: var(--code-text, #e0e0e0); } -body.code-theme-dark .hljs-comment, -body.code-theme-dark .diff .hljs-header, -body.code-theme-dark .hljs-javadoc { +.code-theme-dark .hljs-comment, +.code-theme-dark .diff .hljs-header, +.code-theme-dark .hljs-javadoc { color: #6a9955; } -body.code-theme-dark .hljs-keyword, -body.code-theme-dark .css .rule .hljs-keyword, -body.code-theme-dark .hljs-winutils, -body.code-theme-dark .nginx .hljs-title, -body.code-theme-dark .hljs-subst, -body.code-theme-dark .hljs-request, -body.code-theme-dark .hljs-status { +.code-theme-dark .hljs-keyword, +.code-theme-dark .css .rule .hljs-keyword, +.code-theme-dark .hljs-winutils, +.code-theme-dark .nginx .hljs-title, +.code-theme-dark .hljs-subst, +.code-theme-dark .hljs-request, +.code-theme-dark .hljs-status { color: #569cd6; font-weight: bold; } -body.code-theme-dark .hljs-number, -body.code-theme-dark .hljs-hexcolor, -body.code-theme-dark .ruby .hljs-constant { +.code-theme-dark .hljs-number, +.code-theme-dark .hljs-hexcolor, +.code-theme-dark .ruby .hljs-constant { color: #b5cea8; } -body.code-theme-dark .hljs-string, -body.code-theme-dark .hljs-tag .hljs-value, -body.code-theme-dark .hljs-phpdoc, -body.code-theme-dark .hljs-dartdoc, -body.code-theme-dark .tex .hljs-formula { +.code-theme-dark .hljs-string, +.code-theme-dark .hljs-tag .hljs-value, +.code-theme-dark .hljs-phpdoc, +.code-theme-dark .hljs-dartdoc, +.code-theme-dark .tex .hljs-formula { color: #ce9178; } -body.code-theme-dark .hljs-title, -body.code-theme-dark .hljs-id, -body.code-theme-dark .scss .hljs-preprocessor { +.code-theme-dark .hljs-title, +.code-theme-dark .hljs-id, +.code-theme-dark .scss .hljs-preprocessor { color: #d7ba7d; font-weight: bold; } -body.code-theme-dark .hljs-class .hljs-title, -body.code-theme-dark .hljs-type, -body.code-theme-dark .vhdl .hljs-literal, -body.code-theme-dark .tex .hljs-command { +.code-theme-dark .hljs-class .hljs-title, +.code-theme-dark .hljs-type, +.code-theme-dark .vhdl .hljs-literal, +.code-theme-dark .tex .hljs-command { color: #4ec9b0; font-weight: bold; } -body.code-theme-dark .hljs-tag, -body.code-theme-dark .hljs-tag .hljs-title, -body.code-theme-dark .hljs-rules .hljs-property, -body.code-theme-dark .django .hljs-tag .hljs-keyword { +.code-theme-dark .hljs-tag, +.code-theme-dark .hljs-tag .hljs-title, +.code-theme-dark .hljs-rules .hljs-property, +.code-theme-dark .django .hljs-tag .hljs-keyword { color: #569cd6; font-weight: normal; } -body.code-theme-dark .hljs-attribute, -body.code-theme-dark .hljs-variable, -body.code-theme-dark .lisp .hljs-body { +.code-theme-dark .hljs-attribute, +.code-theme-dark .hljs-variable, +.code-theme-dark .lisp .hljs-body { color: #9cdcfe; } -body.code-theme-dark .hljs-regexp { +.code-theme-dark .hljs-regexp { color: #d16969; } -body.code-theme-dark .hljs-symbol, -body.code-theme-dark .ruby .hljs-symbol .hljs-string, -body.code-theme-dark .lisp .hljs-keyword, -body.code-theme-dark .clojure .hljs-keyword, -body.code-theme-dark .scheme .hljs-keyword, -body.code-theme-dark .tex .hljs-special, -body.code-theme-dark .hljs-prompt { +.code-theme-dark .hljs-symbol, +.code-theme-dark .ruby .hljs-symbol .hljs-string, +.code-theme-dark .lisp .hljs-keyword, +.code-theme-dark .clojure .hljs-keyword, +.code-theme-dark .scheme .hljs-keyword, +.code-theme-dark .tex .hljs-special, +.code-theme-dark .hljs-prompt { color: #c586c0; } -body.code-theme-dark .hljs-built_in { +.code-theme-dark .hljs-built_in { color: #4fc1ff; } -body.code-theme-dark .hljs-preprocessor, -body.code-theme-dark .hljs-pragma, -body.code-theme-dark .hljs-pi, -body.code-theme-dark .hljs-doctype, -body.code-theme-dark .hljs-shebang, -body.code-theme-dark .hljs-cdata { +.code-theme-dark .hljs-preprocessor, +.code-theme-dark .hljs-pragma, +.code-theme-dark .hljs-pi, +.code-theme-dark .hljs-doctype, +.code-theme-dark .hljs-shebang, +.code-theme-dark .hljs-cdata { color: #808080; font-weight: bold; } -body.code-theme-dark .hljs-deletion { +.code-theme-dark .hljs-deletion { background: #5a1d1d; color: #f48771; } -body.code-theme-dark .hljs-addition { +.code-theme-dark .hljs-addition { background: #1e3a1e; color: #b5cea8; } -body.code-theme-dark .diff .hljs-change { +.code-theme-dark .diff .hljs-change { background: #2d4d2d; color: #4ec9b0; } -body.code-theme-dark .hljs-chunk { +.code-theme-dark .hljs-chunk { color: #808080; } -@media (prefers-color-scheme: dark) { - code { - background-color: var(--code-bg, #2d2d2d); - color: var(--link-color, #4a9eff); - } - - pre { - background-color: var(--code-bg, #2d2d2d); - border-color: var(--border-color, #444); - color: var(--code-text, #e0e0e0); - } - - .hljs { - background: var(--code-bg, #2d2d2d); - color: var(--code-text, #e0e0e0); - } - - .hljs-comment, - .diff .hljs-header, - .hljs-javadoc { - color: #6a9955; - } - - .hljs-keyword, - .css .rule .hljs-keyword, - .hljs-winutils, - .nginx .hljs-title, - .hljs-subst, - .hljs-request, - .hljs-status { - color: #569cd6; - font-weight: bold; - } - - .hljs-number, - .hljs-hexcolor, - .ruby .hljs-constant { - color: #b5cea8; - } - - .hljs-string, - .hljs-tag .hljs-value, - .hljs-phpdoc, - .hljs-dartdoc, - .tex .hljs-formula { - color: #ce9178; - } - - .hljs-title, - .hljs-id, - .scss .hljs-preprocessor { - color: #d7ba7d; - font-weight: bold; - } - - .hljs-class .hljs-title, - .hljs-type, - .vhdl .hljs-literal, - .tex .hljs-command { - color: #4ec9b0; - font-weight: bold; - } - - .hljs-tag, - .hljs-tag .hljs-title, - .hljs-rules .hljs-property, - .django .hljs-tag .hljs-keyword { - color: #569cd6; - font-weight: normal; - } - - .hljs-attribute, - .hljs-variable, - .lisp .hljs-body { - color: #9cdcfe; - } - - .hljs-regexp { - color: #d16969; - } - - .hljs-symbol, - .ruby .hljs-symbol .hljs-string, - .lisp .hljs-keyword, - .clojure .hljs-keyword, - .scheme .hljs-keyword, - .tex .hljs-special, - .hljs-prompt { - color: #c586c0; - } - - .hljs-built_in { - color: #4fc1ff; - } - - .hljs-preprocessor, - .hljs-pragma, - .hljs-pi, - .hljs-doctype, - .hljs-shebang, - .hljs-cdata { - color: #808080; - font-weight: bold; - } - - .hljs-deletion { - background: #5a1d1d; - color: #f48771; - } - - .hljs-addition { - background: #1e3a1e; - color: #b5cea8; - } - - .diff .hljs-change { - background: #2d4d2d; - color: #4ec9b0; - } - - .hljs-chunk { - color: #808080; - } -} - /* Accessibility: Ensure sufficient contrast for code */ code, pre, From 032ac6933d8096b4e11cfe9bdfcf3ff340361f45 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 4 Dec 2025 16:23:06 -0500 Subject: [PATCH 059/133] Refactor theme toggle logic for body class --- docs/js/theme-toggle.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/js/theme-toggle.js b/docs/js/theme-toggle.js index 3fc019d4227..d773edb32ac 100644 --- a/docs/js/theme-toggle.js +++ b/docs/js/theme-toggle.js @@ -22,9 +22,7 @@ } const isDark = theme === 'dark'; document.documentElement.classList.toggle(CODE_THEME_CLASS, isDark); - if (document.body) { - document.body.classList.toggle(CODE_THEME_CLASS, isDark); - } + document.body.classList.toggle(CODE_THEME_CLASS, isDark); } const theme = localStorage.getItem(STORAGE_KEY) || (prefersDarkQuery?.matches ? 'dark' : 'light'); From 68757dbf01762c0623b9a3117b2e0985e6396e10 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 4 Dec 2025 17:15:27 -0500 Subject: [PATCH 060/133] Update docs/css/github.css Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/css/github.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/css/github.css b/docs/css/github.css index f7aa548e0b7..d1d69d6f91e 100644 --- a/docs/css/github.css +++ b/docs/css/github.css @@ -33,7 +33,7 @@ pre { transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease; } -/* Improve code block copy functionality */ +/* Enable absolute positioning for copy button and improve hover effect */ pre { position: relative; } From 3398f0c647d72ca79817ab7ac9df39ac64ab7c6e Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 4 Dec 2025 17:16:05 -0500 Subject: [PATCH 061/133] Update docs/css/mongoose5.css Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/css/mongoose5.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/css/mongoose5.css b/docs/css/mongoose5.css index 8cae0fda911..71f26843bf7 100644 --- a/docs/css/mongoose5.css +++ b/docs/css/mongoose5.css @@ -13,8 +13,8 @@ --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); + --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; From e2464af4fdbeb183108e9b04a72d7707334dc01b Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 4 Dec 2025 17:16:47 -0500 Subject: [PATCH 062/133] Update docs/js/theme-toggle.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/js/theme-toggle.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/js/theme-toggle.js b/docs/js/theme-toggle.js index d773edb32ac..1fb347e0458 100644 --- a/docs/js/theme-toggle.js +++ b/docs/js/theme-toggle.js @@ -18,7 +18,11 @@ function applyTheme(theme, skipSetStorage) { document.documentElement.setAttribute('data-theme', theme); if (!skipSetStorage) { - localStorage.setItem(STORAGE_KEY, theme); + try { + localStorage.setItem(STORAGE_KEY, theme); + } catch (e) { + // Silently fail - theme will still work for current session + } } const isDark = theme === 'dark'; document.documentElement.classList.toggle(CODE_THEME_CLASS, isDark); From 9c1e5481c44305808c3aef9fde6922883f9468f5 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 28 Nov 2025 12:57:28 -0500 Subject: [PATCH 063/133] fix(model): bump version if necessary after successful bulkSave() Fix #15800 --- lib/document.js | 16 +++++++++++ lib/model.js | 13 ++++----- test/model.test.js | 72 +++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 92 insertions(+), 9 deletions(-) diff --git a/lib/document.js b/lib/document.js index 6d02354be5d..afb69b5d65d 100644 --- a/lib/document.js +++ b/lib/document.js @@ -4804,6 +4804,22 @@ Document.prototype.$clone = function() { return clonedDoc; }; +/*! + * 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;// increment version if was successful + this.$__setValue(key, version + 1); + } +}; + /*! * Module exports. */ diff --git a/lib/model.js b/lib/model.js index f649ce33651..597f3c67468 100644 --- a/lib/model.js +++ b/lib/model.js @@ -417,11 +417,9 @@ Model.prototype.$__save = function(options, callback) { const versionBump = this.$__.version; // was this an update that required a version bump? if (versionBump && !this.$__.inserting) { - const doIncrement = VERSION_INC === (VERSION_INC & this.$__.version); - this.$__.version = undefined; - const key = this.$__schema.options.versionKey; - const version = this.$__getValue(key) || 0; if (numAffected <= 0) { + const key = this.$__schema.options.versionKey; + const version = this.$__getValue(key) || 0; // the update failed. pass an error back this.$__undoReset(); const err = this.$__.$versionError || @@ -429,10 +427,7 @@ Model.prototype.$__save = function(options, callback) { return callback(err); } - // increment version if was successful - if (doIncrement) { - this.$__setValue(key, version + 1); - } + this._applyVersionIncrement(); } if (result != null && numAffected <= 0) { this.$__undoReset(); @@ -3663,6 +3658,8 @@ function handleSuccessfulWrite(document) { } document.$__reset(); + document._applyVersionIncrement(); + document.schema.s.hooks.execPost('save', document, [document], {}, (err) => { if (err) { reject(err); diff --git a/test/model.test.js b/test/model.test.js index d660960a154..62923512678 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -6522,7 +6522,7 @@ describe('Model', function() { }); describe('bulkSave() (gh-9673)', function() { - it('saves new documents', async function() { + it('saves new documents', async function () { const userSchema = new Schema({ name: { type: String } @@ -6545,7 +6545,77 @@ describe('Model', function() { 'Hafez2_gh-9673-1' ] ); + }); + + 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 } + }); + const User = db.model('User', userSchema); + + + await User.bulkSave([ + new User({ name: 'Hafez1_gh-9673-1' }), + new User({ name: 'Hafez2_gh-9673-1' }) + ], { ordered: false }); + + const users = await User.find().sort('name'); + + assert.deepEqual( + users.map(user => user.name), + [ + 'Hafez1_gh-9673-1', + 'Hafez2_gh-9673-1' + ] + ); }); it('updates documents', async function() { From f6d48dead2ca180bd7a67c5f409857af51a1512b Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 1 Dec 2025 17:37:36 -0500 Subject: [PATCH 064/133] Update lib/document.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- lib/document.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/document.js b/lib/document.js index afb69b5d65d..d765ced3bbf 100644 --- a/lib/document.js +++ b/lib/document.js @@ -4815,8 +4815,8 @@ Document.prototype._applyVersionIncrement = function _applyVersionIncrement() { this.$__.version = undefined; if (doIncrement) { const key = this.$__schema.options.versionKey; - const version = this.$__getValue(key) || 0;// increment version if was successful - this.$__setValue(key, version + 1); + const version = this.$__getValue(key) || 0; + this.$__setValue(key, version + 1); // increment version if was successful } }; From 54b489be80a7f1343091d4617d65980f064f13f2 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 4 Dec 2025 20:37:59 -0500 Subject: [PATCH 065/133] fix backport of #15809 --- lib/document.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/document.js b/lib/document.js index d765ced3bbf..c9f2839a50b 100644 --- a/lib/document.js +++ b/lib/document.js @@ -61,6 +61,8 @@ let Embedded; const specialProperties = utils.specialProperties; +const VERSION_INC = 2; + /** * The core Mongoose document constructor. You should not call this directly, * the Mongoose [Model constructor](./api/model.html#Model) calls this for you. From 40da9765e8aef544c754952065082b564f19fd32 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 4 Dec 2025 20:41:08 -0500 Subject: [PATCH 066/133] style: fix lint --- test/model.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/model.test.js b/test/model.test.js index 62923512678..ee06efc1eb6 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -6522,7 +6522,7 @@ describe('Model', function() { }); describe('bulkSave() (gh-9673)', function() { - it('saves new documents', async function () { + it('saves new documents', async function() { const userSchema = new Schema({ name: { type: String } From a1ef665c7bb9c9573fb0b8461296f74d6e93364d Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 4 Dec 2025 20:43:19 -0500 Subject: [PATCH 067/133] chore: release 7.8.8 --- CHANGELOG.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43d09460f75..0e22303dcaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +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 + 7.8.7 / 2025-04-30 ================== * types(aggregate): allow calling project() with a string #15304 #15300 diff --git a/package.json b/package.json index f1ca3b68dd1..f3a02930962 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "7.8.7", + "version": "7.8.8", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From 446a6b8db3d3cfcbd05984e426530f41502dd2f8 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 4 Dec 2025 21:04:08 -0500 Subject: [PATCH 068/133] clean up some merge conflict residuals --- test/types/models.test.ts | 9 +++- types/models.d.ts | 89 +-------------------------------------- 2 files changed, 8 insertions(+), 90 deletions(-) diff --git a/test/types/models.test.ts b/test/types/models.test.ts index a58d9257c22..84fcbced256 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -12,11 +12,16 @@ import mongoose, { Schema, Types, UpdateQuery, - UpdateWriteOpResult + UpdateWriteOpResult, + connection, + model, + UpdateOneModel, + WithLevel1NestedPaths, + UpdateManyModel } from 'mongoose'; import { expectAssignable, expectError, expectType } from 'tsd'; import { AutoTypedSchemaType, autoTypedSchema } from './schema.test'; -import { UpdateOneModel, ChangeStreamInsertDocument, ObjectId } from 'mongodb'; +import { UpdateOneModel as MongoUpdateOneModel, ChangeStreamInsertDocument, ObjectId, ModifyResult } from 'mongodb'; function rawDocSyntax(): void { interface ITest { diff --git a/types/models.d.ts b/types/models.d.ts index 25463822d14..8af11542a63 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -214,93 +214,6 @@ declare module 'mongoose' { const Model: Model; - export type AnyBulkWriteOperation = { - insertOne: InsertOneModel; - } | { - replaceOne: ReplaceOneModel; - } | { - updateOne: UpdateOneModel; - } | { - updateMany: UpdateManyModel; - } | { - deleteOne: DeleteOneModel; - } | { - deleteMany: DeleteManyModel; - }; - - export interface InsertOneModel { - document: mongodb.OptionalId; - /** 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: RootFilterQuery; - /** 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; - } - - export interface UpdateOneModel { - /** The filter to limit the updated documents. */ - filter: RootFilterQuery; - /** 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 interface UpdateManyModel { - /** The filter to limit the updated documents. */ - filter: RootFilterQuery; - /** 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 interface DeleteOneModel { - /** The filter to limit the deleted documents. */ - filter: RootFilterQuery; - /** 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 interface DeleteManyModel { - /** The filter to limit the deleted documents. */ - filter: RootFilterQuery; - /** 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; - } - type HasLeanOption = 'lean' extends keyof ObtainSchemaGeneric ? ObtainSchemaGeneric['lean'] extends Record ? true : @@ -364,7 +277,7 @@ declare module 'mongoose' { ): Promise; bulkWrite( writes: Array>, - options?: MongooseBulkWriteOptions + options?: mongodb.BulkWriteOptions & MongooseBulkWriteOptions ): Promise; /** From e027e4fd02bcd91da3e35ada56e6d0461bb931c5 Mon Sep 17 00:00:00 2001 From: Hafez Date: Fri, 5 Dec 2025 03:54:16 +0100 Subject: [PATCH 069/133] refactor: use Object.hasOwn instead of Object#hasOwnProperty --- lib/aggregate.js | 2 +- lib/cast.js | 2 +- lib/connection.js | 18 ++++---- lib/document.js | 39 +++++++++-------- lib/drivers/node-mongodb-native/connection.js | 4 +- lib/helpers/common.js | 2 +- lib/helpers/indexes/applySchemaCollation.js | 2 +- lib/helpers/indexes/isDefaultIdIndex.js | 2 +- lib/helpers/model/applyMethods.js | 2 +- lib/helpers/model/castBulkWrite.js | 2 +- .../populate/getModelsMapForPopulate.js | 6 +-- lib/helpers/populate/modelNamesFromRefPath.js | 2 +- .../populate/removeDeselectedForeignField.js | 2 +- lib/helpers/projection/applyProjection.js | 4 +- .../query/getEmbeddedDiscriminatorPath.js | 2 +- lib/helpers/setDefaultsOnInsert.js | 4 +- lib/helpers/timestamps/setupTimestamps.js | 2 +- lib/helpers/update/applyTimestampsToUpdate.js | 2 +- .../update/decorateUpdateWithVersionKey.js | 2 +- lib/model.js | 14 +++---- lib/mongoose.js | 7 ++-- lib/query.js | 6 +-- lib/schema.js | 42 +++++++++---------- lib/schema/array.js | 2 +- lib/schemaType.js | 16 +++---- lib/types/array/index.js | 10 ++--- lib/types/documentArray/index.js | 12 +++--- lib/types/objectid.js | 2 +- lib/utils.js | 16 +------ lib/virtualType.js | 2 +- 30 files changed, 110 insertions(+), 120 deletions(-) diff --git a/lib/aggregate.js b/lib/aggregate.js index e475736da2e..55bddc7ca79 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 6f75d9bfe37..74ee2d516e2 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/connection.js b/lib/connection.js index 1b1d6bdff04..7fbf3dd7a02 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 e9b1e63e90b..042a9a55803 100644 --- a/lib/document.js +++ b/lib/document.js @@ -780,7 +780,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) { @@ -1156,7 +1156,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); @@ -1206,8 +1206,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; @@ -1512,7 +1512,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); @@ -1994,7 +1994,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; @@ -2038,7 +2038,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)); } } @@ -2349,7 +2349,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; @@ -2357,7 +2357,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)); }; /** @@ -2411,7 +2411,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; } @@ -2450,7 +2450,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; @@ -2458,7 +2458,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)); }; /** @@ -2596,7 +2596,7 @@ Document.prototype.isDirectSelected = function isDirectSelected(path) { return true; } - if (this.$__.selected.hasOwnProperty(path)) { + if (Object.hasOwn(this.$__.selected, path)) { return inclusive; } @@ -2772,7 +2772,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); @@ -2843,7 +2843,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); + } + } } } @@ -4184,7 +4189,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; } @@ -5097,7 +5102,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 || diff --git a/lib/drivers/node-mongodb-native/connection.js b/lib/drivers/node-mongodb-native/connection.js index e45d22b0a08..c6108473df4 100644 --- a/lib/drivers/node-mongodb-native/connection.js +++ b/lib/drivers/node-mongodb-native/connection.js @@ -108,7 +108,7 @@ NativeConnection.prototype.useDb = function(name, options) { function wireup() { newConn.client = _this.client; const _opts = {}; - if (options.hasOwnProperty('noListener')) { + if (Object.hasOwn(options, 'noListener')) { _opts.noListener = options.noListener; } newConn.db = _this.client.db(name, _opts); @@ -515,7 +515,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/common.js b/lib/helpers/common.js index 5a1bee1c313..6b3d878f252 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 e864bb1f12a..f0c0ffb1431 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 9dfe292a4a5..b6cdec2a517 100644 --- a/lib/helpers/model/castBulkWrite.js +++ b/lib/helpers/model/castBulkWrite.js @@ -295,7 +295,7 @@ 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; diff --git a/lib/helpers/populate/getModelsMapForPopulate.js b/lib/helpers/populate/getModelsMapForPopulate.js index f90bd0e8f33..bad3637b3f5 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 60bad97f816..0a08bd57653 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 f6ba12b98b6..ba6f041563e 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..240dbe07087 100644 --- a/lib/helpers/update/applyTimestampsToUpdate.js +++ b/lib/helpers/update/applyTimestampsToUpdate.js @@ -74,7 +74,7 @@ function applyTimestampsToUpdate(now, createdAt, updatedAt, currentUpdate, optio updates.$set[updatedAt] = now; } - if (updates.hasOwnProperty(updatedAt)) { + if (Object.hasOwn(updates, updatedAt)) { delete updates[updatedAt]; } } 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 e7247344a55..189c8f808b9 100644 --- a/lib/model.js +++ b/lib/model.js @@ -351,7 +351,7 @@ Model.prototype.$__handleSave = function(options, callback) { const asyncLocalStorage = this[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; } @@ -615,7 +615,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) { @@ -780,7 +780,7 @@ Model.prototype.deleteOne = function deleteOne(options) { options = {}; } - if (options.hasOwnProperty('session')) { + if (Object.hasOwn(options, 'session')) { this.$session(options.session); } @@ -3028,7 +3028,7 @@ Model.$__insertMany = function(arr, options, callback) { 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 }; } @@ -3424,7 +3424,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 }; } @@ -5184,10 +5184,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 fa57f202cef..b348361fe0c 100644 --- a/lib/mongoose.js +++ b/lib/mongoose.js @@ -600,12 +600,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 695b947b0f9..5ec664340e7 100644 --- a/lib/query.js +++ b/lib/query.js @@ -2094,7 +2094,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; } @@ -4590,7 +4590,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()'); } @@ -5039,7 +5039,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 47e0412db26..ffb8535ae75 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -1441,17 +1441,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; @@ -1639,20 +1639,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; @@ -1689,7 +1689,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.'); @@ -1850,24 +1850,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'; } @@ -1897,7 +1897,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]; } @@ -1926,7 +1926,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'; } @@ -2633,7 +2633,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; }; /** @@ -2781,8 +2781,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; } @@ -2795,7 +2795,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); @@ -3021,7 +3021,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 2edf2f20dc7..4434cf3a792 100644 --- a/lib/schema/array.js +++ b/lib/schema/array.js @@ -81,7 +81,7 @@ function SchemaArray(key, cast, options, schemaOptions, parentSchema) { : utils.getFunctionName(cast); const Types = require('./index.js'); - const caster = Types.hasOwnProperty(name) ? Types[name] : cast; + const caster = Object.hasOwn(Types, name) ? Types[name] : cast; this.casterConstructor = caster; diff --git a/lib/schemaType.js b/lib/schemaType.js index f81e0816225..81f5baaac4b 100644 --- a/lib/schemaType.js +++ b/lib/schemaType.js @@ -47,10 +47,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() : []; @@ -63,7 +63,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]; } } @@ -340,7 +340,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; @@ -363,7 +363,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); }; @@ -504,7 +504,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; } @@ -543,7 +543,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; } @@ -580,7 +580,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 c08dbe6b9c3..1c7fb7b514d 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/documentArray/index.js b/lib/types/documentArray/index.js index f43522659c4..a43890d1d7c 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 d38c223659b..c66dc40a1a1 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/utils.js b/lib/utils.js index e0cc40fc94c..7d1af07dae2 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -708,18 +708,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 @@ -770,8 +758,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; @@ -786,7 +772,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) { 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]; } From 04e4c2e4f6b50abdeec16f0185138faee5851de5 Mon Sep 17 00:00:00 2001 From: Hafez Date: Fri, 5 Dec 2025 04:02:45 +0100 Subject: [PATCH 070/133] chore: fix lint --- docs/js/theme-toggle.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/js/theme-toggle.js b/docs/js/theme-toggle.js index 1fb347e0458..6671d792053 100644 --- a/docs/js/theme-toggle.js +++ b/docs/js/theme-toggle.js @@ -1,6 +1,5 @@ +'use strict'; (function() { - 'use strict'; - const STORAGE_KEY = 'mongoose-theme'; const CODE_THEME_CLASS = 'code-theme-dark'; const supportsMatchMedia = typeof window !== 'undefined' && typeof window.matchMedia === 'function'; @@ -20,7 +19,8 @@ if (!skipSetStorage) { try { localStorage.setItem(STORAGE_KEY, theme); - } catch (e) { + // eslint-disable-next-line no-unused-vars + } catch (err) { // Silently fail - theme will still work for current session } } From c1c0fc6f004a4df8f276f5b1234f8a35f3369709 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 5 Dec 2025 10:08:38 -0500 Subject: [PATCH 071/133] types merge conflict cleanup --- types/models.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/types/models.d.ts b/types/models.d.ts index 8af11542a63..f77afd48289 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -272,11 +272,11 @@ declare module 'mongoose' { * round trip to the MongoDB server. */ bulkWrite( - writes: Array>, + writes: Array>, options: mongodb.BulkWriteOptions & MongooseBulkWriteOptions & { ordered: false } ): Promise; bulkWrite( - writes: Array>, + writes: Array>, options?: mongodb.BulkWriteOptions & MongooseBulkWriteOptions ): Promise; From e97a0da8dbeecda19c472faf51d6574b4e3e2a7b Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 5 Dec 2025 11:03:32 -0500 Subject: [PATCH 072/133] fix overwriteImmutable option lost in merge conflict --- lib/helpers/model/castBulkWrite.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/helpers/model/castBulkWrite.js b/lib/helpers/model/castBulkWrite.js index eb493409d0a..be76544f13e 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) { From cb13354463b1c5602d058206e2e8e9bd8c00970a Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 5 Dec 2025 11:10:25 -0500 Subject: [PATCH 073/133] fix lint --- test/model.updateOne.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/model.updateOne.test.js b/test/model.updateOne.test.js index 654ba1406d6..61ecc6e00cf 100644 --- a/test/model.updateOne.test.js +++ b/test/model.updateOne.test.js @@ -2682,7 +2682,7 @@ describe('model: updateOne: ', function() { assert.equal(doc.age, 20); }); - it('overwriting immutable createdAt with bulkWrite (gh-15781)', async function () { + it('overwriting immutable createdAt with bulkWrite (gh-15781)', async function() { const start = new Date().valueOf(); const schema = Schema({ createdAt: { From 1164312473001cb87ab4144fcfaed10c49b7e7b7 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 5 Dec 2025 11:15:34 -0500 Subject: [PATCH 074/133] chore: publish with tag 8x from 8.x branch --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5a48c67920a..022fe63ab2f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -28,6 +28,6 @@ jobs: run: npm run build-browser - name: Dry run publish with provenance - run: npm publish --provenance --access public + run: npm publish --provenance --access public --tag 8x env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} From 8d0f22f0a8c9cd80ee2e3ccf056bffba804ef7a4 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 5 Dec 2025 11:20:28 -0500 Subject: [PATCH 075/133] chore: release 8.20.2 --- CHANGELOG.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b3a7642105..8567e22f6ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +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 diff --git a/package.json b/package.json index 42543cfb46d..eb0f7f8723b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "8.20.1", + "version": "8.20.2", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From 027581563b96c592809fa79209ec70e26e2e1361 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 5 Dec 2025 11:26:30 -0500 Subject: [PATCH 076/133] correct publish step name --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 022fe63ab2f..8d6f920a089 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -27,7 +27,7 @@ jobs: - name: browser build run: npm run build-browser - - name: Dry run publish with provenance + - name: Publish with provenance run: npm publish --provenance --access public --tag 8x env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} From 1b72648edcb0e6e6422ba2402e1887df9c60caf1 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 5 Dec 2025 12:31:30 -0500 Subject: [PATCH 077/133] docs(migrating_to_9): clarify removing `next()` from pre middleware Fix #15813 --- docs/migrating_to_9.md | 56 +++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 31 deletions(-) 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. From adb3a5e0ebc76e5da87adfc4855cc25df70f91bc Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 5 Dec 2025 12:43:08 -0500 Subject: [PATCH 078/133] chore: release 9.0.1 --- CHANGELOG.md | 15 +++++++++++++++ package.json | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5d2996ccfc..0cba5c53e11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +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 diff --git a/package.json b/package.json index f0e9313e92d..8b828716d9d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "9.0.0", + "version": "9.0.1", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From ba9c717947d39d911f53865551316d1b4f3bfe69 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 5 Dec 2025 14:05:35 -0500 Subject: [PATCH 079/133] move dark mode toggle on home page to top right --- docs/css/style.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/css/style.css b/docs/css/style.css index ae06f764eaa..7a9ca464475 100644 --- a/docs/css/style.css +++ b/docs/css/style.css @@ -31,8 +31,9 @@ body { /* Theme Toggle Button for Homepage */ #theme-toggle { position: fixed; - bottom: 20px; + top: 20px; right: 20px; + bottom: auto; z-index: 1000; background-color: rgba(255, 255, 255, 0.9); border: 1px solid #ddd; From 0de0ac03180d4055e50fc8aa3cd2ede977e9842f Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 5 Dec 2025 14:06:25 -0500 Subject: [PATCH 080/133] docs(version-support): update latest version numbers --- docs/version-support.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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) From b76ee2dc700de3dfeec4056ee44a9bb29dde4100 Mon Sep 17 00:00:00 2001 From: Hafez Date: Mon, 8 Dec 2025 02:52:58 +0100 Subject: [PATCH 081/133] feat: improve colors on dark mode --- docs/css/mongoose5.css | 134 +++++++++++++++--- docs/css/style.css | 34 +++++ docs/enterprise.md | 8 +- .../mongoose5_62x30_transparent_light.png | Bin 0 -> 1571 bytes 4 files changed, 150 insertions(+), 26 deletions(-) create mode 100644 docs/images/mongoose5_62x30_transparent_light.png diff --git a/docs/css/mongoose5.css b/docs/css/mongoose5.css index 71f26843bf7..771c31533f8 100644 --- a/docs/css/mongoose5.css +++ b/docs/css/mongoose5.css @@ -26,19 +26,19 @@ --bg-primary: #1a1a1a; --bg-secondary: #2d2d2d; --bg-tertiary: #252525; - --text-primary: #e0e0e0; - --text-secondary: #c0c0c0; - --text-muted: #888888; - --link-color: #4a9eff; - --link-hover: #6bb3ff; + --text-primary: #f0f0f0; + --text-secondary: #d8d8d8; + --text-muted: #a0a0a0; + --link-color: #5aabff; + --link-hover: #7dbfff; --border-color: #444444; --code-bg: #2d2d2d; - --code-text: #e0e0e0; + --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: #4a9eff; + --focus-ring: #5aabff; } } @@ -47,19 +47,19 @@ --bg-primary: #1a1a1a; --bg-secondary: #2d2d2d; --bg-tertiary: #252525; - --text-primary: #e0e0e0; - --text-secondary: #c0c0c0; - --text-muted: #888888; - --link-color: #4a9eff; - --link-hover: #6bb3ff; + --text-primary: #f0f0f0; + --text-secondary: #d8d8d8; + --text-muted: #a0a0a0; + --link-color: #5aabff; + --link-hover: #7dbfff; --border-color: #444444; --code-bg: #2d2d2d; - --code-text: #e0e0e0; + --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: #4a9eff; + --focus-ring: #5aabff; } /* Light mode override */ @@ -179,6 +179,24 @@ h4 a:focus-visible { text-transform: none; } +/* Dark mode logo adjustments for better contrast */ +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) .logo-text { + color: #ff6b6b; + } + :root:not([data-theme="light"]) #logo { + content: url('/docs/images/mongoose5_62x30_transparent_light.png'); + } +} + +[data-theme="dark"] .logo-text { + color: #ff6b6b; +} + +[data-theme="dark"] #logo { + content: url('/docs/images/mongoose5_62x30_transparent_light.png'); +} + .pure-menu-item { font-size: 12pt; padding-top: 0px; @@ -187,6 +205,11 @@ h4 a:focus-visible { .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 */ @@ -222,6 +245,7 @@ li.version { li.version ul.pure-menu-children { border: 1px solid var(--border-color); + background-color: var(--menu-bg); } #logo-container { @@ -539,8 +563,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 { @@ -553,11 +577,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 { @@ -565,7 +589,7 @@ li.version ul.pure-menu-children { } #jobs .location { - color: black; + color: var(--text-secondary); margin-top: 0.25em; font-size: 0.75em; } @@ -580,14 +604,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) { @@ -756,3 +780,69 @@ li.version ul.pure-menu-children { 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 #800; + color: #800; + background-color: transparent; +} + +.mongoose-btn-outline:hover { + background-color: rgba(136, 0, 0, 0.1); +} + +.mongoose-btn-solid { + border: 1px solid transparent; + color: white; + background-color: #800; +} + +.mongoose-btn-solid:hover { + background-color: #990000; +} + +/* Dark mode button colors */ +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) .mongoose-btn-outline { + border-color: #ff6b6b; + color: #ff6b6b; + } + :root:not([data-theme="light"]) .mongoose-btn-outline:hover { + background-color: rgba(255, 107, 107, 0.1); + } + :root:not([data-theme="light"]) .mongoose-btn-solid { + background-color: #ff6b6b; + color: #1a1a1a; + } + :root:not([data-theme="light"]) .mongoose-btn-solid:hover { + background-color: #ff8585; + } +} + +[data-theme="dark"] .mongoose-btn-outline { + border-color: #ff6b6b; + color: #ff6b6b; +} + +[data-theme="dark"] .mongoose-btn-outline:hover { + background-color: rgba(255, 107, 107, 0.1); +} + +[data-theme="dark"] .mongoose-btn-solid { + background-color: #ff6b6b; + color: #1a1a1a; +} + +[data-theme="dark"] .mongoose-btn-solid:hover { + background-color: #ff8585; +} diff --git a/docs/css/style.css b/docs/css/style.css index 7a9ca464475..05a3c7d458c 100644 --- a/docs/css/style.css +++ b/docs/css/style.css @@ -221,6 +221,14 @@ h2 a { text-indent: -23px; color: #800; } +[data-theme="dark"] #header .mongoose { + color: #ff6b6b; +} +@media (prefers-color-scheme: dark) { + body:not([data-theme="light"]) #header .mongoose { + color: #ff6b6b; + } +} .load #header .mongoose { letter-spacing: -14px; } @@ -245,6 +253,16 @@ h2 a { text-decoration: none; color: #800; } +[data-theme="dark"] .tagline a, +[data-theme="dark"] .blurb a { + color: #ff6b6b; +} +@media (prefers-color-scheme: dark) { + body:not([data-theme="light"]) .tagline a, + body:not([data-theme="light"]) .blurb a { + color: #ff6b6b; + } +} #links { margin: 50px 10px 20px; text-align: center; @@ -288,6 +306,22 @@ h2 a { position: relative; top: -4px; } +/* GitHub buttons iframe - use color-scheme to make iframe bg transparent in dark mode */ +.github-btn { + background: transparent; +} +[data-theme="dark"] .github-btn { + color-scheme: light; + filter: invert(0.85) hue-rotate(180deg); + background: transparent; +} +@media (prefers-color-scheme: dark) { + body:not([data-theme="light"]) .github-btn { + color-scheme: light; + filter: invert(0.85) hue-rotate(180deg); + background: transparent; + } +} #what { margin-bottom: 32px; } 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/mongoose5_62x30_transparent_light.png b/docs/images/mongoose5_62x30_transparent_light.png new file mode 100644 index 0000000000000000000000000000000000000000..6aee1b65c55ea6b646bba9852dbaac5b2b80fd69 GIT binary patch literal 1571 zcmeAS@N?(olHy`uVBq!ia0vp^c0er0!3-pwH^({vDTQQ@AYTTCDm4a%h86~fUqGRT z7Yq!g1`G_Z5*Qe)W-u^_7tGleXv4t3_&dNS#C87s`Ts}J2n>P$|L=clu>huzo&~DGDy z`0>d_Ci5MOn)lkW*KO`Pz-{>S{grQbXF15Kg-p79>q1dOZOieS%gb$ea$R*#Z|`7~ zFEQX}@etcu`sfOCAnJd0zidwgL_;9m3QgP8Ich) zA_dii)=uu7+dR2@cKh`H`QZ(*0Ui-9A~z#y9DHJ&g1X9oetLSzXj9Rr(~b^~fu51B zp}w;F*RGM8G%G6M>n*EYRlnFA+8!3AEJ#YpS=4t_RH1+7tW47#T!F9^PTL zZC6>qvc1iVH(Et`Fx>Pzr{{5P$1dIS^n?!|zBFZpU;FayK_pI>^O&Wy~o*m_!j``NY^=RO`q0DkKN3T9uvU zSY`C(%NM3s4Qqa}?LD?EZAaa0v3n-6t_sd8P4}_o5T9CFw;D{&s9$MlXG#XwgcEsbZ< zTW)l7o`~2qWzMHdrwcar$_f{zN}Ihsu%RtW{WweX;Tbb7tb7v5%d*e->z8T13QM>c zri(_V?JWHE0GO^+OI#yLQW8s2t&)pUffR$0fswJUftjw6Nr-`gm4UgHv4J*-VQ?gk zp&CU)ZhlH;S|x4`Tdj8-2WrrO+fb63n_66wm|FnSW2$Rtq-$UiVqj=xY-nX@46!6M TGX Date: Mon, 8 Dec 2025 03:15:29 +0100 Subject: [PATCH 082/133] chore: use higher contrast color for dark mode --- docs/css/mongoose5.css | 20 +++++++++--------- docs/css/style.css | 8 +++---- .../mongoose5_62x30_transparent_light.png | Bin 1571 -> 1526 bytes 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/css/mongoose5.css b/docs/css/mongoose5.css index 771c31533f8..74df2197c34 100644 --- a/docs/css/mongoose5.css +++ b/docs/css/mongoose5.css @@ -182,7 +182,7 @@ h4 a:focus-visible { /* Dark mode logo adjustments for better contrast */ @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) .logo-text { - color: #ff6b6b; + color: #FFB4B4; } :root:not([data-theme="light"]) #logo { content: url('/docs/images/mongoose5_62x30_transparent_light.png'); @@ -190,7 +190,7 @@ h4 a:focus-visible { } [data-theme="dark"] .logo-text { - color: #ff6b6b; + color: #FFB4B4; } [data-theme="dark"] #logo { @@ -814,24 +814,24 @@ li.version ul.pure-menu-children { /* Dark mode button colors */ @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) .mongoose-btn-outline { - border-color: #ff6b6b; - color: #ff6b6b; + border-color: #FFB4B4; + color: #FFB4B4; } :root:not([data-theme="light"]) .mongoose-btn-outline:hover { background-color: rgba(255, 107, 107, 0.1); } :root:not([data-theme="light"]) .mongoose-btn-solid { - background-color: #ff6b6b; + background-color: #FFB4B4; color: #1a1a1a; } :root:not([data-theme="light"]) .mongoose-btn-solid:hover { - background-color: #ff8585; + background-color: #FFCECE; } } [data-theme="dark"] .mongoose-btn-outline { - border-color: #ff6b6b; - color: #ff6b6b; + border-color: #FFB4B4; + color: #FFB4B4; } [data-theme="dark"] .mongoose-btn-outline:hover { @@ -839,10 +839,10 @@ li.version ul.pure-menu-children { } [data-theme="dark"] .mongoose-btn-solid { - background-color: #ff6b6b; + background-color: #FFB4B4; color: #1a1a1a; } [data-theme="dark"] .mongoose-btn-solid:hover { - background-color: #ff8585; + background-color: #FFCECE; } diff --git a/docs/css/style.css b/docs/css/style.css index 05a3c7d458c..3a18cd61525 100644 --- a/docs/css/style.css +++ b/docs/css/style.css @@ -222,11 +222,11 @@ h2 a { color: #800; } [data-theme="dark"] #header .mongoose { - color: #ff6b6b; + color: #FFB4B4; } @media (prefers-color-scheme: dark) { body:not([data-theme="light"]) #header .mongoose { - color: #ff6b6b; + color: #FFB4B4; } } .load #header .mongoose { @@ -255,12 +255,12 @@ h2 a { } [data-theme="dark"] .tagline a, [data-theme="dark"] .blurb a { - color: #ff6b6b; + color: #FFB4B4; } @media (prefers-color-scheme: dark) { body:not([data-theme="light"]) .tagline a, body:not([data-theme="light"]) .blurb a { - color: #ff6b6b; + color: #FFB4B4; } } #links { diff --git a/docs/images/mongoose5_62x30_transparent_light.png b/docs/images/mongoose5_62x30_transparent_light.png index 6aee1b65c55ea6b646bba9852dbaac5b2b80fd69..1028fcf23a7783fab1eda3516847a518bd99d059 100644 GIT binary patch literal 1526 zcmeAS@N?(olHy`uVBq!ia0vp^c0er0!3-pwH^({vDTQQ@AYTTCDm4a%h86~fUqGRT z7Yq!g1`G_Z5*Qe)W-u^_7tGleXv4t3_#?n4#P$D{Eu(0}g~0#+6O|5B0Taivk|4ie z23BDS&bW0OW@H(q9zAzz_w4!@6D|#>0AH)0pP#RP_~-klch{%-afr@;wC3>D$~!;) zwi*j6$8OwoEoj%jS8tvzPSlxp<9ufX^TfBNJ_QT+l=98{@c5-%&DXi=_kJ%8k5c^j zJ6dAqwTKd@aJlhU>q#z!Y5BPBKY`GY5q zo;`f}_+h%RfQXQoV%YK(Gj_D7oZpmsddZU1l&8}g8}>9UnzX5Fl~i4?&MP6QSqWdG zZdvWB`sL7e&?sd=PDV=4X&&KV-P##e#>L!hY~I=u*KfHU7dy3ObGgBxq^8A_jVGDv ztvh{3LgJ3C&B9w#P4?`mttnlg_T#7V;w(N6o=t9r%bW{5G@4T*EMgRzv=3MWnQ|TK z{MO@G)EKqqk&(#)ne9F8ns$kjdJ{4xPT{#acT$(Doo;QUTbh>N+oIDsn-q3!w%w?8 z;-u+?(}!2yt(vJAQEb<5$|rj2rsC-WX4Vb2xw%BUau#=)+6sFq9CK-An31~S%)GrE zp3WtmbqhtNNXDIF?R#zX{}Y#h^3F}GcCjr{j|~sMzF+Lbs@2zXGjf_I-DLdM&sKjXt^Wj-cZ_U^)UQ|?Rmyr2!=EBw3 zTmBxR3~4c8sf}qY^SQ(gG85?MW z7zRhu7*I9j=BH$)RpQpL)q2NqpbZ*u8%i>BQ;SOya|=LvOmz*7bPX&*42-M{jIE3; WA(q5@Kl2CbVeoYIb6Mw<&;$V6wJFsA literal 1571 zcmeAS@N?(olHy`uVBq!ia0vp^c0er0!3-pwH^({vDTQQ@AYTTCDm4a%h86~fUqGRT z7Yq!g1`G_Z5*Qe)W-u^_7tGleXv4t3_&dNS#C87s`Ts}J2n>P$|L=clu>huzo&~DGDy z`0>d_Ci5MOn)lkW*KO`Pz-{>S{grQbXF15Kg-p79>q1dOZOieS%gb$ea$R*#Z|`7~ zFEQX}@etcu`sfOCAnJd0zidwgL_;9m3QgP8Ich) zA_dii)=uu7+dR2@cKh`H`QZ(*0Ui-9A~z#y9DHJ&g1X9oetLSzXj9Rr(~b^~fu51B zp}w;F*RGM8G%G6M>n*EYRlnFA+8!3AEJ#YpS=4t_RH1+7tW47#T!F9^PTL zZC6>qvc1iVH(Et`Fx>Pzr{{5P$1dIS^n?!|zBFZpU;FayK_pI>^O&Wy~o*m_!j``NY^=RO`q0DkKN3T9uvU zSY`C(%NM3s4Qqa}?LD?EZAaa0v3n-6t_sd8P4}_o5T9CFw;D{&s9$MlXG#XwgcEsbZ< zTW)l7o`~2qWzMHdrwcar$_f{zN}Ihsu%RtW{WweX;Tbb7tb7v5%d*e->z8T13QM>c zri(_V?JWHE0GO^+OI#yLQW8s2t&)pUffR$0fswJUftjw6Nr-`gm4UgHv4J*-VQ?gk zp&CU)ZhlH;S|x4`Tdj8-2WrrO+fb63n_66wm|FnSW2$Rtq-$UiVqj=xY-nX@46!6M TGX Date: Mon, 8 Dec 2025 03:24:16 +0100 Subject: [PATCH 083/133] refactor: parameterize colors instead of using hard-coded values --- docs/css/mongoose5.css | 95 +++++++++++++++++++----------------------- docs/css/style.css | 71 +++++++------------------------ 2 files changed, 60 insertions(+), 106 deletions(-) diff --git a/docs/css/mongoose5.css b/docs/css/mongoose5.css index 74df2197c34..c863217c869 100644 --- a/docs/css/mongoose5.css +++ b/docs/css/mongoose5.css @@ -18,6 +18,15 @@ --shadow: rgba(0, 0, 0, 0.1); --focus-ring: #0971B2; --focus-ring-width: 3px; + /* 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 colors */ @@ -39,6 +48,15 @@ --menu-selected: rgba(255,255,255, 0.15); --shadow: rgba(0, 0, 0, 0.3); --focus-ring: #5aabff; + /* Brand colors */ + --brand-primary: #FFB4B4; + --brand-primary-hover: #FFCECE; + --brand-primary-subtle: rgba(255, 107, 107, 0.1); + /* Button colors */ + --btn-bg: #444; + --btn-text-shadow: #222; + /* Tagline text shadow */ + --tagline-text-shadow: #222; } } @@ -60,6 +78,15 @@ --menu-selected: rgba(255,255,255, 0.15); --shadow: rgba(0, 0, 0, 0.3); --focus-ring: #5aabff; + /* Brand colors */ + --brand-primary: #FFB4B4; + --brand-primary-hover: #FFCECE; + --brand-primary-subtle: rgba(255, 107, 107, 0.1); + /* Button colors */ + --btn-bg: #444; + --btn-text-shadow: #222; + /* Tagline text shadow */ + --tagline-text-shadow: #222; } /* Light mode override */ @@ -80,6 +107,15 @@ --menu-selected: rgba(0,0,0, 0.15); --shadow: rgba(0, 0, 0, 0.1); --focus-ring: #0971B2; + /* 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; } html { @@ -171,7 +207,7 @@ h4 a:focus-visible { } .logo-text { - color: #800; + color: var(--brand-primary); font-size: 20pt; position: relative; top: 0px; @@ -179,20 +215,13 @@ h4 a:focus-visible { text-transform: none; } -/* Dark mode logo adjustments for better contrast */ +/* Dark mode logo image swap */ @media (prefers-color-scheme: dark) { - :root:not([data-theme="light"]) .logo-text { - color: #FFB4B4; - } :root:not([data-theme="light"]) #logo { content: url('/docs/images/mongoose5_62x30_transparent_light.png'); } } -[data-theme="dark"] .logo-text { - color: #FFB4B4; -} - [data-theme="dark"] #logo { content: url('/docs/images/mongoose5_62x30_transparent_light.png'); } @@ -792,57 +821,21 @@ li.version ul.pure-menu-children { } .mongoose-btn-outline { - border: 1px solid #800; - color: #800; + border: 1px solid var(--brand-primary); + color: var(--brand-primary); background-color: transparent; } .mongoose-btn-outline:hover { - background-color: rgba(136, 0, 0, 0.1); + background-color: var(--brand-primary-subtle); } .mongoose-btn-solid { border: 1px solid transparent; - color: white; - background-color: #800; + color: var(--bg-primary); + background-color: var(--brand-primary); } .mongoose-btn-solid:hover { - background-color: #990000; -} - -/* Dark mode button colors */ -@media (prefers-color-scheme: dark) { - :root:not([data-theme="light"]) .mongoose-btn-outline { - border-color: #FFB4B4; - color: #FFB4B4; - } - :root:not([data-theme="light"]) .mongoose-btn-outline:hover { - background-color: rgba(255, 107, 107, 0.1); - } - :root:not([data-theme="light"]) .mongoose-btn-solid { - background-color: #FFB4B4; - color: #1a1a1a; - } - :root:not([data-theme="light"]) .mongoose-btn-solid:hover { - background-color: #FFCECE; - } -} - -[data-theme="dark"] .mongoose-btn-outline { - border-color: #FFB4B4; - color: #FFB4B4; -} - -[data-theme="dark"] .mongoose-btn-outline:hover { - background-color: rgba(255, 107, 107, 0.1); -} - -[data-theme="dark"] .mongoose-btn-solid { - background-color: #FFB4B4; - color: #1a1a1a; -} - -[data-theme="dark"] .mongoose-btn-solid:hover { - background-color: #FFCECE; + background-color: var(--brand-primary-hover); } diff --git a/docs/css/style.css b/docs/css/style.css index 3a18cd61525..cdaf11cecf5 100644 --- a/docs/css/style.css +++ b/docs/css/style.css @@ -35,18 +35,13 @@ body { right: 20px; bottom: auto; z-index: 1000; - background-color: rgba(255, 255, 255, 0.9); - border: 1px solid #ddd; + background-color: var(--bg-primary, rgba(255, 255, 255, 0.9)); + border: 1px solid var(--border-color, #ddd); border-radius: 50%; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + box-shadow: 0 2px 8px var(--shadow, rgba(0, 0, 0, 0.1)); transition: background-color 0.3s ease, border-color 0.3s ease; } -[data-theme="dark"] #theme-toggle { - background-color: rgba(26, 26, 26, 0.9); - border-color: #444; -} - #theme-toggle-btn { width: 44px; height: 44px; @@ -59,20 +54,12 @@ body { align-items: center; justify-content: center; border-radius: 50%; - color: #333; + color: var(--text-secondary, #333); transition: background-color 0.2s ease, transform 0.1s ease; } -body[data-theme="dark"] #theme-toggle-btn { - color: #e0e0e0; -} - #theme-toggle-btn:hover { - background-color: rgba(0, 0, 0, 0.1); -} - -body[data-theme="dark"] #theme-toggle-btn:hover { - background-color: rgba(255, 255, 255, 0.1); + background-color: var(--menu-hover, rgba(0, 0, 0, 0.1)); } #theme-toggle-btn:active { @@ -80,14 +67,10 @@ body[data-theme="dark"] #theme-toggle-btn:hover { } #theme-toggle-btn:focus-visible { - outline: 3px solid #0971B2; + outline: 3px solid var(--focus-ring, #0971B2); outline-offset: 2px; } -body[data-theme="dark"] #theme-toggle-btn:focus-visible { - outline-color: #4a9eff; -} - #theme-icon-light, #theme-icon-dark { position: absolute; @@ -124,7 +107,7 @@ body[data-theme="dark"] #theme-toggle-btn:focus-visible { /* location.hash */ :target::before { content: ">>> "; - color: #1371C9; + color: var(--link-color, #1371C9); font-weight: bold; font-size: 20px; } @@ -219,30 +202,18 @@ h2 a { font-size: 146px; font-weight: 100; text-indent: -23px; - color: #800; -} -[data-theme="dark"] #header .mongoose { - color: #FFB4B4; -} -@media (prefers-color-scheme: dark) { - body:not([data-theme="light"]) #header .mongoose { - color: #FFB4B4; - } + color: var(--brand-primary, #800); } .load #header .mongoose { letter-spacing: -14px; } .tagline { - color: #333; + color: var(--text-secondary, #333); font-size: 25px; - text-shadow: 1px 1px #f8f8f8; + text-shadow: 1px 1px var(--tagline-text-shadow, #f8f8f8); text-align: center; margin: 7px 0; } -[data-theme="dark"] .tagline { - color: #f8f8f8; - text-shadow: 1px 1px #222; -} .blurb { text-align: center; font-style: oblique; @@ -251,17 +222,7 @@ h2 a { } .tagline a, .blurb a { text-decoration: none; - color: #800; -} -[data-theme="dark"] .tagline a, -[data-theme="dark"] .blurb a { - color: #FFB4B4; -} -@media (prefers-color-scheme: dark) { - body:not([data-theme="light"]) .tagline a, - body:not([data-theme="light"]) .blurb a { - color: #FFB4B4; - } + color: var(--brand-primary, #800); } #links { margin: 50px 10px 20px; @@ -277,7 +238,7 @@ h2 a { margin: 0 15px; } #links a { - background: #444; + background: var(--btn-bg, #444); padding: 9px 0px; border-radius: 3px; color: white; @@ -285,7 +246,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, #222); } #follow { margin-bottom: 36px; @@ -401,7 +362,7 @@ h2 a { margin: 0 12px; } #links a { - background: #444; + background: var(--btn-bg, #444); padding: 7px 34px; font-size: 15px; } @@ -451,7 +412,7 @@ h2 a { #tidelift-button { display: inline-block; - background-color: #444; + background-color: var(--btn-bg, #444); padding-top: 9px; padding-left: 15px; padding-right: 15px; @@ -461,7 +422,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, #222); margin: auto; } #tidelift-button span { From 36d4f47d8e0d030465b7a3731d9818e5d444a893 Mon Sep 17 00:00:00 2001 From: Hafez Date: Mon, 8 Dec 2025 03:29:59 +0100 Subject: [PATCH 084/133] refactor: consolidate css properties into one single source of truth --- docs/css/api.css | 40 ++++++++++---------- docs/css/mongoose5.css | 86 ++++-------------------------------------- docs/css/style.css | 30 +-------------- 3 files changed, 29 insertions(+), 127 deletions(-) diff --git a/docs/css/api.css b/docs/css/api.css index 3d9b50fe0df..f46c6839e69 100644 --- a/docs/css/api.css +++ b/docs/css/api.css @@ -246,25 +246,23 @@ hr.separate-api { } /* Dark mode support for API page */ -@media (prefers-color-scheme: dark) { - .api-nav { - background-color: var(--menu-bg, #252525); - border-left-color: var(--border-color, #444); - } - - .api-nav a { - color: var(--text-muted, #888); - } - - .api-nav a:hover { - color: var(--link-color, #4a9eff); - } - - .native-ad { - background-color: var(--bg-secondary, #2d2d2d); - } - - .native-ad a { - color: var(--text-primary, #e0e0e0); - } +[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/mongoose5.css b/docs/css/mongoose5.css index c863217c869..d42cc7407a6 100644 --- a/docs/css/mongoose5.css +++ b/docs/css/mongoose5.css @@ -1,6 +1,6 @@ /* CSS Custom Properties for Theming */ -:root { - /* Light mode colors */ +/* Light mode (default) - JS sets data-theme based on system preference */ +:root, [data-theme="light"] { --bg-primary: #ffffff; --bg-secondary: #eee; --bg-tertiary: #fafafa; @@ -18,6 +18,7 @@ --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; @@ -29,38 +30,7 @@ --tagline-text-shadow: #f8f8f8; } -/* Dark mode colors */ -@media (prefers-color-scheme: dark) { - :root:not([data-theme="light"]) { - --bg-primary: #1a1a1a; - --bg-secondary: #2d2d2d; - --bg-tertiary: #252525; - --text-primary: #f0f0f0; - --text-secondary: #d8d8d8; - --text-muted: #a0a0a0; - --link-color: #5aabff; - --link-hover: #7dbfff; - --border-color: #444444; - --code-bg: #2d2d2d; - --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: #5aabff; - /* Brand colors */ - --brand-primary: #FFB4B4; - --brand-primary-hover: #FFCECE; - --brand-primary-subtle: rgba(255, 107, 107, 0.1); - /* Button colors */ - --btn-bg: #444; - --btn-text-shadow: #222; - /* Tagline text shadow */ - --tagline-text-shadow: #222; - } -} - -/* Manual dark mode override */ +/* Dark mode - applied via JS based on system preference or user toggle */ [data-theme="dark"] { --bg-primary: #1a1a1a; --bg-secondary: #2d2d2d; @@ -74,10 +44,11 @@ --code-bg: #2d2d2d; --code-text: #e8e8e8; --menu-bg: #252525; - --menu-hover: rgba(255,255,255, 0.1); - --menu-selected: rgba(255,255,255, 0.15); + --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: #5aabff; + --focus-ring-shadow: rgba(74, 158, 255, 0.2); /* Brand colors */ --brand-primary: #FFB4B4; --brand-primary-hover: #FFCECE; @@ -89,35 +60,6 @@ --tagline-text-shadow: #222; } -/* Light mode override */ -[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; - /* 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; -} - html { font-family: 'Open Sans'; color-scheme: light dark; @@ -216,12 +158,6 @@ h4 a:focus-visible { } /* Dark mode logo image swap */ -@media (prefers-color-scheme: dark) { - :root:not([data-theme="light"]) #logo { - content: url('/docs/images/mongoose5_62x30_transparent_light.png'); - } -} - [data-theme="dark"] #logo { content: url('/docs/images/mongoose5_62x30_transparent_light.png'); } @@ -348,13 +284,7 @@ li.version ul.pure-menu-children { .search input:focus { outline: none; border-color: var(--focus-ring); - box-shadow: 0 0 0 2px rgba(9, 113, 178, 0.1); -} - -@media (prefers-color-scheme: dark) { - .search input:focus { - box-shadow: 0 0 0 2px rgba(74, 158, 255, 0.2); - } + box-shadow: 0 0 0 2px var(--focus-ring-shadow); } #search-input-nav { diff --git a/docs/css/style.css b/docs/css/style.css index cdaf11cecf5..ba51a662cb8 100644 --- a/docs/css/style.css +++ b/docs/css/style.css @@ -1,33 +1,14 @@ body { font-family: 'Open Sans', Helvetica, Arial, FreeSans; - color: var(--text-secondary, #333); + color: var(--text-secondary); -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: 100%; padding: 0; margin: 0; - background-color: var(--bg-primary, #ffffff); + background-color: var(--bg-primary); transition: background-color 0.3s ease, color 0.3s ease; } -/* Dark mode support for homepage */ -@media (prefers-color-scheme: dark) { - body:not([data-theme="light"]) { - background-color: var(--bg-primary, #1a1a1a); - color: var(--text-secondary, #c0c0c0); - } -} - -/* Manual theme overrides */ -[data-theme="dark"] { - background-color: var(--bg-primary, #1a1a1a); - color: var(--text-secondary, #c0c0c0); -} - -[data-theme="light"] { - background-color: var(--bg-primary, #ffffff); - color: var(--text-secondary, #333); -} - /* Theme Toggle Button for Homepage */ #theme-toggle { position: fixed; @@ -276,13 +257,6 @@ h2 a { filter: invert(0.85) hue-rotate(180deg); background: transparent; } -@media (prefers-color-scheme: dark) { - body:not([data-theme="light"]) .github-btn { - color-scheme: light; - filter: invert(0.85) hue-rotate(180deg); - background: transparent; - } -} #what { margin-bottom: 32px; } From 48dc3c8bd8b523b0a8021dd4a2293b495675bb95 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Dec 2025 02:40:59 +0000 Subject: [PATCH 085/133] Initial plan From 75bafd7561f8ca084eb0e889056563ddc8bc8e31 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Dec 2025 02:44:28 +0000 Subject: [PATCH 086/133] refactor: remove all CSS variable fallbacks for consistency Co-authored-by: AbdelrahmanHafez <19984935+AbdelrahmanHafez@users.noreply.github.com> --- docs/css/style.css | 52 +++++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/docs/css/style.css b/docs/css/style.css index ba51a662cb8..1b4c5be5719 100644 --- a/docs/css/style.css +++ b/docs/css/style.css @@ -16,10 +16,10 @@ body { right: 20px; bottom: auto; z-index: 1000; - background-color: var(--bg-primary, rgba(255, 255, 255, 0.9)); - border: 1px solid var(--border-color, #ddd); + background-color: var(--bg-primary); + border: 1px solid var(--border-color); border-radius: 50%; - box-shadow: 0 2px 8px var(--shadow, rgba(0, 0, 0, 0.1)); + box-shadow: 0 2px 8px var(--shadow); transition: background-color 0.3s ease, border-color 0.3s ease; } @@ -35,12 +35,12 @@ body { align-items: center; justify-content: center; border-radius: 50%; - color: var(--text-secondary, #333); + color: var(--text-secondary); transition: background-color 0.2s ease, transform 0.1s ease; } #theme-toggle-btn:hover { - background-color: var(--menu-hover, rgba(0, 0, 0, 0.1)); + background-color: var(--menu-hover); } #theme-toggle-btn:active { @@ -48,7 +48,7 @@ body { } #theme-toggle-btn:focus-visible { - outline: 3px solid var(--focus-ring, #0971B2); + outline: 3px solid var(--focus-ring); outline-offset: 2px; } @@ -88,7 +88,7 @@ body { /* location.hash */ :target::before { content: ">>> "; - color: var(--link-color, #1371C9); + color: var(--link-color); font-weight: bold; font-size: 20px; } @@ -99,7 +99,7 @@ body { } a { - color: var(--link-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; @@ -109,7 +109,7 @@ a:hover { opacity: 0.8; } a:focus-visible { - outline: 3px solid var(--focus-ring, #0971B2); + outline: 3px solid var(--focus-ring); outline-offset: 2px; border-radius: 2px; } @@ -127,18 +127,18 @@ h1 { } pre { - background: var(--code-bg, #eee); + background: var(--code-bg); padding: 12px 16px; border-radius: 6px; overflow-x: auto; - border: 1px solid var(--border-color, #ddd); + border: 1px solid var(--border-color); transition: background-color 0.3s ease, border-color 0.3s ease; } code { - color: var(--code-text, #333); + color: var(--code-text); font-size: 11px; font-family: Consolas, "Liberation Mono", Courier, monospace; - background-color: var(--code-bg, #eee); + background-color: var(--code-bg); padding: 2px 6px; border-radius: 4px; } @@ -151,12 +151,12 @@ pre code { /* Dark mode for homepage code blocks */ [data-theme="dark"] pre { - background: var(--code-bg, #2d2d2d); - border-color: var(--border-color, #444); + background: var(--code-bg); + border-color: var(--border-color); } [data-theme="dark"] code { - color: var(--code-text, #e0e0e0); - background-color: var(--code-bg, #2d2d2d); + color: var(--code-text); + background-color: var(--code-bg); } #header { text-align: center; @@ -183,15 +183,15 @@ h2 a { font-size: 146px; font-weight: 100; text-indent: -23px; - color: var(--brand-primary, #800); + color: var(--brand-primary); } .load #header .mongoose { letter-spacing: -14px; } .tagline { - color: var(--text-secondary, #333); + color: var(--text-secondary); font-size: 25px; - text-shadow: 1px 1px var(--tagline-text-shadow, #f8f8f8); + text-shadow: 1px 1px var(--tagline-text-shadow); text-align: center; margin: 7px 0; } @@ -203,7 +203,7 @@ h2 a { } .tagline a, .blurb a { text-decoration: none; - color: var(--brand-primary, #800); + color: var(--brand-primary); } #links { margin: 50px 10px 20px; @@ -219,7 +219,7 @@ h2 a { margin: 0 15px; } #links a { - background: var(--btn-bg, #444); + background: var(--btn-bg); padding: 9px 0px; border-radius: 3px; color: white; @@ -227,7 +227,7 @@ h2 a { display: inline-block; text-decoration: none; text-transform: lowercase; - text-shadow: 1px 1px 7px var(--btn-text-shadow, #222); + text-shadow: 1px 1px 7px var(--btn-text-shadow); } #follow { margin-bottom: 36px; @@ -336,7 +336,7 @@ h2 a { margin: 0 12px; } #links a { - background: var(--btn-bg, #444); + background: var(--btn-bg); padding: 7px 34px; font-size: 15px; } @@ -386,7 +386,7 @@ h2 a { #tidelift-button { display: inline-block; - background-color: var(--btn-bg, #444); + background-color: var(--btn-bg); padding-top: 9px; padding-left: 15px; padding-right: 15px; @@ -396,7 +396,7 @@ h2 a { font-size: 20px; display: inline-block; text-decoration: none; - text-shadow: 1px 1px 7px var(--btn-text-shadow, #222); + text-shadow: 1px 1px 7px var(--btn-text-shadow); margin: auto; } #tidelift-button span { From 925c0cd6bc9babd8d7fd4e92f6cb02a463304475 Mon Sep 17 00:00:00 2001 From: Hafez Date: Mon, 8 Dec 2025 03:48:25 +0100 Subject: [PATCH 087/133] Update docs/css/mongoose5.css Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/css/mongoose5.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/css/mongoose5.css b/docs/css/mongoose5.css index d42cc7407a6..7202f7b8b22 100644 --- a/docs/css/mongoose5.css +++ b/docs/css/mongoose5.css @@ -52,7 +52,7 @@ /* Brand colors */ --brand-primary: #FFB4B4; --brand-primary-hover: #FFCECE; - --brand-primary-subtle: rgba(255, 107, 107, 0.1); + --brand-primary-subtle: rgba(255, 180, 180, 0.1); /* Button colors */ --btn-bg: #444; --btn-text-shadow: #222; From d5276cc453f24ea23fa8766a6127cf69456a1938 Mon Sep 17 00:00:00 2001 From: Hafez Date: Mon, 8 Dec 2025 11:03:29 +0100 Subject: [PATCH 088/133] fix: address PR feedback, use svg logo instead of image --- docs/css/api.css | 19 ++++++ docs/css/github.css | 138 ++++++++++++++++++++++----------------- docs/css/mongoose5.css | 58 +++++++++++++--- docs/css/style.css | 19 +++--- docs/images/mongoose.svg | 6 +- docs/js/theme-toggle.js | 14 ++-- docs/layout.pug | 4 +- 7 files changed, 162 insertions(+), 96 deletions(-) diff --git a/docs/css/api.css b/docs/css/api.css index f46c6839e69..997c39ec0e0 100644 --- a/docs/css/api.css +++ b/docs/css/api.css @@ -246,6 +246,25 @@ hr.separate-api { } /* 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); diff --git a/docs/css/github.css b/docs/css/github.css index d1d69d6f91e..723a91d0e58 100644 --- a/docs/css/github.css +++ b/docs/css/github.css @@ -169,126 +169,142 @@ github.com style (c) Vasily Polovnyov } /* Dark mode support for syntax highlighting */ -.code-theme-dark code { - background-color: var(--code-bg, #2d2d2d); - color: var(--link-color, #4a9eff); +@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); } -.code-theme-dark pre { - background-color: var(--code-bg, #2d2d2d); - border-color: var(--border-color, #444); - color: var(--code-text, #e0e0e0); +[data-theme="dark"] pre { + background-color: var(--code-bg); + border-color: var(--border-color); + color: var(--code-text); } -.code-theme-dark .hljs { - background: var(--code-bg, #2d2d2d); - color: var(--code-text, #e0e0e0); +[data-theme="dark"] .hljs { + background: var(--code-bg); + color: var(--code-text); } -.code-theme-dark .hljs-comment, -.code-theme-dark .diff .hljs-header, -.code-theme-dark .hljs-javadoc { +[data-theme="dark"] .hljs-comment, +[data-theme="dark"] .diff .hljs-header, +[data-theme="dark"] .hljs-javadoc { color: #6a9955; } -.code-theme-dark .hljs-keyword, -.code-theme-dark .css .rule .hljs-keyword, -.code-theme-dark .hljs-winutils, -.code-theme-dark .nginx .hljs-title, -.code-theme-dark .hljs-subst, -.code-theme-dark .hljs-request, -.code-theme-dark .hljs-status { +[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; } -.code-theme-dark .hljs-number, -.code-theme-dark .hljs-hexcolor, -.code-theme-dark .ruby .hljs-constant { +[data-theme="dark"] .hljs-number, +[data-theme="dark"] .hljs-hexcolor, +[data-theme="dark"] .ruby .hljs-constant { color: #b5cea8; } -.code-theme-dark .hljs-string, -.code-theme-dark .hljs-tag .hljs-value, -.code-theme-dark .hljs-phpdoc, -.code-theme-dark .hljs-dartdoc, -.code-theme-dark .tex .hljs-formula { +[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; } -.code-theme-dark .hljs-title, -.code-theme-dark .hljs-id, -.code-theme-dark .scss .hljs-preprocessor { +[data-theme="dark"] .hljs-title, +[data-theme="dark"] .hljs-id, +[data-theme="dark"] .scss .hljs-preprocessor { color: #d7ba7d; font-weight: bold; } -.code-theme-dark .hljs-class .hljs-title, -.code-theme-dark .hljs-type, -.code-theme-dark .vhdl .hljs-literal, -.code-theme-dark .tex .hljs-command { +[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; } -.code-theme-dark .hljs-tag, -.code-theme-dark .hljs-tag .hljs-title, -.code-theme-dark .hljs-rules .hljs-property, -.code-theme-dark .django .hljs-tag .hljs-keyword { +[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; } -.code-theme-dark .hljs-attribute, -.code-theme-dark .hljs-variable, -.code-theme-dark .lisp .hljs-body { +[data-theme="dark"] .hljs-attribute, +[data-theme="dark"] .hljs-variable, +[data-theme="dark"] .lisp .hljs-body { color: #9cdcfe; } -.code-theme-dark .hljs-regexp { +[data-theme="dark"] .hljs-regexp { color: #d16969; } -.code-theme-dark .hljs-symbol, -.code-theme-dark .ruby .hljs-symbol .hljs-string, -.code-theme-dark .lisp .hljs-keyword, -.code-theme-dark .clojure .hljs-keyword, -.code-theme-dark .scheme .hljs-keyword, -.code-theme-dark .tex .hljs-special, -.code-theme-dark .hljs-prompt { +[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; } -.code-theme-dark .hljs-built_in { +[data-theme="dark"] .hljs-built_in { color: #4fc1ff; } -.code-theme-dark .hljs-preprocessor, -.code-theme-dark .hljs-pragma, -.code-theme-dark .hljs-pi, -.code-theme-dark .hljs-doctype, -.code-theme-dark .hljs-shebang, -.code-theme-dark .hljs-cdata { +[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; } -.code-theme-dark .hljs-deletion { +[data-theme="dark"] .hljs-deletion { background: #5a1d1d; color: #f48771; } -.code-theme-dark .hljs-addition { +[data-theme="dark"] .hljs-addition { background: #1e3a1e; color: #b5cea8; } -.code-theme-dark .diff .hljs-change { +[data-theme="dark"] .diff .hljs-change { background: #2d4d2d; color: #4ec9b0; } -.code-theme-dark .hljs-chunk { +[data-theme="dark"] .hljs-chunk { color: #808080; } diff --git a/docs/css/mongoose5.css b/docs/css/mongoose5.css index 7202f7b8b22..7103b7d642a 100644 --- a/docs/css/mongoose5.css +++ b/docs/css/mongoose5.css @@ -1,5 +1,5 @@ /* CSS Custom Properties for Theming */ -/* Light mode (default) - JS sets data-theme based on system preference */ +/* Light mode (default) */ :root, [data-theme="light"] { --bg-primary: #ffffff; --bg-secondary: #eee; @@ -30,7 +30,39 @@ --tagline-text-shadow: #f8f8f8; } -/* Dark mode - applied via JS based on system preference or user toggle */ +/* 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: #2d2d2d; + --bg-tertiary: #252525; + --text-primary: #f0f0f0; + --text-secondary: #d8d8d8; + --text-muted: #a0a0a0; + --link-color: #FFB4B4; + --link-hover: #FFCECE; + --border-color: #444444; + --code-bg: #2d2d2d; + --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: #FFB4B4; + --brand-primary-hover: #FFCECE; + --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; @@ -38,8 +70,8 @@ --text-primary: #f0f0f0; --text-secondary: #d8d8d8; --text-muted: #a0a0a0; - --link-color: #5aabff; - --link-hover: #7dbfff; + --link-color: #FFB4B4; + --link-hover: #FFCECE; --border-color: #444444; --code-bg: #2d2d2d; --code-text: #e8e8e8; @@ -47,8 +79,8 @@ --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: #5aabff; - --focus-ring-shadow: rgba(74, 158, 255, 0.2); + --focus-ring: #FFB4B4; + --focus-ring-shadow: rgba(255, 180, 180, 0.2); /* Brand colors */ --brand-primary: #FFB4B4; --brand-primary-hover: #FFCECE; @@ -142,10 +174,20 @@ h4 a:focus-visible { } #logo { + display: inline-block; width: 62px; height: 30px; position: relative; top: 5px; + background-color: var(--brand-primary); + -webkit-mask-image: url('/docs/images/mongoose.svg'); + mask-image: url('/docs/images/mongoose.svg'); + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-position: center; + mask-position: center; } .logo-text { @@ -157,10 +199,6 @@ h4 a:focus-visible { text-transform: none; } -/* Dark mode logo image swap */ -[data-theme="dark"] #logo { - content: url('/docs/images/mongoose5_62x30_transparent_light.png'); -} .pure-menu-item { font-size: 12pt; diff --git a/docs/css/style.css b/docs/css/style.css index 1b4c5be5719..c85e99c1c18 100644 --- a/docs/css/style.css +++ b/docs/css/style.css @@ -133,6 +133,7 @@ pre { overflow-x: auto; border: 1px solid var(--border-color); transition: background-color 0.3s ease, border-color 0.3s ease; + line-height: 1.5; } code { color: var(--code-text); @@ -149,15 +150,6 @@ pre code { background-color: transparent; } -/* Dark mode for homepage code blocks */ -[data-theme="dark"] pre { - background: var(--code-bg); - border-color: var(--border-color); -} -[data-theme="dark"] code { - color: var(--code-text); - background-color: var(--code-bg); -} #header { text-align: center; padding-top: 20px; @@ -248,14 +240,19 @@ h2 a { position: relative; top: -4px; } -/* GitHub buttons iframe - use color-scheme to make iframe bg transparent in dark mode */ +/* 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); - background: transparent; } #what { margin-bottom: 32px; diff --git a/docs/images/mongoose.svg b/docs/images/mongoose.svg index a230aefd1f9..0920144c523 100644 --- a/docs/images/mongoose.svg +++ b/docs/images/mongoose.svg @@ -16,18 +16,18 @@ - + - + diff --git a/docs/js/theme-toggle.js b/docs/js/theme-toggle.js index 6671d792053..bb8f987d3b2 100644 --- a/docs/js/theme-toggle.js +++ b/docs/js/theme-toggle.js @@ -1,10 +1,14 @@ 'use strict'; (function() { const STORAGE_KEY = 'mongoose-theme'; - const CODE_THEME_CLASS = 'code-theme-dark'; 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') { @@ -24,13 +28,5 @@ // Silently fail - theme will still work for current session } } - const isDark = theme === 'dark'; - document.documentElement.classList.toggle(CODE_THEME_CLASS, isDark); - document.body.classList.toggle(CODE_THEME_CLASS, isDark); } - - const theme = localStorage.getItem(STORAGE_KEY) || (prefersDarkQuery?.matches ? 'dark' : 'light'); - applyTheme(theme, true); - const toggleBtn = document.getElementById('theme-toggle-btn'); - toggleBtn.addEventListener('click', toggleTheme); })(); diff --git a/docs/layout.pug b/docs/layout.pug index b9a39c233f5..83b7915310d 100644 --- a/docs/layout.pug +++ b/docs/layout.pug @@ -32,13 +32,13 @@ html(lang='en') #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 From 64b1960561bd196d70d1b2a43ab2f4baf24d93de Mon Sep 17 00:00:00 2001 From: Hafez Date: Mon, 8 Dec 2025 11:18:10 +0100 Subject: [PATCH 089/133] fix: fix line-height in home page intro code --- docs/css/github.css | 3 +++ docs/css/style.css | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/css/github.css b/docs/css/github.css index 723a91d0e58..36e77e2183e 100644 --- a/docs/css/github.css +++ b/docs/css/github.css @@ -13,6 +13,8 @@ pre code { padding: 0; font-size: 1em; color: var(--code-text, #222); + line-height: 1.6; + display: block; } pre { @@ -56,6 +58,7 @@ github.com style (c) Vasily Polovnyov 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, diff --git a/docs/css/style.css b/docs/css/style.css index c85e99c1c18..a9c8a7fc6a1 100644 --- a/docs/css/style.css +++ b/docs/css/style.css @@ -133,7 +133,6 @@ pre { overflow-x: auto; border: 1px solid var(--border-color); transition: background-color 0.3s ease, border-color 0.3s ease; - line-height: 1.5; } code { color: var(--code-text); From 01bc484a961339069ad6d0414cbdc795c5d03020 Mon Sep 17 00:00:00 2001 From: Hafez Date: Mon, 8 Dec 2025 11:19:05 +0100 Subject: [PATCH 090/133] chore: delete mongoose image logos --- docs/images/mongoose5_62x30_transparent.png | Bin 1208 -> 0 bytes .../images/mongoose5_62x30_transparent_light.png | Bin 1526 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 docs/images/mongoose5_62x30_transparent.png delete mode 100644 docs/images/mongoose5_62x30_transparent_light.png diff --git a/docs/images/mongoose5_62x30_transparent.png b/docs/images/mongoose5_62x30_transparent.png deleted file mode 100644 index 4df8e77ed1d96bca263800bb8c8780c91011af9e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1208 zcmV;p1V{UcP)BBQ~oSmI>_UZ}3KMb?;&hwtx|NptZ z9K=B!#QzyBLm7+*((Im;o{MYE#sMduYD_L=GTOx*_F+%L-a7UeyI|u=yb;Ni8*vpT z;3sUwCY+6%a433Y#IYN{;RozM2eu{guK2s75@B44=SVpfvxFym1%bzKLBXs$yZ#aa zoQ2~IMz0WKpAhC}yo-0RKB22(&yW}q@v^#Ucxf8W!_~M1vn#f~hNE#LR%02CMQg#P zPej8=(Sg^q>uc;Sicv2jhDE%hIGrTCcO@<}kU$mIif_$HmNw}D}|TZhuT*MH{)R(f^!nS%e+1p_hL$; z%^9A<)A&k=c9d|yN}O0{PY-tCZ7jYbU6`8CS0k53 zm0TyPYnKR(UBV;nLX=5jv}{u?Oi#tyq>8n?XRj7%v>4MX?CK$(@Fs}iXr?Hd^=!LE zRbQI0IhFnMSzo>f+wn1WBz#}ooyJGzU_LIyMWVgs=)(#FN6H@Dijxa#pW+44`&)&; zt-||5k>4rC*FG`QP8BV!tzffTw6gr#f)5hjr_j!_#y(9P4EmH%%>B~(Y%zO3gd+sW z=?Q%iG23^dGl__;p~z*#d0Z-j zEn>c2C%iv1iO?(BPZ6Era;!?YO$linRMx6bd>5V-v%n0zC%(A>--xj9PAXEZp5v3v3vQ8k0Dt(uV zBV&e5!sEYJyc;eSiFz=g#Wo_{2qX4*6*BT0%j=miZAr}9wc>)-gn!ER1#A_e{H3TF zJ@y|@yBq8qHpWam_j&_7g0AH)0pP#RP_~-klch{%-afr@;wC3>D$~!;) zwi*j6$8OwoEoj%jS8tvzPSlxp<9ufX^TfBNJ_QT+l=98{@c5-%&DXi=_kJ%8k5c^j zJ6dAqwTKd@aJlhU>q#z!Y5BPBKY`GY5q zo;`f}_+h%RfQXQoV%YK(Gj_D7oZpmsddZU1l&8}g8}>9UnzX5Fl~i4?&MP6QSqWdG zZdvWB`sL7e&?sd=PDV=4X&&KV-P##e#>L!hY~I=u*KfHU7dy3ObGgBxq^8A_jVGDv ztvh{3LgJ3C&B9w#P4?`mttnlg_T#7V;w(N6o=t9r%bW{5G@4T*EMgRzv=3MWnQ|TK z{MO@G)EKqqk&(#)ne9F8ns$kjdJ{4xPT{#acT$(Doo;QUTbh>N+oIDsn-q3!w%w?8 z;-u+?(}!2yt(vJAQEb<5$|rj2rsC-WX4Vb2xw%BUau#=)+6sFq9CK-An31~S%)GrE zp3WtmbqhtNNXDIF?R#zX{}Y#h^3F}GcCjr{j|~sMzF+Lbs@2zXGjf_I-DLdM&sKjXt^Wj-cZ_U^)UQ|?Rmyr2!=EBw3 zTmBxR3~4c8sf}qY^SQ(gG85?MW z7zRhu7*I9j=BH$)RpQpL)q2NqpbZ*u8%i>BQ;SOya|=LvOmz*7bPX&*42-M{jIE3; WA(q5@Kl2CbVeoYIb6Mw<&;$V6wJFsA From fb1887738c8040f2e768d32272b3643f4607684c Mon Sep 17 00:00:00 2001 From: Hafez Date: Mon, 8 Dec 2025 12:01:32 +0100 Subject: [PATCH 091/133] fix(docs): fix wrong theme flashing on page reload --- docs/layout.pug | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/layout.pug b/docs/layout.pug index 83b7915310d..42c50652759 100644 --- a/docs/layout.pug +++ b/docs/layout.pug @@ -4,6 +4,14 @@ 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 From a93d7780dac730fc9d23635fc808d1ec7ed6bf42 Mon Sep 17 00:00:00 2001 From: Hafez Date: Tue, 9 Dec 2025 06:42:06 +0100 Subject: [PATCH 092/133] fix(docs): fix SVG logo size to match original logo size --- docs/css/mongoose5.css | 6 ++-- docs/images/mongoose.svg | 76 +++++++++++++++++++++++++++++----------- 2 files changed, 59 insertions(+), 23 deletions(-) diff --git a/docs/css/mongoose5.css b/docs/css/mongoose5.css index 7103b7d642a..a098a45f20f 100644 --- a/docs/css/mongoose5.css +++ b/docs/css/mongoose5.css @@ -178,12 +178,12 @@ h4 a:focus-visible { 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: contain; - mask-size: contain; + -webkit-mask-size: 100% 100%; + mask-size: 100% 100%; -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat; -webkit-mask-position: center; diff --git a/docs/images/mongoose.svg b/docs/images/mongoose.svg index 0920144c523..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 + From 23089265080af2a5a78c763f43a1c756c3c3b77a Mon Sep 17 00:00:00 2001 From: Hafez Date: Tue, 9 Dec 2025 12:40:11 +0100 Subject: [PATCH 093/133] test(model): add tests re #15881 --- test/model.insertMany.test.js | 73 +++++++++++++++++++++++++++++++ test/model.middleware.test.js | 81 +++++++++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+) diff --git a/test/model.insertMany.test.js b/test/model.insertMany.test.js index 181b984b4e2..eba53ce1661 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.only('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..32e2af213dc 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.only('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 }; + } + }); }); }); From 329a42317e7b08cea65c4751ea639e0dfc615dea Mon Sep 17 00:00:00 2001 From: Hafez Date: Tue, 9 Dec 2025 12:43:40 +0100 Subject: [PATCH 094/133] fix(model): run error post hook handler for bulkwrite when an error is thrown from pre hook --- lib/model.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/model.js b/lib/model.js index 7b61cecff6a..2f02eae6fbf 100644 --- a/lib/model.js +++ b/lib/model.js @@ -3286,12 +3286,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]; From 662e4397e1442b6417a51b3f0406cc3ebaf4e258 Mon Sep 17 00:00:00 2001 From: Hafez Date: Tue, 9 Dec 2025 12:46:17 +0100 Subject: [PATCH 095/133] fix(test): remove `.only` --- test/model.insertMany.test.js | 2 +- test/model.middleware.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/model.insertMany.test.js b/test/model.insertMany.test.js index eba53ce1661..b12cac2a3a8 100644 --- a/test/model.insertMany.test.js +++ b/test/model.insertMany.test.js @@ -739,7 +739,7 @@ describe('insertMany()', function() { assert.ok(err.stack.includes('postInsertManyError')); }); - describe.only('pre-hook errors should propagate', function() { + 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'); diff --git a/test/model.middleware.test.js b/test/model.middleware.test.js index 32e2af213dc..d6bfcb5e118 100644 --- a/test/model.middleware.test.js +++ b/test/model.middleware.test.js @@ -625,7 +625,7 @@ describe('model middleware', function() { assert.strictEqual(res, 'skipMiddlewareFunction test'); }); - describe.only('pre-hook errors should propagate (gh-15881)', function() { + 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 From a428dd777cda6859bfa9aa842123b6419ba67f6a Mon Sep 17 00:00:00 2001 From: Hafez Date: Tue, 9 Dec 2025 16:03:26 +0100 Subject: [PATCH 096/133] docs(model): add `overwriteImmutable` option --- lib/model.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/model.js b/lib/model.js index 7b61cecff6a..24bce3c1056 100644 --- a/lib/model.js +++ b/lib/model.js @@ -3248,12 +3248,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] Set to true to allow updating immutable schema paths. * @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] Set to true to allow updating immutable schema paths. * @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 From 0daa2e10573a575e2d0143affa0d68fc0784c118 Mon Sep 17 00:00:00 2001 From: Hafez Date: Tue, 9 Dec 2025 16:44:45 +0100 Subject: [PATCH 097/133] Update lib/model.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- lib/model.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/model.js b/lib/model.js index 24bce3c1056..d5f1cb6d1a4 100644 --- a/lib/model.js +++ b/lib/model.js @@ -3248,14 +3248,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] Set to true to allow updating immutable schema paths. + * @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] Set to true to allow updating immutable schema paths. + * @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 From e1fdd39a2ddd7aecd87558c03d5009f0eb08e797 Mon Sep 17 00:00:00 2001 From: __Enderlord__ <22118126+I-Enderlord-I@users.noreply.github.com> Date: Thu, 11 Dec 2025 13:26:20 +0100 Subject: [PATCH 098/133] type: fixed `this` parameter type detection for methods with arguments --- types/utility.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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. From 8939114b7e9c74cf78deeb25f3703e367c808fa1 Mon Sep 17 00:00:00 2001 From: __Enderlord__ <22118126+I-Enderlord-I@users.noreply.github.com> Date: Thu, 11 Dec 2025 13:36:53 +0100 Subject: [PATCH 099/133] types: added test case for `this` parameter type on methods with arguments See #15778 --- test/types/models.test.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/types/models.test.ts b/test/types/models.test.ts index 0f3c14b7c7f..f5ecd80867a 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -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,7 +1094,7 @@ 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() { From 95d654d5167ecb5104f9f621a806af4861214d78 Mon Sep 17 00:00:00 2001 From: Hafez Date: Thu, 11 Dec 2025 20:17:37 +0100 Subject: [PATCH 100/133] ci(benchmark): use shallow clone (default fetch-depth: 1) Remove explicit fetch-depth: 0 from benchmark workflow. The TypeScript benchmark does not require git history - it only runs tsc type checking benchmarks on the current code. actions/checkout defaults to fetch-depth: 1 (shallow clone), which is sufficient for this workflow and reduces checkout time. --- .github/workflows/benchmark.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index a040e4f7e3c..42f2d32a09d 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -23,8 +23,6 @@ jobs: name: Benchmark TypeScript Types steps: - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - with: - fetch-depth: 0 - name: Setup node uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: From ecc878ad51d1fc0afa7a052918e57aab6c165ebc Mon Sep 17 00:00:00 2001 From: Hafez Date: Thu, 11 Dec 2025 20:24:20 +0100 Subject: [PATCH 101/133] chore: increase TS instantiation limit to 400k --- scripts/tsc-diagnostics-check.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/tsc-diagnostics-check.js b/scripts/tsc-diagnostics-check.js index fcebf5415f5..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]) ? 390000 : parseInt(process.argv[2], 10); +const maxInstantiations = isNaN(process.argv[2]) ? 400000 : parseInt(process.argv[2], 10); console.log(stdin); From 0a676c82fa5a15c0bd24e7c2f96cff6f9ae0d301 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 12 Dec 2025 12:32:07 -0500 Subject: [PATCH 102/133] quick color fixes --- docs/css/mongoose5.css | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/css/mongoose5.css b/docs/css/mongoose5.css index a098a45f20f..1ea4b374bf8 100644 --- a/docs/css/mongoose5.css +++ b/docs/css/mongoose5.css @@ -34,15 +34,15 @@ @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { --bg-primary: #1a1a1a; - --bg-secondary: #2d2d2d; + --bg-secondary: #252525; --bg-tertiary: #252525; --text-primary: #f0f0f0; --text-secondary: #d8d8d8; --text-muted: #a0a0a0; - --link-color: #FFB4B4; - --link-hover: #FFCECE; + --link-color: #7BC3FF; + --link-hover: #8AD0FF; --border-color: #444444; - --code-bg: #2d2d2d; + --code-bg: #252525; --code-text: #e8e8e8; --menu-bg: #252525; --menu-hover: rgba(255, 255, 255, 0.1); @@ -51,8 +51,8 @@ --focus-ring: #FFB4B4; --focus-ring-shadow: rgba(255, 180, 180, 0.2); /* Brand colors */ - --brand-primary: #FFB4B4; - --brand-primary-hover: #FFCECE; + --brand-primary: #FF5E5E; + --brand-primary-hover: #F55; --brand-primary-subtle: rgba(255, 180, 180, 0.1); /* Button colors */ --btn-bg: #444; @@ -70,10 +70,10 @@ --text-primary: #f0f0f0; --text-secondary: #d8d8d8; --text-muted: #a0a0a0; - --link-color: #FFB4B4; - --link-hover: #FFCECE; + --link-color: #7BC3FF; + --link-hover: #8AD0FF; --border-color: #444444; - --code-bg: #2d2d2d; + --code-bg: #252525; --code-text: #e8e8e8; --menu-bg: #252525; --menu-hover: rgba(255, 255, 255, 0.1); @@ -82,8 +82,8 @@ --focus-ring: #FFB4B4; --focus-ring-shadow: rgba(255, 180, 180, 0.2); /* Brand colors */ - --brand-primary: #FFB4B4; - --brand-primary-hover: #FFCECE; + --brand-primary: #FF5E5E; + --brand-primary-hover: #F55; --brand-primary-subtle: rgba(255, 180, 180, 0.1); /* Button colors */ --btn-bg: #444; @@ -221,7 +221,7 @@ h4 a:focus-visible { padding-bottom: 0px; } -.pure-menu-link:hover, +.pure-menu-link:hover, .pure-menu-link.selected { background-color: var(--menu-hover); } From b82a4c9dafe4578cfa4f7adf9b4b6724d9c5e3ef Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 13 Dec 2025 16:37:48 -0500 Subject: [PATCH 103/133] types(schema): avoid treating paths with default: null as required Fix #15878 --- test/types/schema.test.ts | 17 +++++++++++++++++ types/inferschematype.d.ts | 8 +++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index 95f70e37a2a..bb6a6fb6d1d 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -1999,3 +1999,20 @@ function autoInferredNestedMaps() { const doc = new TestModel({ nestedMap: new Map([['1', new Map([['2', 'value']])]]) }); expectType>>(doc.nestedMap); } + +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/types/inferschematype.d.ts b/types/inferschematype.d.ts index ab607930d52..6fa39add6b5 100644 --- a/types/inferschematype.d.ts +++ b/types/inferschematype.d.ts @@ -127,9 +127,11 @@ declare module 'mongoose' { : T; } -type IsPathDefaultUndefined = +type IsPathDefaultNullish = PathType extends { default: undefined } ? true + : PathType extends { default: null } ? true : PathType extends { default: (...args: any[]) => undefined } ? true + : PathType extends { default: (...args: any[]) => null } ? true : false; type RequiredPropertyDefinition = @@ -151,12 +153,12 @@ type IsPathRequired = false : true : P extends Record ? - IsPathDefaultUndefined

extends true ? + IsPathDefaultNullish

extends true ? false : true : P extends Record ? P extends { default: any } ? - IfEquals + IsPathDefaultNullish

extends true ? false : true : false : false; From 19a9bc795bff84ba6631509b87b56fe65ca0cd36 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 13 Dec 2025 21:36:40 -0500 Subject: [PATCH 104/133] refactor: remove internal callbacks for buffering Re: #15871 --- lib/drivers/node-mongodb-native/collection.js | 137 ++++-------------- test/collection.test.js | 15 +- 2 files changed, 36 insertions(+), 116 deletions(-) diff --git a/lib/drivers/node-mongodb-native/collection.js b/lib/drivers/node-mongodb-native/collection.js index b4a61fe075a..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,9 +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, @@ -134,78 +126,28 @@ function iter(i) { args: args }); - let callback; - let _args = args; - let promise = 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,60 +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)); - if (timeout != null) { - clearTimeout(timeout); - } - 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 (timeout != null) { clearTimeout(timeout); } - if (typeof lastArg === 'function') { - lastArg(null, result); - } else { - this.conn.emit('operation-end', { _id: opId, modelName: _this.modelName, collectionName: this.name, method: i, result }); - } + this.conn.emit('operation-end', { _id: opId, modelName: _this.modelName, collectionName: this.name, method: i, result }); return result; }, error => { if (timeout != null) { clearTimeout(timeout); } - if (typeof lastArg === 'function') { - lastArg(error); - return; - } else { - this.conn.emit('operation-end', { _id: opId, modelName: _this.modelName, collectionName: this.name, method: i, error }); - } + 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 (timeout != null) { clearTimeout(timeout); } - 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; - } + this.conn.emit('operation-end', { _id: opId, modelName: _this.modelName, collectionName: this.name, method: i, error: error }); + throw error; } }; } 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() { From cf66cfe5c3bb682afdd34b03e6f7df7303c31d03 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 14 Dec 2025 13:58:54 -0500 Subject: [PATCH 105/133] types(queries): apply Mongoose casting to default MongoDB driver _id in RootFilterOperators --- test/types/queries.test.ts | 14 ++++++++++++++ types/query.d.ts | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/test/types/queries.test.ts b/test/types/queries.test.ts index 23ae0424cb3..1f6857eadc8 100644 --- a/test/types/queries.test.ts +++ b/test/types/queries.test.ts @@ -865,3 +865,17 @@ async function gh15786() { const schema = new Schema, {}, {}, {}, DocStatics>({}); schema.static({ m1() {} } as DocStatics); } + +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/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 = From b5d2a0219a504028a660336bb0e9ed8ef703bb3c Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 14 Dec 2025 14:09:45 -0500 Subject: [PATCH 106/133] fix lint --- test/types/queries.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/types/queries.test.ts b/test/types/queries.test.ts index 1f6857eadc8..a3b41f70ce1 100644 --- a/test/types/queries.test.ts +++ b/test/types/queries.test.ts @@ -876,6 +876,6 @@ async function gh15779_2() { const JobModel = model('Job', jobSchema); const jobs = await JobModel.aggregate([ - { $match: {} as QueryFilter }, + { $match: {} as QueryFilter } ]); } From a282301b7e489d94287f143887e6132f6f7ca7d6 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 14 Dec 2025 17:20:38 -0500 Subject: [PATCH 107/133] types(schema): correctly infer virtuals, methods on hydrated doc type from schema options Fix #15869 --- test/types/schema.create.test.ts | 13 ++++++++++--- test/types/schema.test.ts | 28 ++++++++++++++++++++++++++++ types/index.d.ts | 12 ++++++++++-- types/inferschematype.d.ts | 8 ++++---- 4 files changed, 52 insertions(+), 9 deletions(-) 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..0915999a918 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -2086,3 +2086,31 @@ 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); +} diff --git a/types/index.d.ts b/types/index.d.ts index a445427e4c7..8be508dfeca 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, diff --git a/types/inferschematype.d.ts b/types/inferschematype.d.ts index ed758828054..67462ce6dcd 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; From 8e12d23bc83b146a1eb0a0f8e0282d4d59caee76 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 14 Dec 2025 21:00:37 -0500 Subject: [PATCH 108/133] chore: add script to more easily run benchmarks repeatedly --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index eb0f7f8723b..e8a7f5635eb 100644 --- a/package.json +++ b/package.json @@ -114,6 +114,7 @@ "tdd": "mocha ./test/*.test.js --inspect --watch --recursive --watch-files ./**/*.{js,ts}", "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", From 4bb2f778176e2e388874a8e00015d30fa7eef99e Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 14 Dec 2025 21:10:10 -0500 Subject: [PATCH 109/133] types(schema): simplify check for nullish defaults --- types/inferschematype.d.ts | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/types/inferschematype.d.ts b/types/inferschematype.d.ts index 6fa39add6b5..4d1e38ffe35 100644 --- a/types/inferschematype.d.ts +++ b/types/inferschematype.d.ts @@ -127,13 +127,6 @@ declare module 'mongoose' { : T; } -type IsPathDefaultNullish = - PathType extends { default: undefined } ? true - : PathType extends { default: null } ? true - : PathType extends { default: (...args: any[]) => undefined } ? true - : PathType extends { default: (...args: any[]) => null } ? true - : false; - type RequiredPropertyDefinition = | { required: true | string | [true, string | undefined] | { isRequired: true }; @@ -152,14 +145,9 @@ type IsPathRequired = P extends { required: false } ? false : true - : P extends Record ? - IsPathDefaultNullish

extends true ? - false - : true - : P extends Record ? - P extends { default: any } ? - IsPathDefaultNullish

extends true ? false : true - : false + : P extends { default: undefined | null | ((...args: any[]) => undefined) | ((...args: any[]) => null) } ? false + : P extends { default: any } ? true + : P extends Record ? true : false; /** From 48988be01c14f5fdcd696c02bd43906e17e2032f Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 15 Dec 2025 10:33:57 -0500 Subject: [PATCH 110/133] types: some performance optimizations and crank up max instantiations --- scripts/tsc-diagnostics-check.js | 2 +- types/inferrawdoctype.d.ts | 2 +- types/inferschematype.d.ts | 23 +++++++++++++---------- types/virtuals.d.ts | 6 +++--- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/scripts/tsc-diagnostics-check.js b/scripts/tsc-diagnostics-check.js index 55a6b01fe59..460376c0984 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]) ? 300000 : parseInt(process.argv[2], 10); +const maxInstantiations = isNaN(process.argv[2]) ? 350000 : parseInt(process.argv[2], 10); console.log(stdin); diff --git a/types/inferrawdoctype.d.ts b/types/inferrawdoctype.d.ts index 0e7956fd23f..59c3d7caf2c 100644 --- a/types/inferrawdoctype.d.ts +++ b/types/inferrawdoctype.d.ts @@ -97,7 +97,7 @@ declare module 'mongoose' { ResolveRawPathType ? Item : 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 ? InferRawDocType diff --git a/types/inferschematype.d.ts b/types/inferschematype.d.ts index 4d1e38ffe35..7423f400215 100644 --- a/types/inferschematype.d.ts +++ b/types/inferschematype.d.ts @@ -150,6 +150,12 @@ type IsPathRequired = : 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. @@ -228,6 +234,7 @@ type PathEnumOrString['enum']> = 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 @@ -244,7 +251,6 @@ type IsSchemaTypeFromBuiltinClass = : T extends Types.Decimal128 ? true : T extends NativeDate ? true : T extends typeof Schema.Types.Mixed ? true - : unknown extends Buffer ? false : T extends Buffer ? true : false; @@ -260,12 +266,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. @@ -304,7 +308,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 ? @@ -315,6 +319,5 @@ type ResolvePathType< typeKey: TypeKey; } > - : unknown, - TypeHint ->; + : unknown + : TypeHint; 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> + }; } From acd7f24cacb8bf8d2096b083e24484a62922a330 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 15 Dec 2025 10:51:09 -0500 Subject: [PATCH 111/133] types(schema): allow partial statics to schema.statics() Fix #15780 --- test/types/queries.test.ts | 2 +- types/index.d.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/test/types/queries.test.ts b/test/types/queries.test.ts index 16e85e51484..22ae7773bd9 100644 --- a/test/types/queries.test.ts +++ b/test/types/queries.test.ts @@ -810,5 +810,5 @@ async function gh15786() { } const schema = new Schema, {}, {}, {}, DocStatics>({}); - schema.static({ m1() {} } as DocStatics); + schema.static({ m1() {} }); } diff --git a/types/index.d.ts b/types/index.d.ts index 30fb89a9abd..644b55cd416 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -542,8 +542,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. */ From 8fc2f22cd77daf5f5d9eb041bb11b0031787177d Mon Sep 17 00:00:00 2001 From: Rohit Nair P Date: Mon, 15 Dec 2025 21:32:17 +0530 Subject: [PATCH 112/133] Fix: Prevent crash in Document.prototype.init() with null/undefined (#15812) * fix(document): prevent crash when calling init() with null/undefined * Fix: revert formatting changes and apply null check * refactor: move gh-15812 tests to model.test.js and fix lint * fix: resolved the lint errors * Moved test gh-15812 to model.test.js * Refactor error handling in model tests * Update model.test.js --- lib/document.js | 3 +++ test/model.test.js | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/lib/document.js b/lib/document.js index 63615cf0351..1a85eddd75b 100644 --- a/lib/document.js +++ b/lib/document.js @@ -677,6 +677,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 || {}; diff --git a/test/model.test.js b/test/model.test.js index d5bcdcea97b..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; @@ -9257,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')); + } + }); + }); }); From 3749ffb5655370f96309b7c24b4a3a658a14193d Mon Sep 17 00:00:00 2001 From: Rohit Nair P Date: Mon, 15 Dec 2025 21:32:17 +0530 Subject: [PATCH 113/133] Fix: Prevent crash in Document.prototype.init() with null/undefined (#15812) * fix(document): prevent crash when calling init() with null/undefined * Fix: revert formatting changes and apply null check * refactor: move gh-15812 tests to model.test.js and fix lint * fix: resolved the lint errors * Moved test gh-15812 to model.test.js * Refactor error handling in model tests * Update model.test.js --- lib/document.js | 3 +++ test/model.test.js | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/lib/document.js b/lib/document.js index 899cf4d7151..1f4258636fa 100644 --- a/lib/document.js +++ b/lib/document.js @@ -677,6 +677,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 || {}; diff --git a/test/model.test.js b/test/model.test.js index 10587ea4aa2..bf9f3d9503e 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; @@ -9094,6 +9095,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')); + } + }); + }); }); From 799deef819cc062f1e61a3b2451eb0263f588033 Mon Sep 17 00:00:00 2001 From: shash-hq Date: Mon, 15 Dec 2025 21:36:27 +0530 Subject: [PATCH 114/133] refactor: replace deprecated lodash.isequal with util.isDeepStrictEqual --- package.json | 3 +-- test/model.findOneAndUpdate.test.js | 8 ++++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 8b828716d9d..7e09099e3a8 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,6 @@ "express": "^4.19.2", "fs-extra": "~11.3.0", "highlight.js": "11.11.1", - "lodash.isequal": "4.5.0", "lodash.isequalwith": "4.4.0", "markdownlint-cli2": "^0.19.1", "marked": "15.x", @@ -139,4 +138,4 @@ "target": "ES2022" } } -} +} \ No newline at end of file diff --git a/test/model.findOneAndUpdate.test.js b/test/model.findOneAndUpdate.test.js index 272f37faaee..162524d2210 100644 --- a/test/model.findOneAndUpdate.test.js +++ b/test/model.findOneAndUpdate.test.js @@ -14,7 +14,7 @@ const Utils = require('../lib/utils'); const Schema = mongoose.Schema; const ObjectId = Schema.Types.ObjectId; const DocumentObjectId = mongoose.Types.ObjectId; -const isEqual = require('lodash.isequal'); +const { isDeepStrictEqual } = require('util'); const isEqualWith = require('lodash.isequalwith'); const util = require('./util'); const uuid = require('uuid'); @@ -696,7 +696,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(null, doc.name); }); - it('can do various deep equal checks (lodash.isEqual, lodash.isEqualWith, assert.deepEqual, utils.deepEqual) on object id after findOneAndUpdate (gh-2070)', async function() { + it('can do various deep equal checks (util.isDeepStrictEqual, lodash.isEqualWith, assert.deepEqual, utils.deepEqual) on object id after findOneAndUpdate (gh-2070)', async function() { const userSchema = new Schema({ name: String, contacts: [{ @@ -725,14 +725,14 @@ describe('model: findOneAndUpdate:', function() { // Re: commends on https://github.com/mongodb/js-bson/commit/aa0b54597a0af28cce3530d2144af708e4b66bf0 // Deep equality checks no longer work as expected with node 0.10. // Please file an issue if this is a problem for you - assert.ok(isEqual(doc.contacts[0].account, a2._id)); + assert.ok(isDeepStrictEqual(doc.contacts[0].account, a2._id)); const doc2 = await User.findOne({ name: 'parent' }); assert.deepEqual(doc2.contacts[0].account, a2._id); assert.ok(Utils.deepEqual(doc2.contacts[0].account, a2._id)); assert.ok(isEqualWith(doc2.contacts[0].account, a2._id, compareBuffers)); - assert.ok(isEqual(doc2.contacts[0].account, a2._id)); + assert.ok(isDeepStrictEqual(doc2.contacts[0].account, a2._id)); function compareBuffers(a, b) { if (Buffer.isBuffer(a) && Buffer.isBuffer(b)) { From b5a05b90763cda231a24e871266214a967336033 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 15 Dec 2025 11:13:30 -0500 Subject: [PATCH 115/133] fix: fix some merge issues from #15812 --- lib/document.js | 7 ++++--- lib/error/objectParameter.js | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/document.js b/lib/document.js index 1f4258636fa..4f8768e4bd0 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,9 +681,6 @@ Document.prototype.$init = function() { */ Document.prototype.$__init = function(doc, opts) { - if (doc == null) { - throw new ObjectParameterError(doc, 'doc', 'init'); - } this.$isNew = false; opts = opts || {}; diff --git a/lib/error/objectParameter.js b/lib/error/objectParameter.js index 0a2108e5c9b..938b6c29bb5 100644 --- a/lib/error/objectParameter.js +++ b/lib/error/objectParameter.js @@ -20,7 +20,7 @@ class ObjectParameterError extends MongooseError { constructor(value, paramName, fnName) { super('Parameter "' + paramName + '" to ' + fnName + - '() must be an object, got "' + value.toString() + '" (type ' + typeof value + ')'); + '() must be an object, got "' + (value?.toString() ?? (value + '')) + '" (type ' + typeof value + ')'); } } From 728cba33995c96ae73b8552f2564213dca7323dd Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 15 Dec 2025 11:25:26 -0500 Subject: [PATCH 116/133] chore: release 8.20.3 --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8567e22f6ff..f0f9ee8e856 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +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 + 8.20.2 / 2025-12-05 =================== * fix(model): bump version if necessary after successful bulkSave() #15809 #15800 diff --git a/package.json b/package.json index e8a7f5635eb..147b74bab85 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "8.20.2", + "version": "8.20.3", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From df6faab4cc353dfb4250907a09be998bbd440bc6 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 15 Dec 2025 11:54:20 -0500 Subject: [PATCH 117/133] Add InferRawDocTypeWithout_id type definition --- types/inferrawdoctype.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/types/inferrawdoctype.d.ts b/types/inferrawdoctype.d.ts index 906e4571581..9b44ee6bb8f 100644 --- a/types/inferrawdoctype.d.ts +++ b/types/inferrawdoctype.d.ts @@ -12,7 +12,7 @@ declare module 'mongoose' { ? ObtainSchemaGeneric : FlattenMaps>>; - export type InferPojoType< + export type InferRawDocTypeWithout_id< SchemaDefinition, TSchemaOptions extends Record = DefaultSchemaOptions, TTransformOptions = { bufferToBinary: false } @@ -29,7 +29,7 @@ declare module 'mongoose' { SchemaDefinition, TSchemaOptions extends Record = DefaultSchemaOptions, TTransformOptions = { bufferToBinary: false } - > = Require_id>; + > = Require_id>; /** * @summary Allows users to optionally choose their own type for a schema field for stronger typing. From 4607fe02e41781993e31007890d734247ff18446 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 15 Dec 2025 11:54:46 -0500 Subject: [PATCH 118/133] Replace InferPojoType with InferRawDocTypeWithout_id --- test/types/inferrawdoctype.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/types/inferrawdoctype.test.ts b/test/types/inferrawdoctype.test.ts index 743bb85be73..a4fb580e634 100644 --- a/test/types/inferrawdoctype.test.ts +++ b/test/types/inferrawdoctype.test.ts @@ -1,4 +1,4 @@ -import { InferRawDocType, type InferPojoType, type ResolveTimestamps, type Schema, type Types } from 'mongoose'; +import { InferRawDocType, type InferRawDocTypeWithout_id, type ResolveTimestamps, type Schema, type Types } from 'mongoose'; import { expectType } from 'tsd'; function inferPojoType() { @@ -20,7 +20,7 @@ function inferPojoType() { } }; - type UserType = InferPojoType< typeof schemaDefinition>; + type UserType = InferRawDocTypeWithout_id; expectType<{ email: string, password: string, dateOfBirth: Date }>({} as UserType); } function gh14839() { From 20e75af53e761efe8ae30071182cd7c59e04d0b7 Mon Sep 17 00:00:00 2001 From: hasezoey Date: Wed, 3 Dec 2025 16:22:19 +0100 Subject: [PATCH 119/133] chore(dev-deps): update mongodb-memory-server to 11.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a1c6158b287..c72ee36876f 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "mkdirp": "^3.0.1", "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", From 26021242b3514e6ede66c559db79dfd551a8cd85 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 15 Dec 2025 16:03:19 -0500 Subject: [PATCH 120/133] fix(model): ensure $isDeleted is set after calling `doc.deleteOne()` successfully Fix #15858 --- lib/model.js | 6 ++++++ test/document.test.js | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/lib/model.js b/lib/model.js index 189c8f808b9..caf159181a4 100644 --- a/lib/model.js +++ b/lib/model.js @@ -829,6 +829,12 @@ Model.prototype.deleteOne = function deleteOne(options) { query.post(function queryPostDeleteOne(cb) { self.constructor._middleware.execPost('deleteOne', self, [self], {}, cb); }); + query.transform(function setIsDeleted(result) { + if (result && result.deletedCount != null && result.deletedCount > 0) { + self.$isDeleted(true); + } + return result; + }); return query; }; diff --git a/test/document.test.js b/test/document.test.js index 5658e263e10..ce6fc3c2f1a 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-15878)', 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() { From b37d01c81f90a7bc9d03bf3e5bd8ef82d088da08 Mon Sep 17 00:00:00 2001 From: shash-hq Date: Tue, 16 Dec 2025 12:20:12 +0530 Subject: [PATCH 121/133] test: restore lodash.isequal and add util.isDeepStrictEqual coverage Realized we need to keep lodash.isequal for the existing compatibility tests. Added the native util check as an additional test case instead of a replacement. --- package.json | 1 + test/model.findOneAndUpdate.test.js | 253 ++++++++++++++-------------- 2 files changed, 129 insertions(+), 125 deletions(-) diff --git a/package.json b/package.json index 7e09099e3a8..eb572a92751 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "express": "^4.19.2", "fs-extra": "~11.3.0", "highlight.js": "11.11.1", + "lodash.isequal": "4.5.0", "lodash.isequalwith": "4.4.0", "markdownlint-cli2": "^0.19.1", "marked": "15.x", diff --git a/test/model.findOneAndUpdate.test.js b/test/model.findOneAndUpdate.test.js index 162524d2210..beb7dca7443 100644 --- a/test/model.findOneAndUpdate.test.js +++ b/test/model.findOneAndUpdate.test.js @@ -14,21 +14,22 @@ const Utils = require('../lib/utils'); const Schema = mongoose.Schema; const ObjectId = Schema.Types.ObjectId; const DocumentObjectId = mongoose.Types.ObjectId; +const isEqual = require('lodash.isequal'); const { isDeepStrictEqual } = require('util'); const isEqualWith = require('lodash.isequalwith'); const util = require('./util'); const uuid = require('uuid'); -describe('model: findOneAndUpdate:', function() { +describe('model: findOneAndUpdate:', function () { let Comments; let BlogPost; let db; - before(function() { + before(function () { db = start(); }); - after(async function() { + after(async function () { await db.close(); }); @@ -36,7 +37,7 @@ describe('model: findOneAndUpdate:', function() { afterEach(() => util.clearTestData(db)); afterEach(() => require('./util').stopRemainingOps(db)); - beforeEach(function() { + beforeEach(function () { Comments = new Schema(); Comments.add({ @@ -63,27 +64,27 @@ describe('model: findOneAndUpdate:', function() { }); BlogPost.virtual('titleWithAuthor') - .get(function() { + .get(function () { return this.get('title') + ' by ' + this.get('author'); }) - .set(function(val) { + .set(function (val) { const split = val.split(' by '); this.set('title', split[0]); this.set('author', split[1]); }); - BlogPost.method('cool', function() { + BlogPost.method('cool', function () { return this; }); - BlogPost.static('woot', function() { + BlogPost.static('woot', function () { return this; }); BlogPost = db.model('BlogPost', BlogPost); }); - it('returns the edited document', async function() { + it('returns the edited document', async function () { const M = BlogPost; const title = 'Tobi ' + random(); const author = 'Brian ' + random(); @@ -155,13 +156,13 @@ describe('model: findOneAndUpdate:', function() { assert.ok(up.comments[1]._id instanceof DocumentObjectId); }); - describe('will correctly', function() { + describe('will correctly', function () { let ItemParentModel, ItemChildModel; - beforeEach(function() { + beforeEach(function () { const itemSpec = new Schema({ item_id: { - type: ObjectId, required: true, default: function() { + type: ObjectId, required: true, default: function () { return new DocumentObjectId(); } }, @@ -178,7 +179,7 @@ describe('model: findOneAndUpdate:', function() { ItemChildModel = db.model('Test2', itemSpec); }); - it('update subdocument in array item', async function() { + it('update subdocument in array item', async function () { const item1 = new ItemChildModel({ address: { street: 'times square', @@ -214,7 +215,7 @@ describe('model: findOneAndUpdate:', function() { }); }); - it('returns the original document', async function() { + it('returns the original document', async function () { const M = BlogPost; const title = 'Tobi ' + random(); const author = 'Brian ' + random(); @@ -266,7 +267,7 @@ describe('model: findOneAndUpdate:', function() { assert.ok(up.comments[1]._id instanceof DocumentObjectId); }); - it('allows upserting', async function() { + it('allows upserting', async function () { const M = BlogPost; const title = 'Tobi ' + random(); const author = 'Brian ' + random(); @@ -309,7 +310,7 @@ describe('model: findOneAndUpdate:', function() { assert.strictEqual(0, up.owners.length); }); - it('options/conditions/doc are merged when no callback is passed', function(done) { + it('options/conditions/doc are merged when no callback is passed', function (done) { const M = BlogPost; const now = new Date(); let query; @@ -354,13 +355,13 @@ describe('model: findOneAndUpdate:', function() { done(); }); - it('updates numbers atomically', async function() { + it('updates numbers atomically', async function () { const post = new BlogPost(); post.set('meta.visitors', 5); await post.save(); - await Promise.all(Array(4).fill(null).map(async() => { + await Promise.all(Array(4).fill(null).map(async () => { await BlogPost.findOneAndUpdate({ _id: post._id }, { $inc: { 'meta.visitors': 1 } }); })); @@ -368,7 +369,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(doc.get('meta.visitors'), 9); }); - it('honors strict schemas', async function() { + it('honors strict schemas', async function () { const S = db.model('Test', Schema({ name: String }, { strict: true })); const s = new S({ name: 'orange crush' }); @@ -394,7 +395,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(doc3.name, 'orange crush'); }); - it('returns errors with strict:throw schemas', async function() { + it('returns errors with strict:throw schemas', async function () { const S = db.model('Test', Schema({ name: String }, { strict: 'throw' })); const s = new S({ name: 'orange crush' }); @@ -414,7 +415,7 @@ describe('model: findOneAndUpdate:', function() { assert.ok(/not in schema/.test(err2)); }); - it('returns the original document', async function() { + it('returns the original document', async function () { const M = BlogPost; const title = 'Tobi ' + random(); const author = 'Brian ' + random(); @@ -469,7 +470,7 @@ describe('model: findOneAndUpdate:', function() { assert.ok(up.comments[1]._id instanceof DocumentObjectId); }); - it('options/conditions/doc are merged when no callback is passed', function() { + it('options/conditions/doc are merged when no callback is passed', function () { const M = BlogPost; const _id = new DocumentObjectId(); @@ -498,7 +499,7 @@ describe('model: findOneAndUpdate:', function() { assert.strictEqual(undefined, query._conditions._id); }); - it('supports v3 select string syntax', function() { + it('supports v3 select string syntax', function () { const M = BlogPost; const _id = new DocumentObjectId(); @@ -516,7 +517,7 @@ describe('model: findOneAndUpdate:', function() { assert.strictEqual(0, query._fields.title); }); - it('supports v3 select object syntax', function() { + it('supports v3 select object syntax', function () { const M = BlogPost; const _id = new DocumentObjectId(); @@ -532,7 +533,7 @@ describe('model: findOneAndUpdate:', function() { assert.strictEqual(0, query._fields.title); }); - it('supports v3 sort string syntax', async function() { + it('supports v3 sort string syntax', async function () { const M = BlogPost; const now = new Date(); @@ -562,7 +563,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(doc.meta.visitors, 10); }); - it('supports v3 sort object syntax', function(done) { + it('supports v3 sort object syntax', function (done) { const M = BlogPost; const _id = new DocumentObjectId(); @@ -582,7 +583,7 @@ describe('model: findOneAndUpdate:', function() { done(); }); - it('supports $elemMatch with $in (gh-1091 gh-1100)', async function() { + it('supports $elemMatch with $in (gh-1091 gh-1100)', async function () { const postSchema = new Schema({ ids: [{ type: Schema.ObjectId }], title: String @@ -607,7 +608,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(found.ids[0].toString(), _id2.toString()); }); - it('supports population (gh-1395)', async function() { + it('supports population (gh-1395)', async function () { const M = db.model('Test1', { name: String }); const N = db.model('Test2', { a: { type: Schema.ObjectId, ref: 'Test1' }, i: Number }); @@ -623,7 +624,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal('i am an A', doc.a.name); }); - it('returns null when doing an upsert & new=false gh-1533', async function() { + it('returns null when doing an upsert & new=false gh-1533', async function () { const thingSchema = new Schema({ _id: String, flag: { @@ -643,7 +644,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(thing2.flag, false); }); - it('return hydrated document (gh-7734 gh-7735)', async function() { + it('return hydrated document (gh-7734 gh-7735)', async function () { const fruitSchema = new Schema({ name: { type: String } }); @@ -661,7 +662,7 @@ describe('model: findOneAndUpdate:', function() { assert.ok(fruit instanceof mongoose.Document); }); - it('return includeResultMetadata when doing an upsert & new=false gh-7770', async function() { + it('return includeResultMetadata when doing an upsert & new=false gh-7770', async function () { const thingSchema = new Schema({ _id: String, flag: { @@ -683,7 +684,7 @@ describe('model: findOneAndUpdate:', function() { }); - it('allows properties to be set to null gh-1643', async function() { + it('allows properties to be set to null gh-1643', async function () { const testSchema = new Schema({ name: [String] }); @@ -696,7 +697,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(null, doc.name); }); - it('can do various deep equal checks (util.isDeepStrictEqual, lodash.isEqualWith, assert.deepEqual, utils.deepEqual) on object id after findOneAndUpdate (gh-2070)', async function() { + it('can do various deep equal checks (lodash.isEqual, util.isDeepStrictEqual, lodash.isEqualWith, assert.deepEqual, utils.deepEqual) on object id after findOneAndUpdate (gh-2070)', async function () { const userSchema = new Schema({ name: String, contacts: [{ @@ -725,6 +726,7 @@ describe('model: findOneAndUpdate:', function() { // Re: commends on https://github.com/mongodb/js-bson/commit/aa0b54597a0af28cce3530d2144af708e4b66bf0 // Deep equality checks no longer work as expected with node 0.10. // Please file an issue if this is a problem for you + assert.ok(isEqual(doc.contacts[0].account, a2._id)); assert.ok(isDeepStrictEqual(doc.contacts[0].account, a2._id)); const doc2 = await User.findOne({ name: 'parent' }); @@ -732,6 +734,7 @@ describe('model: findOneAndUpdate:', function() { assert.deepEqual(doc2.contacts[0].account, a2._id); assert.ok(Utils.deepEqual(doc2.contacts[0].account, a2._id)); assert.ok(isEqualWith(doc2.contacts[0].account, a2._id, compareBuffers)); + assert.ok(isEqual(doc2.contacts[0].account, a2._id)); assert.ok(isDeepStrictEqual(doc2.contacts[0].account, a2._id)); function compareBuffers(a, b) { @@ -741,7 +744,7 @@ describe('model: findOneAndUpdate:', function() { } }); - it('adds __v on upsert (gh-2122) (gh-4505)', async function() { + it('adds __v on upsert (gh-2122) (gh-4505)', async function () { const accountSchema = new Schema({ name: String }); @@ -763,7 +766,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(doc2.__v, 0); }); - it('doesn\'t add __v on upsert if `$set` (gh-4505) (gh-5973)', function() { + it('doesn\'t add __v on upsert if `$set` (gh-4505) (gh-5973)', function () { const accountSchema = new Schema({ name: String }); @@ -777,7 +780,7 @@ describe('model: findOneAndUpdate:', function() { then(doc => assert.strictEqual(doc.__v, 1)); }); - it('doesn\'t add __v on upsert if `$set` with `update()` (gh-5973)', function() { + it('doesn\'t add __v on upsert if `$set` with `update()` (gh-5973)', function () { const accountSchema = new Schema({ name: String }); @@ -791,7 +794,7 @@ describe('model: findOneAndUpdate:', function() { then(doc => assert.strictEqual(doc.__v, 1)); }); - it('works with nested schemas and $pull+$or (gh-1932)', async function() { + it('works with nested schemas and $pull+$or (gh-1932)', async function () { const TickSchema = new Schema({ name: String }); const TestSchema = new Schema({ a: Number, b: Number, ticks: [TickSchema] }); @@ -804,7 +807,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(doc.ticks[0].name, 'coffee'); }); - it('accepts undefined', async function() { + it('accepts undefined', async function () { const s = new Schema({ time: Date, base: String @@ -815,7 +818,7 @@ describe('model: findOneAndUpdate:', function() { await Breakfast.findOneAndUpdate({}, { time: undefined, base: undefined }, {}); }); - it('cast errors for empty objects as object ids (gh-2732)', async function() { + it('cast errors for empty objects as object ids (gh-2732)', async function () { const s = new Schema({ base: ObjectId }); @@ -830,7 +833,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(err.name, 'CastError'); }); - it('cast errors for empty objects as object ids (gh-2732)', async function() { + it('cast errors for empty objects as object ids (gh-2732)', async function () { const s = new Schema({ base: ObjectId }); @@ -847,20 +850,20 @@ describe('model: findOneAndUpdate:', function() { } }); - describe('middleware', function() { - it('works', async function() { + describe('middleware', function () { + it('works', async function () { const s = new Schema({ topping: { type: String, default: 'bacon' }, base: String }); let preCount = 0; - s.pre('findOneAndUpdate', function() { + s.pre('findOneAndUpdate', function () { ++preCount; }); let postCount = 0; - s.post('findOneAndUpdate', function() { + s.post('findOneAndUpdate', function () { ++postCount; }); @@ -871,19 +874,19 @@ describe('model: findOneAndUpdate:', function() { assert.equal(postCount, 1); }); - it('works with exec()', async function() { + it('works with exec()', async function () { const s = new Schema({ topping: { type: String, default: 'bacon' }, base: String }); let preCount = 0; - s.pre('findOneAndUpdate', function() { + s.pre('findOneAndUpdate', function () { ++preCount; }); let postCount = 0; - s.post('findOneAndUpdate', function() { + s.post('findOneAndUpdate', function () { ++postCount; }); @@ -895,8 +898,8 @@ describe('model: findOneAndUpdate:', function() { }); }); - describe('validators (gh-860)', function() { - it('applies defaults on upsert', async function() { + describe('validators (gh-860)', function () { + it('applies defaults on upsert', async function () { const s = new Schema({ topping: { type: String, default: 'bacon' }, base: String @@ -916,7 +919,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(1, count); }); - it('doesnt set default on upsert if query sets it', async function() { + it('doesnt set default on upsert if query sets it', async function () { const s = new Schema({ topping: { type: String, default: 'bacon' }, numEggs: { type: Number, default: 3 }, @@ -935,7 +938,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(breakfast.numEggs, 4); }); - it('properly sets default on upsert if query wont set it', async function() { + it('properly sets default on upsert if query wont set it', async function () { const s = new Schema({ topping: { type: String, default: 'bacon' }, base: String @@ -955,7 +958,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(1, count); }); - it('skips setting defaults within maps (gh-7909)', async function() { + it('skips setting defaults within maps (gh-7909)', async function () { const socialMediaHandleSchema = Schema({ links: [String] }); const profileSchema = Schema({ username: String, @@ -974,17 +977,17 @@ describe('model: findOneAndUpdate:', function() { assert.equal(doc.socialMediaHandles, undefined); }); - it('runs validators if theyre set', async function() { + it('runs validators if theyre set', async function () { const s = new Schema({ topping: { type: String, - validate: function() { + validate: function () { return false; } }, base: { type: String, - validate: function() { + validate: function () { return true; } } @@ -1008,11 +1011,11 @@ describe('model: findOneAndUpdate:', function() { assert.equal(error.errors.topping.message, 'Validator failed for path `topping` with value `bacon`'); }); - it('validators handle $unset and $setOnInsert', async function() { + it('validators handle $unset and $setOnInsert', async function () { const s = new Schema({ steak: { type: String, required: true }, eggs: { - type: String, validate: function() { + type: String, validate: function () { return false; } } @@ -1034,7 +1037,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(error.errors.steak.message, 'Path `steak` is required.'); }); - it('min/max, enum, and regex built-in validators work', async function() { + it('min/max, enum, and regex built-in validators work', async function () { const s = new Schema({ steak: { type: String, enum: ['ribeye', 'sirloin'] }, eggs: { type: Number, min: 4, max: 6 }, @@ -1072,7 +1075,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(error.errors.bacon.message, 'Path `bacon` is invalid (none).'); }); - it('multiple validation errors', async function() { + it('multiple validation errors', async function () { const s = new Schema({ steak: { type: String, enum: ['ribeye', 'sirloin'] }, eggs: { type: Number, min: 4, max: 6 }, @@ -1092,7 +1095,7 @@ describe('model: findOneAndUpdate:', function() { assert.ok(Object.keys(error.errors).indexOf('eggs') !== -1); }); - it('validators ignore $inc', async function() { + it('validators ignore $inc', async function () { const s = new Schema({ steak: { type: String, required: true }, eggs: { type: Number, min: 4 } @@ -1108,7 +1111,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(breakfast.eggs, 1); }); - it('validators ignore paths underneath mixed (gh-8659)', function() { + it('validators ignore paths underneath mixed (gh-8659)', function () { let called = 0; const s = new Schema({ n: { @@ -1123,7 +1126,7 @@ describe('model: findOneAndUpdate:', function() { then(() => assert.equal(called, 0)); }); - it('should work with arrays (gh-3035)', async function() { + it('should work with arrays (gh-3035)', async function () { const testSchema = new mongoose.Schema({ id: String, name: String, @@ -1139,7 +1142,7 @@ describe('model: findOneAndUpdate:', function() { await TestModel.findOneAndUpdate({ id: '1' }, { $set: { name: 'Joe' } }, { upsert: true }); }); - it('should allow null values in query (gh-3135)', async function() { + it('should allow null values in query (gh-3135)', async function () { const testSchema = new mongoose.Schema({ id: String, blob: ObjectId, @@ -1151,7 +1154,7 @@ describe('model: findOneAndUpdate:', function() { await TestModel.findOneAndUpdate({ id: '1', blob: null }, { $set: { status: 'inactive' } }, { upsert: true }); }); - it('should work with array documents (gh-3034)', async function() { + it('should work with array documents (gh-3034)', async function () { const testSchema = new mongoose.Schema({ id: String, name: String, @@ -1169,7 +1172,7 @@ describe('model: findOneAndUpdate:', function() { await TestModel.findOneAndUpdate({ id: '1' }, { $set: { name: 'Joe' } }, { upsert: true }); }); - it('handles setting array (gh-3107)', async function() { + it('handles setting array (gh-3107)', async function () { const testSchema = new mongoose.Schema({ name: String, a: [{ @@ -1188,7 +1191,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(doc.b[0], 2); }); - it('handles nested cast errors (gh-3468)', async function() { + it('handles nested cast errors (gh-3468)', async function () { const recordSchema = new mongoose.Schema({ kind: String, amount: Number @@ -1220,7 +1223,7 @@ describe('model: findOneAndUpdate:', function() { } }); - it('cast errors with nested schemas (gh-3580)', async function() { + it('cast errors with nested schemas (gh-3580)', async function () { const nested = new Schema({ num: Number }); const s = new Schema({ nested: nested }); @@ -1231,7 +1234,7 @@ describe('model: findOneAndUpdate:', function() { assert.ok(error); }); - it('pull with nested schemas (gh-3616)', async function() { + it('pull with nested schemas (gh-3616)', async function () { const nested = new Schema({ arr: [{ num: Number }] }); const s = new Schema({ nested: nested }); @@ -1244,7 +1247,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(doc.nested.arr.length, 0); }); - it('setting nested schema (gh-3889)', async function() { + it('setting nested schema (gh-3889)', async function () { const nested = new Schema({ test: String }); const s = new Schema({ nested: nested }); const MyModel = db.model('Test', s); @@ -1255,8 +1258,8 @@ describe('model: findOneAndUpdate:', function() { }); }); - describe('bug fixes', function() { - it('passes raw result if includeResultMetadata specified (gh-4925)', async function() { + describe('bug fixes', function () { + it('passes raw result if includeResultMetadata specified (gh-4925)', async function () { const testSchema = new mongoose.Schema({ test: String }); @@ -1273,7 +1276,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(res.lastErrorObject.n, 1); }); - it('handles setting single embedded docs to null (gh-4281)', async function() { + it('handles setting single embedded docs to null (gh-4281)', async function () { const foodSchema = new mongoose.Schema({ name: { type: String, default: 'Bacon' } }); @@ -1293,7 +1296,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(doc.main, null); }); - it('custom validator on mixed field (gh-4305)', async function() { + it('custom validator on mixed field (gh-4305)', async function () { let called = 0; const boardSchema = new Schema({ @@ -1305,7 +1308,7 @@ describe('model: findOneAndUpdate:', function() { type: Schema.Types.Mixed, required: true, validate: { - validator: function() { + validator: function () { ++called; return true; }, @@ -1338,7 +1341,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(called, 1); }); - it('single nested doc cast errors (gh-3602)', async() => { + it('single nested doc cast errors (gh-3602)', async () => { const AddressSchema = new Schema({ street: { type: Number @@ -1360,7 +1363,7 @@ describe('model: findOneAndUpdate:', function() { ); }); - it('projection option as alias for fields (gh-4315)', async function() { + it('projection option as alias for fields (gh-4315)', async function () { const TestSchema = new Schema({ test1: String, test2: String @@ -1373,7 +1376,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(doc.test1, 'a'); }); - it('handles upserting a non-existing field (gh-4757)', async function() { + it('handles upserting a non-existing field (gh-4757)', async function () { const modelSchema = new Schema({ field: Number }, { strict: 'throw' }); const Model = db.model('Test', modelSchema); @@ -1389,7 +1392,7 @@ describe('model: findOneAndUpdate:', function() { } }); - it('strict option (gh-5108)', async function() { + it('strict option (gh-5108)', async function () { const modelSchema = new Schema({ field: Number }, { strict: 'throw' }); const Model = db.model('Test', modelSchema); @@ -1402,7 +1405,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(doc.get('otherField'), 3); }); - it('correct key order (gh-6484)', function() { + it('correct key order (gh-6484)', function () { const modelSchema = new Schema({ nested: { field1: Number, field2: Number } }); @@ -1410,19 +1413,19 @@ describe('model: findOneAndUpdate:', function() { const Model = db.model('Test', modelSchema); const opts = { upsert: true, new: true }; return Model.findOneAndUpdate({}, { nested: { field1: 1, field2: 2 } }, opts).exec(). - then(function() { + then(function () { return Model.collection.findOne(); }). - then(function(doc) { + then(function (doc) { // Make sure order is correct assert.deepEqual(Object.keys(doc.nested), ['field1', 'field2']); }); }); - it('should not apply schema transforms (gh-4574)', function(done) { + it('should not apply schema transforms (gh-4574)', function (done) { const options = { toObject: { - transform: function() { + transform: function () { assert.ok(false, 'should not call transform'); } } @@ -1440,17 +1443,17 @@ describe('model: findOneAndUpdate:', function() { const Collection = db.model('Test', CollectionSchema); Collection.create({ field2: { arrayField: [] } }). - then(function(doc) { + then(function (doc) { return Collection.findByIdAndUpdate(doc._id, { $push: { 'field2.arrayField': { test: 'test' } } }, { new: true }); }). - then(function() { + then(function () { done(); }); }); - it('update using $ (gh-5628)', async function() { + it('update using $ (gh-5628)', async function () { const schema = new mongoose.Schema({ elems: [String] }); @@ -1469,7 +1472,7 @@ describe('model: findOneAndUpdate:', function() { assert.deepEqual(updatedDoc.elems, ['c', 'b']); }); - it('projection with $elemMatch (gh-5661)', async function() { + it('projection with $elemMatch (gh-5661)', async function () { const schema = new mongoose.Schema({ name: { type: String, default: 'test' }, arr: [{ tag: String }] @@ -1490,7 +1493,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(doc2.arr[0].tag, 't1'); }); - it('multi cast error (gh-5609)', async function() { + it('multi cast error (gh-5609)', async function () { const schema = new mongoose.Schema({ num1: Number, num2: Number @@ -1508,7 +1511,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(error.errors['num2'].name, 'CastError'); }); - it('update validators with pushing null (gh-5710)', async function() { + it('update validators with pushing null (gh-5710)', async function () { const schema = new mongoose.Schema({ arr: [String] }); @@ -1520,14 +1523,14 @@ describe('model: findOneAndUpdate:', function() { await Model.findOneAndUpdate({}, update, options); }); - it('only calls setters once (gh-6203)', async function() { + it('only calls setters once (gh-6203)', async function () { const calls = []; const userSchema = new mongoose.Schema({ name: String, foo: { type: String, - set: function(val) { + set: function (val) { calls.push(val); return val + val; } @@ -1540,14 +1543,14 @@ describe('model: findOneAndUpdate:', function() { assert.deepEqual(calls, ['123']); }); - it('only calls setters once (gh-6203)', async function() { + it('only calls setters once (gh-6203)', async function () { const calls = []; const userSchema = new mongoose.Schema({ name: String, foo: { type: String, - set: function(val) { + set: function (val) { calls.push(val); return val + val; } @@ -1560,7 +1563,7 @@ describe('model: findOneAndUpdate:', function() { assert.deepEqual(calls, ['123']); }); - it('update validators with pull + $in (gh-6240)', async function() { + it('update validators with pull + $in (gh-6240)', async function () { const highlightSchema = new mongoose.Schema({ _id: { type: String, @@ -1616,7 +1619,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(res.highlights.length, 0); }); - it('avoids edge case with middleware cloning buffers (gh-5702)', async function() { + it('avoids edge case with middleware cloning buffers (gh-5702)', async function () { function toUUID(string) { if (!string) { return null; @@ -1649,7 +1652,7 @@ describe('model: findOneAndUpdate:', function() { }] }, { collection: 'users' }); - UserSchema.pre('findOneAndUpdate', function() { + UserSchema.pre('findOneAndUpdate', function () { this.updateOne({}, { $set: { lastUpdate: new Date() } }); }); @@ -1671,7 +1674,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(updatedUser.friends[0].status, 'Active'); }); - it('setting subtype when saving (gh-5551)', async function() { + it('setting subtype when saving (gh-5551)', async function () { const uuid = require('uuid'); function toUUID(string) { if (!string) { @@ -1707,7 +1710,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(result.foo.sub_type, 4); }); - it('properly handles casting nested objects in update (gh-4724)', function(done) { + it('properly handles casting nested objects in update (gh-4724)', function (done) { const locationSchema = new Schema({ _id: false, location: { @@ -1729,7 +1732,7 @@ describe('model: findOneAndUpdate:', function() { }); t.save(). - then(function(t) { + then(function (t) { return T.findByIdAndUpdate(t._id, { $set: { 'locations.0': { @@ -1738,18 +1741,18 @@ describe('model: findOneAndUpdate:', function() { } }, { new: true }); }). - then(function(res) { + then(function (res) { assert.equal(res.locations[0].location.coordinates[0], -123); done(); }). catch(done); }); - it('doesnt do double validation on document arrays during updates (gh-4440)', async function() { + it('doesnt do double validation on document arrays during updates (gh-4440)', async function () { const A = new Schema({ str: String }); let B = new Schema({ a: [A] }); let validateCalls = 0; - B.path('a').validate(function(val) { + B.path('a').validate(function (val) { ++validateCalls; assert(Array.isArray(val)); return true; @@ -1766,7 +1769,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(validateCalls, 1); }); - it('runs setters on array elements (gh-7679)', function() { + it('runs setters on array elements (gh-7679)', function () { const bookSchema = new Schema({ genres: { type: [{ @@ -1783,7 +1786,7 @@ describe('model: findOneAndUpdate:', function() { then(doc => assert.equal(doc.genres[0], 'sci-fi')); }); - it('avoid calling $pull in doc array (gh-6971) (gh-6889)', function() { + it('avoid calling $pull in doc array (gh-6971) (gh-6889)', function () { const schema = new Schema({ arr: { type: [{ x: String }], @@ -1799,7 +1802,7 @@ describe('model: findOneAndUpdate:', function() { return Model.findOneAndUpdate({}, { $pull: { arr: { x: 'three' } } }, opts); }); - it('$pull with `required` and runValidators (gh-6972)', async function() { + it('$pull with `required` and runValidators (gh-6972)', async function () { const schema = new mongoose.Schema({ someArray: { type: [{ @@ -1821,7 +1824,7 @@ describe('model: findOneAndUpdate:', function() { }); }); - it('with versionKey in top-level and a `$` key (gh-7003)', async function() { + it('with versionKey in top-level and a `$` key (gh-7003)', async function () { const schema = new Schema({ name: String }); const Model = db.model('Test', schema); @@ -1837,7 +1840,7 @@ describe('model: findOneAndUpdate:', function() { assert.ok(!doc.name); }); - it('empty update with timestamps (gh-7041)', async function() { + it('empty update with timestamps (gh-7041)', async function () { const schema = new Schema({ name: String }, { timestamps: true }); const Model = db.model('Test', schema); @@ -1848,7 +1851,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(doc.name, 'test'); }); - it('skipping updatedAt and createdAt (gh-3934)', async function() { + it('skipping updatedAt and createdAt (gh-3934)', async function () { const schema = new Schema({ name: String }, { timestamps: true }); const Model = db.model('Test', schema); @@ -1868,7 +1871,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(doc.updatedAt.valueOf(), start.valueOf()); }); - it('runs lowercase on $addToSet, $push, etc (gh-4185)', async function() { + it('runs lowercase on $addToSet, $push, etc (gh-4185)', async function () { const Cat = db.model('Test', { _id: String, myArr: { type: [{ type: String, lowercase: true }], default: undefined } @@ -1882,7 +1885,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(res.myArr[0], 'case sensitive'); }); - it('returnOriginal (gh-7846)', async function() { + it('returnOriginal (gh-7846)', async function () { const Cat = db.model('Cat', { name: String }); @@ -1895,7 +1898,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(res.name, 'test2'); }); - it('updating embedded discriminator with discriminator key in update (gh-8378)', async function() { + it('updating embedded discriminator with discriminator key in update (gh-8378)', async function () { const shapeSchema = Schema({ name: String }, { discriminatorKey: 'kind' }); const schema = Schema({ shape: shapeSchema }); @@ -1926,7 +1929,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(doc.shape.radius, 10); }); - it('setDefaultsOnInsert with doubly nested subdocs (gh-8392)', function() { + it('setDefaultsOnInsert with doubly nested subdocs (gh-8392)', function () { const nestedSchema = Schema({ name: String }); const Model = db.model('Test', Schema({ L1: Schema({ @@ -1943,7 +1946,7 @@ describe('model: findOneAndUpdate:', function() { then(doc => assert.equal(doc.L1.L2.name, 'foo')); }); - it('calls setters on mixed type (gh-8444)', async function() { + it('calls setters on mixed type (gh-8444)', async function () { const userSchema = new Schema({ jobCategory: { type: Object, @@ -1976,7 +1979,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(doc.jobCategory.value, 'from setter 2'); }); - it('returnDocument should work (gh-10321)', async function() { + it('returnDocument should work (gh-10321)', async function () { const testSchema = Schema({ a: Number }); const Model = db.model('Test', testSchema); @@ -1987,7 +1990,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(doc.a, 2); }); - it('supports overwriting nested map paths (gh-10485)', async function() { + it('supports overwriting nested map paths (gh-10485)', async function () { const child = new mongoose.Schema({ vals: { type: mongoose.Schema.Types.Map, @@ -2012,7 +2015,7 @@ describe('model: findOneAndUpdate:', function() { assert.deepEqual(res.toObject().children[0].vals, new Map([['telegram', 'hello']])); }); - it('supports $set on elements of map of subdocuments (gh-10720)', async function() { + it('supports $set on elements of map of subdocuments (gh-10720)', async function () { const parentSchema = new mongoose.Schema({ data: new mongoose.Schema({ children: { @@ -2036,7 +2039,7 @@ describe('model: findOneAndUpdate:', function() { assert.strictEqual(res.data.children.get('kenny').age, 1); }); - it('handles validating deeply nested subdocuments (gh-11394)', async function() { + it('handles validating deeply nested subdocuments (gh-11394)', async function () { const userSchema = new Schema({ myId: Number, address: { @@ -2058,7 +2061,7 @@ describe('model: findOneAndUpdate:', function() { assert.ifError(err); }); - it('casts array filters (gh-13219)', async function() { + it('casts array filters (gh-13219)', async function () { const MyModel = db.model('Test', new Schema({ _id: Number, grades: [Number] @@ -2078,7 +2081,7 @@ describe('model: findOneAndUpdate:', function() { assert.deepEqual(doc.toObject().grades, [95, 100, 90]); }); - it('throws error if filter is not an object (gh-13264)', async function() { + it('throws error if filter is not an object (gh-13264)', async function () { const schema = new Schema({ name: String }); const Model = db.model('Test', schema); @@ -2087,7 +2090,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(err.name, 'ObjectParameterError'); }); - it('handles plus path in projection (gh-13413)', async function() { + it('handles plus path in projection (gh-13413)', async function () { const testSchema = new mongoose.Schema({ name: String, nickName: { @@ -2118,7 +2121,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(res.nickName, 'Quiz'); }); - it('allows setting paths with dots in non-strict paths (gh-13434) (gh-10200)', async function() { + it('allows setting paths with dots in non-strict paths (gh-13434) (gh-10200)', async function () { const testSchema = new mongoose.Schema({ name: String, info: Object @@ -2139,7 +2142,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(doc.info['second.name'], 'Quiz'); assert.equal(doc.info2['second.name'], 'Quiz'); }); - it('supports the `includeResultMetadata` option (gh-13539)', async function() { + it('supports the `includeResultMetadata` option (gh-13539)', async function () { const testSchema = new mongoose.Schema({ name: String }); @@ -2164,7 +2167,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(data.value.name, 'Test'); }); - it('successfully runs findOneAndUpdate with no update and versionKey set to false (gh-13783)', async function() { + it('successfully runs findOneAndUpdate with no update and versionKey set to false (gh-13783)', async function () { const exampleSchema = new mongoose.Schema({ name: String }, { versionKey: false }); @@ -2180,7 +2183,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(document.name, 'test'); }); - it('skips adding defaults to filter when passing empty update (gh-13962)', async function() { + it('skips adding defaults to filter when passing empty update (gh-13962)', async function () { const schema = new Schema({ myField: Number, defaultField: { type: String, default: 'default' } @@ -2197,7 +2200,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(updated.defaultField, 'some non-default value'); }); - it('sets CastError path to full path (gh-14114)', async function() { + it('sets CastError path to full path (gh-14114)', async function () { const testSchema = new mongoose.Schema({ id: mongoose.Schema.Types.ObjectId, name: String, From e7377b0f2694a86b01524856fd8fca32824e5592 Mon Sep 17 00:00:00 2001 From: shash-hq Date: Wed, 17 Dec 2025 22:51:00 +0530 Subject: [PATCH 122/133] style: revert extraneous whitespace changes --- test/model.findOneAndUpdate.test.js | 246 ++++++++++++++-------------- 1 file changed, 123 insertions(+), 123 deletions(-) diff --git a/test/model.findOneAndUpdate.test.js b/test/model.findOneAndUpdate.test.js index beb7dca7443..b7b69b3c23d 100644 --- a/test/model.findOneAndUpdate.test.js +++ b/test/model.findOneAndUpdate.test.js @@ -20,16 +20,16 @@ const isEqualWith = require('lodash.isequalwith'); const util = require('./util'); const uuid = require('uuid'); -describe('model: findOneAndUpdate:', function () { +describe('model: findOneAndUpdate:', function() { let Comments; let BlogPost; let db; - before(function () { + before(function() { db = start(); }); - after(async function () { + after(async function() { await db.close(); }); @@ -37,7 +37,7 @@ describe('model: findOneAndUpdate:', function () { afterEach(() => util.clearTestData(db)); afterEach(() => require('./util').stopRemainingOps(db)); - beforeEach(function () { + beforeEach(function() { Comments = new Schema(); Comments.add({ @@ -64,27 +64,27 @@ describe('model: findOneAndUpdate:', function () { }); BlogPost.virtual('titleWithAuthor') - .get(function () { + .get(function() { return this.get('title') + ' by ' + this.get('author'); }) - .set(function (val) { + .set(function(val) { const split = val.split(' by '); this.set('title', split[0]); this.set('author', split[1]); }); - BlogPost.method('cool', function () { + BlogPost.method('cool', function() { return this; }); - BlogPost.static('woot', function () { + BlogPost.static('woot', function() { return this; }); BlogPost = db.model('BlogPost', BlogPost); }); - it('returns the edited document', async function () { + it('returns the edited document', async function() { const M = BlogPost; const title = 'Tobi ' + random(); const author = 'Brian ' + random(); @@ -156,13 +156,13 @@ describe('model: findOneAndUpdate:', function () { assert.ok(up.comments[1]._id instanceof DocumentObjectId); }); - describe('will correctly', function () { + describe('will correctly', function() { let ItemParentModel, ItemChildModel; - beforeEach(function () { + beforeEach(function() { const itemSpec = new Schema({ item_id: { - type: ObjectId, required: true, default: function () { + type: ObjectId, required: true, default: function() { return new DocumentObjectId(); } }, @@ -179,7 +179,7 @@ describe('model: findOneAndUpdate:', function () { ItemChildModel = db.model('Test2', itemSpec); }); - it('update subdocument in array item', async function () { + it('update subdocument in array item', async function() { const item1 = new ItemChildModel({ address: { street: 'times square', @@ -215,7 +215,7 @@ describe('model: findOneAndUpdate:', function () { }); }); - it('returns the original document', async function () { + it('returns the original document', async function() { const M = BlogPost; const title = 'Tobi ' + random(); const author = 'Brian ' + random(); @@ -267,7 +267,7 @@ describe('model: findOneAndUpdate:', function () { assert.ok(up.comments[1]._id instanceof DocumentObjectId); }); - it('allows upserting', async function () { + it('allows upserting', async function() { const M = BlogPost; const title = 'Tobi ' + random(); const author = 'Brian ' + random(); @@ -310,7 +310,7 @@ describe('model: findOneAndUpdate:', function () { assert.strictEqual(0, up.owners.length); }); - it('options/conditions/doc are merged when no callback is passed', function (done) { + it('options/conditions/doc are merged when no callback is passed', function(done) { const M = BlogPost; const now = new Date(); let query; @@ -355,7 +355,7 @@ describe('model: findOneAndUpdate:', function () { done(); }); - it('updates numbers atomically', async function () { + it('updates numbers atomically', async function() { const post = new BlogPost(); post.set('meta.visitors', 5); @@ -369,7 +369,7 @@ describe('model: findOneAndUpdate:', function () { assert.equal(doc.get('meta.visitors'), 9); }); - it('honors strict schemas', async function () { + it('honors strict schemas', async function() { const S = db.model('Test', Schema({ name: String }, { strict: true })); const s = new S({ name: 'orange crush' }); @@ -395,7 +395,7 @@ describe('model: findOneAndUpdate:', function () { assert.equal(doc3.name, 'orange crush'); }); - it('returns errors with strict:throw schemas', async function () { + it('returns errors with strict:throw schemas', async function() { const S = db.model('Test', Schema({ name: String }, { strict: 'throw' })); const s = new S({ name: 'orange crush' }); @@ -415,7 +415,7 @@ describe('model: findOneAndUpdate:', function () { assert.ok(/not in schema/.test(err2)); }); - it('returns the original document', async function () { + it('returns the original document', async function() { const M = BlogPost; const title = 'Tobi ' + random(); const author = 'Brian ' + random(); @@ -470,7 +470,7 @@ describe('model: findOneAndUpdate:', function () { assert.ok(up.comments[1]._id instanceof DocumentObjectId); }); - it('options/conditions/doc are merged when no callback is passed', function () { + it('options/conditions/doc are merged when no callback is passed', function() { const M = BlogPost; const _id = new DocumentObjectId(); @@ -499,7 +499,7 @@ describe('model: findOneAndUpdate:', function () { assert.strictEqual(undefined, query._conditions._id); }); - it('supports v3 select string syntax', function () { + it('supports v3 select string syntax', function() { const M = BlogPost; const _id = new DocumentObjectId(); @@ -517,7 +517,7 @@ describe('model: findOneAndUpdate:', function () { assert.strictEqual(0, query._fields.title); }); - it('supports v3 select object syntax', function () { + it('supports v3 select object syntax', function() { const M = BlogPost; const _id = new DocumentObjectId(); @@ -533,7 +533,7 @@ describe('model: findOneAndUpdate:', function () { assert.strictEqual(0, query._fields.title); }); - it('supports v3 sort string syntax', async function () { + it('supports v3 sort string syntax', async function() { const M = BlogPost; const now = new Date(); @@ -563,7 +563,7 @@ describe('model: findOneAndUpdate:', function () { assert.equal(doc.meta.visitors, 10); }); - it('supports v3 sort object syntax', function (done) { + it('supports v3 sort object syntax', function(done) { const M = BlogPost; const _id = new DocumentObjectId(); @@ -583,7 +583,7 @@ describe('model: findOneAndUpdate:', function () { done(); }); - it('supports $elemMatch with $in (gh-1091 gh-1100)', async function () { + it('supports $elemMatch with $in (gh-1091 gh-1100)', async function() { const postSchema = new Schema({ ids: [{ type: Schema.ObjectId }], title: String @@ -608,7 +608,7 @@ describe('model: findOneAndUpdate:', function () { assert.equal(found.ids[0].toString(), _id2.toString()); }); - it('supports population (gh-1395)', async function () { + it('supports population (gh-1395)', async function() { const M = db.model('Test1', { name: String }); const N = db.model('Test2', { a: { type: Schema.ObjectId, ref: 'Test1' }, i: Number }); @@ -624,7 +624,7 @@ describe('model: findOneAndUpdate:', function () { assert.equal('i am an A', doc.a.name); }); - it('returns null when doing an upsert & new=false gh-1533', async function () { + it('returns null when doing an upsert & new=false gh-1533', async function() { const thingSchema = new Schema({ _id: String, flag: { @@ -644,7 +644,7 @@ describe('model: findOneAndUpdate:', function () { assert.equal(thing2.flag, false); }); - it('return hydrated document (gh-7734 gh-7735)', async function () { + it('return hydrated document (gh-7734 gh-7735)', async function() { const fruitSchema = new Schema({ name: { type: String } }); @@ -662,7 +662,7 @@ describe('model: findOneAndUpdate:', function () { assert.ok(fruit instanceof mongoose.Document); }); - it('return includeResultMetadata when doing an upsert & new=false gh-7770', async function () { + it('return includeResultMetadata when doing an upsert & new=false gh-7770', async function() { const thingSchema = new Schema({ _id: String, flag: { @@ -684,7 +684,7 @@ describe('model: findOneAndUpdate:', function () { }); - it('allows properties to be set to null gh-1643', async function () { + it('allows properties to be set to null gh-1643', async function() { const testSchema = new Schema({ name: [String] }); @@ -697,7 +697,7 @@ describe('model: findOneAndUpdate:', function () { assert.equal(null, doc.name); }); - it('can do various deep equal checks (lodash.isEqual, util.isDeepStrictEqual, lodash.isEqualWith, assert.deepEqual, utils.deepEqual) on object id after findOneAndUpdate (gh-2070)', async function () { + it('can do various deep equal checks (lodash.isEqual, util.isDeepStrictEqual, lodash.isEqualWith, assert.deepEqual, utils.deepEqual) on object id after findOneAndUpdate (gh-2070)', async function() { const userSchema = new Schema({ name: String, contacts: [{ @@ -744,7 +744,7 @@ describe('model: findOneAndUpdate:', function () { } }); - it('adds __v on upsert (gh-2122) (gh-4505)', async function () { + it('adds __v on upsert (gh-2122) (gh-4505)', async function() { const accountSchema = new Schema({ name: String }); @@ -766,7 +766,7 @@ describe('model: findOneAndUpdate:', function () { assert.equal(doc2.__v, 0); }); - it('doesn\'t add __v on upsert if `$set` (gh-4505) (gh-5973)', function () { + it('doesn\'t add __v on upsert if `$set` (gh-4505) (gh-5973)', function() { const accountSchema = new Schema({ name: String }); @@ -780,7 +780,7 @@ describe('model: findOneAndUpdate:', function () { then(doc => assert.strictEqual(doc.__v, 1)); }); - it('doesn\'t add __v on upsert if `$set` with `update()` (gh-5973)', function () { + it('doesn\'t add __v on upsert if `$set` with `update()` (gh-5973)', function() { const accountSchema = new Schema({ name: String }); @@ -794,7 +794,7 @@ describe('model: findOneAndUpdate:', function () { then(doc => assert.strictEqual(doc.__v, 1)); }); - it('works with nested schemas and $pull+$or (gh-1932)', async function () { + it('works with nested schemas and $pull+$or (gh-1932)', async function() { const TickSchema = new Schema({ name: String }); const TestSchema = new Schema({ a: Number, b: Number, ticks: [TickSchema] }); @@ -807,7 +807,7 @@ describe('model: findOneAndUpdate:', function () { assert.equal(doc.ticks[0].name, 'coffee'); }); - it('accepts undefined', async function () { + it('accepts undefined', async function() { const s = new Schema({ time: Date, base: String @@ -818,7 +818,7 @@ describe('model: findOneAndUpdate:', function () { await Breakfast.findOneAndUpdate({}, { time: undefined, base: undefined }, {}); }); - it('cast errors for empty objects as object ids (gh-2732)', async function () { + it('cast errors for empty objects as object ids (gh-2732)', async function() { const s = new Schema({ base: ObjectId }); @@ -833,7 +833,7 @@ describe('model: findOneAndUpdate:', function () { assert.equal(err.name, 'CastError'); }); - it('cast errors for empty objects as object ids (gh-2732)', async function () { + it('cast errors for empty objects as object ids (gh-2732)', async function() { const s = new Schema({ base: ObjectId }); @@ -850,20 +850,20 @@ describe('model: findOneAndUpdate:', function () { } }); - describe('middleware', function () { - it('works', async function () { + describe('middleware', function() { + it('works', async function() { const s = new Schema({ topping: { type: String, default: 'bacon' }, base: String }); let preCount = 0; - s.pre('findOneAndUpdate', function () { + s.pre('findOneAndUpdate', function() { ++preCount; }); let postCount = 0; - s.post('findOneAndUpdate', function () { + s.post('findOneAndUpdate', function() { ++postCount; }); @@ -874,19 +874,19 @@ describe('model: findOneAndUpdate:', function () { assert.equal(postCount, 1); }); - it('works with exec()', async function () { + it('works with exec()', async function() { const s = new Schema({ topping: { type: String, default: 'bacon' }, base: String }); let preCount = 0; - s.pre('findOneAndUpdate', function () { + s.pre('findOneAndUpdate', function() { ++preCount; }); let postCount = 0; - s.post('findOneAndUpdate', function () { + s.post('findOneAndUpdate', function() { ++postCount; }); @@ -898,8 +898,8 @@ describe('model: findOneAndUpdate:', function () { }); }); - describe('validators (gh-860)', function () { - it('applies defaults on upsert', async function () { + describe('validators (gh-860)', function() { + it('applies defaults on upsert', async function() { const s = new Schema({ topping: { type: String, default: 'bacon' }, base: String @@ -919,7 +919,7 @@ describe('model: findOneAndUpdate:', function () { assert.equal(1, count); }); - it('doesnt set default on upsert if query sets it', async function () { + it('doesnt set default on upsert if query sets it', async function() { const s = new Schema({ topping: { type: String, default: 'bacon' }, numEggs: { type: Number, default: 3 }, @@ -938,7 +938,7 @@ describe('model: findOneAndUpdate:', function () { assert.equal(breakfast.numEggs, 4); }); - it('properly sets default on upsert if query wont set it', async function () { + it('properly sets default on upsert if query wont set it', async function() { const s = new Schema({ topping: { type: String, default: 'bacon' }, base: String @@ -958,7 +958,7 @@ describe('model: findOneAndUpdate:', function () { assert.equal(1, count); }); - it('skips setting defaults within maps (gh-7909)', async function () { + it('skips setting defaults within maps (gh-7909)', async function() { const socialMediaHandleSchema = Schema({ links: [String] }); const profileSchema = Schema({ username: String, @@ -977,17 +977,17 @@ describe('model: findOneAndUpdate:', function () { assert.equal(doc.socialMediaHandles, undefined); }); - it('runs validators if theyre set', async function () { + it('runs validators if theyre set', async function() { const s = new Schema({ topping: { type: String, - validate: function () { + validate: function() { return false; } }, base: { type: String, - validate: function () { + validate: function() { return true; } } @@ -1011,11 +1011,11 @@ describe('model: findOneAndUpdate:', function () { assert.equal(error.errors.topping.message, 'Validator failed for path `topping` with value `bacon`'); }); - it('validators handle $unset and $setOnInsert', async function () { + it('validators handle $unset and $setOnInsert', async function() { const s = new Schema({ steak: { type: String, required: true }, eggs: { - type: String, validate: function () { + type: String, validate: function() { return false; } } @@ -1037,7 +1037,7 @@ describe('model: findOneAndUpdate:', function () { assert.equal(error.errors.steak.message, 'Path `steak` is required.'); }); - it('min/max, enum, and regex built-in validators work', async function () { + it('min/max, enum, and regex built-in validators work', async function() { const s = new Schema({ steak: { type: String, enum: ['ribeye', 'sirloin'] }, eggs: { type: Number, min: 4, max: 6 }, @@ -1075,7 +1075,7 @@ describe('model: findOneAndUpdate:', function () { assert.equal(error.errors.bacon.message, 'Path `bacon` is invalid (none).'); }); - it('multiple validation errors', async function () { + it('multiple validation errors', async function() { const s = new Schema({ steak: { type: String, enum: ['ribeye', 'sirloin'] }, eggs: { type: Number, min: 4, max: 6 }, @@ -1095,7 +1095,7 @@ describe('model: findOneAndUpdate:', function () { assert.ok(Object.keys(error.errors).indexOf('eggs') !== -1); }); - it('validators ignore $inc', async function () { + it('validators ignore $inc', async function() { const s = new Schema({ steak: { type: String, required: true }, eggs: { type: Number, min: 4 } @@ -1111,7 +1111,7 @@ describe('model: findOneAndUpdate:', function () { assert.equal(breakfast.eggs, 1); }); - it('validators ignore paths underneath mixed (gh-8659)', function () { + it('validators ignore paths underneath mixed (gh-8659)', function() { let called = 0; const s = new Schema({ n: { @@ -1126,7 +1126,7 @@ describe('model: findOneAndUpdate:', function () { then(() => assert.equal(called, 0)); }); - it('should work with arrays (gh-3035)', async function () { + it('should work with arrays (gh-3035)', async function() { const testSchema = new mongoose.Schema({ id: String, name: String, @@ -1142,7 +1142,7 @@ describe('model: findOneAndUpdate:', function () { await TestModel.findOneAndUpdate({ id: '1' }, { $set: { name: 'Joe' } }, { upsert: true }); }); - it('should allow null values in query (gh-3135)', async function () { + it('should allow null values in query (gh-3135)', async function() { const testSchema = new mongoose.Schema({ id: String, blob: ObjectId, @@ -1154,7 +1154,7 @@ describe('model: findOneAndUpdate:', function () { await TestModel.findOneAndUpdate({ id: '1', blob: null }, { $set: { status: 'inactive' } }, { upsert: true }); }); - it('should work with array documents (gh-3034)', async function () { + it('should work with array documents (gh-3034)', async function() { const testSchema = new mongoose.Schema({ id: String, name: String, @@ -1172,7 +1172,7 @@ describe('model: findOneAndUpdate:', function () { await TestModel.findOneAndUpdate({ id: '1' }, { $set: { name: 'Joe' } }, { upsert: true }); }); - it('handles setting array (gh-3107)', async function () { + it('handles setting array (gh-3107)', async function() { const testSchema = new mongoose.Schema({ name: String, a: [{ @@ -1191,7 +1191,7 @@ describe('model: findOneAndUpdate:', function () { assert.equal(doc.b[0], 2); }); - it('handles nested cast errors (gh-3468)', async function () { + it('handles nested cast errors (gh-3468)', async function() { const recordSchema = new mongoose.Schema({ kind: String, amount: Number @@ -1223,7 +1223,7 @@ describe('model: findOneAndUpdate:', function () { } }); - it('cast errors with nested schemas (gh-3580)', async function () { + it('cast errors with nested schemas (gh-3580)', async function() { const nested = new Schema({ num: Number }); const s = new Schema({ nested: nested }); @@ -1234,7 +1234,7 @@ describe('model: findOneAndUpdate:', function () { assert.ok(error); }); - it('pull with nested schemas (gh-3616)', async function () { + it('pull with nested schemas (gh-3616)', async function() { const nested = new Schema({ arr: [{ num: Number }] }); const s = new Schema({ nested: nested }); @@ -1247,7 +1247,7 @@ describe('model: findOneAndUpdate:', function () { assert.equal(doc.nested.arr.length, 0); }); - it('setting nested schema (gh-3889)', async function () { + it('setting nested schema (gh-3889)', async function() { const nested = new Schema({ test: String }); const s = new Schema({ nested: nested }); const MyModel = db.model('Test', s); @@ -1258,8 +1258,8 @@ describe('model: findOneAndUpdate:', function () { }); }); - describe('bug fixes', function () { - it('passes raw result if includeResultMetadata specified (gh-4925)', async function () { + describe('bug fixes', function() { + it('passes raw result if includeResultMetadata specified (gh-4925)', async function() { const testSchema = new mongoose.Schema({ test: String }); @@ -1276,7 +1276,7 @@ describe('model: findOneAndUpdate:', function () { assert.equal(res.lastErrorObject.n, 1); }); - it('handles setting single embedded docs to null (gh-4281)', async function () { + it('handles setting single embedded docs to null (gh-4281)', async function() { const foodSchema = new mongoose.Schema({ name: { type: String, default: 'Bacon' } }); @@ -1296,7 +1296,7 @@ describe('model: findOneAndUpdate:', function () { assert.equal(doc.main, null); }); - it('custom validator on mixed field (gh-4305)', async function () { + it('custom validator on mixed field (gh-4305)', async function() { let called = 0; const boardSchema = new Schema({ @@ -1308,7 +1308,7 @@ describe('model: findOneAndUpdate:', function () { type: Schema.Types.Mixed, required: true, validate: { - validator: function () { + validator: function() { ++called; return true; }, @@ -1363,7 +1363,7 @@ describe('model: findOneAndUpdate:', function () { ); }); - it('projection option as alias for fields (gh-4315)', async function () { + it('projection option as alias for fields (gh-4315)', async function() { const TestSchema = new Schema({ test1: String, test2: String @@ -1376,7 +1376,7 @@ describe('model: findOneAndUpdate:', function () { assert.equal(doc.test1, 'a'); }); - it('handles upserting a non-existing field (gh-4757)', async function () { + it('handles upserting a non-existing field (gh-4757)', async function() { const modelSchema = new Schema({ field: Number }, { strict: 'throw' }); const Model = db.model('Test', modelSchema); @@ -1392,7 +1392,7 @@ describe('model: findOneAndUpdate:', function () { } }); - it('strict option (gh-5108)', async function () { + it('strict option (gh-5108)', async function() { const modelSchema = new Schema({ field: Number }, { strict: 'throw' }); const Model = db.model('Test', modelSchema); @@ -1405,7 +1405,7 @@ describe('model: findOneAndUpdate:', function () { assert.equal(doc.get('otherField'), 3); }); - it('correct key order (gh-6484)', function () { + it('correct key order (gh-6484)', function() { const modelSchema = new Schema({ nested: { field1: Number, field2: Number } }); @@ -1413,19 +1413,19 @@ describe('model: findOneAndUpdate:', function () { const Model = db.model('Test', modelSchema); const opts = { upsert: true, new: true }; return Model.findOneAndUpdate({}, { nested: { field1: 1, field2: 2 } }, opts).exec(). - then(function () { + then(function() { return Model.collection.findOne(); }). - then(function (doc) { + then(function(doc) { // Make sure order is correct assert.deepEqual(Object.keys(doc.nested), ['field1', 'field2']); }); }); - it('should not apply schema transforms (gh-4574)', function (done) { + it('should not apply schema transforms (gh-4574)', function(done) { const options = { toObject: { - transform: function () { + transform: function() { assert.ok(false, 'should not call transform'); } } @@ -1443,17 +1443,17 @@ describe('model: findOneAndUpdate:', function () { const Collection = db.model('Test', CollectionSchema); Collection.create({ field2: { arrayField: [] } }). - then(function (doc) { + then(function(doc) { return Collection.findByIdAndUpdate(doc._id, { $push: { 'field2.arrayField': { test: 'test' } } }, { new: true }); }). - then(function () { + then(function() { done(); }); }); - it('update using $ (gh-5628)', async function () { + it('update using $ (gh-5628)', async function() { const schema = new mongoose.Schema({ elems: [String] }); @@ -1472,7 +1472,7 @@ describe('model: findOneAndUpdate:', function () { assert.deepEqual(updatedDoc.elems, ['c', 'b']); }); - it('projection with $elemMatch (gh-5661)', async function () { + it('projection with $elemMatch (gh-5661)', async function() { const schema = new mongoose.Schema({ name: { type: String, default: 'test' }, arr: [{ tag: String }] @@ -1493,7 +1493,7 @@ describe('model: findOneAndUpdate:', function () { assert.equal(doc2.arr[0].tag, 't1'); }); - it('multi cast error (gh-5609)', async function () { + it('multi cast error (gh-5609)', async function() { const schema = new mongoose.Schema({ num1: Number, num2: Number @@ -1511,7 +1511,7 @@ describe('model: findOneAndUpdate:', function () { assert.equal(error.errors['num2'].name, 'CastError'); }); - it('update validators with pushing null (gh-5710)', async function () { + it('update validators with pushing null (gh-5710)', async function() { const schema = new mongoose.Schema({ arr: [String] }); @@ -1523,14 +1523,14 @@ describe('model: findOneAndUpdate:', function () { await Model.findOneAndUpdate({}, update, options); }); - it('only calls setters once (gh-6203)', async function () { + it('only calls setters once (gh-6203)', async function() { const calls = []; const userSchema = new mongoose.Schema({ name: String, foo: { type: String, - set: function (val) { + set: function(val) { calls.push(val); return val + val; } @@ -1543,14 +1543,14 @@ describe('model: findOneAndUpdate:', function () { assert.deepEqual(calls, ['123']); }); - it('only calls setters once (gh-6203)', async function () { + it('only calls setters once (gh-6203)', async function() { const calls = []; const userSchema = new mongoose.Schema({ name: String, foo: { type: String, - set: function (val) { + set: function(val) { calls.push(val); return val + val; } @@ -1563,7 +1563,7 @@ describe('model: findOneAndUpdate:', function () { assert.deepEqual(calls, ['123']); }); - it('update validators with pull + $in (gh-6240)', async function () { + it('update validators with pull + $in (gh-6240)', async function() { const highlightSchema = new mongoose.Schema({ _id: { type: String, @@ -1619,7 +1619,7 @@ describe('model: findOneAndUpdate:', function () { assert.equal(res.highlights.length, 0); }); - it('avoids edge case with middleware cloning buffers (gh-5702)', async function () { + it('avoids edge case with middleware cloning buffers (gh-5702)', async function() { function toUUID(string) { if (!string) { return null; @@ -1652,7 +1652,7 @@ describe('model: findOneAndUpdate:', function () { }] }, { collection: 'users' }); - UserSchema.pre('findOneAndUpdate', function () { + UserSchema.pre('findOneAndUpdate', function() { this.updateOne({}, { $set: { lastUpdate: new Date() } }); }); @@ -1674,7 +1674,7 @@ describe('model: findOneAndUpdate:', function () { assert.equal(updatedUser.friends[0].status, 'Active'); }); - it('setting subtype when saving (gh-5551)', async function () { + it('setting subtype when saving (gh-5551)', async function() { const uuid = require('uuid'); function toUUID(string) { if (!string) { @@ -1710,7 +1710,7 @@ describe('model: findOneAndUpdate:', function () { assert.equal(result.foo.sub_type, 4); }); - it('properly handles casting nested objects in update (gh-4724)', function (done) { + it('properly handles casting nested objects in update (gh-4724)', function(done) { const locationSchema = new Schema({ _id: false, location: { @@ -1732,7 +1732,7 @@ describe('model: findOneAndUpdate:', function () { }); t.save(). - then(function (t) { + then(function(t) { return T.findByIdAndUpdate(t._id, { $set: { 'locations.0': { @@ -1741,18 +1741,18 @@ describe('model: findOneAndUpdate:', function () { } }, { new: true }); }). - then(function (res) { + then(function(res) { assert.equal(res.locations[0].location.coordinates[0], -123); done(); }). catch(done); }); - it('doesnt do double validation on document arrays during updates (gh-4440)', async function () { + it('doesnt do double validation on document arrays during updates (gh-4440)', async function() { const A = new Schema({ str: String }); let B = new Schema({ a: [A] }); let validateCalls = 0; - B.path('a').validate(function (val) { + B.path('a').validate(function(val) { ++validateCalls; assert(Array.isArray(val)); return true; @@ -1769,7 +1769,7 @@ describe('model: findOneAndUpdate:', function () { assert.equal(validateCalls, 1); }); - it('runs setters on array elements (gh-7679)', function () { + it('runs setters on array elements (gh-7679)', function() { const bookSchema = new Schema({ genres: { type: [{ @@ -1786,7 +1786,7 @@ describe('model: findOneAndUpdate:', function () { then(doc => assert.equal(doc.genres[0], 'sci-fi')); }); - it('avoid calling $pull in doc array (gh-6971) (gh-6889)', function () { + it('avoid calling $pull in doc array (gh-6971) (gh-6889)', function() { const schema = new Schema({ arr: { type: [{ x: String }], @@ -1802,7 +1802,7 @@ describe('model: findOneAndUpdate:', function () { return Model.findOneAndUpdate({}, { $pull: { arr: { x: 'three' } } }, opts); }); - it('$pull with `required` and runValidators (gh-6972)', async function () { + it('$pull with `required` and runValidators (gh-6972)', async function() { const schema = new mongoose.Schema({ someArray: { type: [{ @@ -1824,7 +1824,7 @@ describe('model: findOneAndUpdate:', function () { }); }); - it('with versionKey in top-level and a `$` key (gh-7003)', async function () { + it('with versionKey in top-level and a `$` key (gh-7003)', async function() { const schema = new Schema({ name: String }); const Model = db.model('Test', schema); @@ -1840,7 +1840,7 @@ describe('model: findOneAndUpdate:', function () { assert.ok(!doc.name); }); - it('empty update with timestamps (gh-7041)', async function () { + it('empty update with timestamps (gh-7041)', async function() { const schema = new Schema({ name: String }, { timestamps: true }); const Model = db.model('Test', schema); @@ -1851,7 +1851,7 @@ describe('model: findOneAndUpdate:', function () { assert.equal(doc.name, 'test'); }); - it('skipping updatedAt and createdAt (gh-3934)', async function () { + it('skipping updatedAt and createdAt (gh-3934)', async function() { const schema = new Schema({ name: String }, { timestamps: true }); const Model = db.model('Test', schema); @@ -1871,7 +1871,7 @@ describe('model: findOneAndUpdate:', function () { assert.equal(doc.updatedAt.valueOf(), start.valueOf()); }); - it('runs lowercase on $addToSet, $push, etc (gh-4185)', async function () { + it('runs lowercase on $addToSet, $push, etc (gh-4185)', async function() { const Cat = db.model('Test', { _id: String, myArr: { type: [{ type: String, lowercase: true }], default: undefined } @@ -1885,7 +1885,7 @@ describe('model: findOneAndUpdate:', function () { assert.equal(res.myArr[0], 'case sensitive'); }); - it('returnOriginal (gh-7846)', async function () { + it('returnOriginal (gh-7846)', async function() { const Cat = db.model('Cat', { name: String }); @@ -1898,7 +1898,7 @@ describe('model: findOneAndUpdate:', function () { assert.equal(res.name, 'test2'); }); - it('updating embedded discriminator with discriminator key in update (gh-8378)', async function () { + it('updating embedded discriminator with discriminator key in update (gh-8378)', async function() { const shapeSchema = Schema({ name: String }, { discriminatorKey: 'kind' }); const schema = Schema({ shape: shapeSchema }); @@ -1929,7 +1929,7 @@ describe('model: findOneAndUpdate:', function () { assert.equal(doc.shape.radius, 10); }); - it('setDefaultsOnInsert with doubly nested subdocs (gh-8392)', function () { + it('setDefaultsOnInsert with doubly nested subdocs (gh-8392)', function() { const nestedSchema = Schema({ name: String }); const Model = db.model('Test', Schema({ L1: Schema({ @@ -1946,7 +1946,7 @@ describe('model: findOneAndUpdate:', function () { then(doc => assert.equal(doc.L1.L2.name, 'foo')); }); - it('calls setters on mixed type (gh-8444)', async function () { + it('calls setters on mixed type (gh-8444)', async function() { const userSchema = new Schema({ jobCategory: { type: Object, @@ -1979,7 +1979,7 @@ describe('model: findOneAndUpdate:', function () { assert.equal(doc.jobCategory.value, 'from setter 2'); }); - it('returnDocument should work (gh-10321)', async function () { + it('returnDocument should work (gh-10321)', async function() { const testSchema = Schema({ a: Number }); const Model = db.model('Test', testSchema); @@ -1990,7 +1990,7 @@ describe('model: findOneAndUpdate:', function () { assert.equal(doc.a, 2); }); - it('supports overwriting nested map paths (gh-10485)', async function () { + it('supports overwriting nested map paths (gh-10485)', async function() { const child = new mongoose.Schema({ vals: { type: mongoose.Schema.Types.Map, @@ -2015,7 +2015,7 @@ describe('model: findOneAndUpdate:', function () { assert.deepEqual(res.toObject().children[0].vals, new Map([['telegram', 'hello']])); }); - it('supports $set on elements of map of subdocuments (gh-10720)', async function () { + it('supports $set on elements of map of subdocuments (gh-10720)', async function() { const parentSchema = new mongoose.Schema({ data: new mongoose.Schema({ children: { @@ -2039,7 +2039,7 @@ describe('model: findOneAndUpdate:', function () { assert.strictEqual(res.data.children.get('kenny').age, 1); }); - it('handles validating deeply nested subdocuments (gh-11394)', async function () { + it('handles validating deeply nested subdocuments (gh-11394)', async function() { const userSchema = new Schema({ myId: Number, address: { @@ -2061,7 +2061,7 @@ describe('model: findOneAndUpdate:', function () { assert.ifError(err); }); - it('casts array filters (gh-13219)', async function () { + it('casts array filters (gh-13219)', async function() { const MyModel = db.model('Test', new Schema({ _id: Number, grades: [Number] @@ -2081,7 +2081,7 @@ describe('model: findOneAndUpdate:', function () { assert.deepEqual(doc.toObject().grades, [95, 100, 90]); }); - it('throws error if filter is not an object (gh-13264)', async function () { + it('throws error if filter is not an object (gh-13264)', async function() { const schema = new Schema({ name: String }); const Model = db.model('Test', schema); @@ -2090,7 +2090,7 @@ describe('model: findOneAndUpdate:', function () { assert.equal(err.name, 'ObjectParameterError'); }); - it('handles plus path in projection (gh-13413)', async function () { + it('handles plus path in projection (gh-13413)', async function() { const testSchema = new mongoose.Schema({ name: String, nickName: { @@ -2121,7 +2121,7 @@ describe('model: findOneAndUpdate:', function () { assert.equal(res.nickName, 'Quiz'); }); - it('allows setting paths with dots in non-strict paths (gh-13434) (gh-10200)', async function () { + it('allows setting paths with dots in non-strict paths (gh-13434) (gh-10200)', async function() { const testSchema = new mongoose.Schema({ name: String, info: Object @@ -2142,7 +2142,7 @@ describe('model: findOneAndUpdate:', function () { assert.equal(doc.info['second.name'], 'Quiz'); assert.equal(doc.info2['second.name'], 'Quiz'); }); - it('supports the `includeResultMetadata` option (gh-13539)', async function () { + it('supports the `includeResultMetadata` option (gh-13539)', async function() { const testSchema = new mongoose.Schema({ name: String }); @@ -2167,7 +2167,7 @@ describe('model: findOneAndUpdate:', function () { assert.equal(data.value.name, 'Test'); }); - it('successfully runs findOneAndUpdate with no update and versionKey set to false (gh-13783)', async function () { + it('successfully runs findOneAndUpdate with no update and versionKey set to false (gh-13783)', async function() { const exampleSchema = new mongoose.Schema({ name: String }, { versionKey: false }); @@ -2183,7 +2183,7 @@ describe('model: findOneAndUpdate:', function () { assert.equal(document.name, 'test'); }); - it('skips adding defaults to filter when passing empty update (gh-13962)', async function () { + it('skips adding defaults to filter when passing empty update (gh-13962)', async function() { const schema = new Schema({ myField: Number, defaultField: { type: String, default: 'default' } @@ -2200,7 +2200,7 @@ describe('model: findOneAndUpdate:', function () { assert.equal(updated.defaultField, 'some non-default value'); }); - it('sets CastError path to full path (gh-14114)', async function () { + it('sets CastError path to full path (gh-14114)', async function() { const testSchema = new mongoose.Schema({ id: mongoose.Schema.Types.ObjectId, name: String, From 7a2203e1135ffdb0417f8bef246cf3180b5a086e Mon Sep 17 00:00:00 2001 From: Hafez Date: Wed, 17 Dec 2025 19:47:29 +0100 Subject: [PATCH 123/133] fix(document): use bitwise OR to accumulate version mode flags (#15893) * chore: add .worktrees/ to .gitignore * test(document): add tests re #15888 * fix(document): fix version mode flag from being overwritten --- .gitignore | 1 + lib/document.js | 4 +-- test/versioning.test.js | 72 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 2 deletions(-) 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/lib/document.js b/lib/document.js index dfb775fc9bb..98dd86da0c9 100644 --- a/lib/document.js +++ b/lib/document.js @@ -5181,7 +5181,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 @@ -5192,7 +5192,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; } } 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 }; + } + }); }); From 351ac777ec786e48c29c02373c09a75cbb5f877e Mon Sep 17 00:00:00 2001 From: Hafez Date: Wed, 17 Dec 2025 19:47:29 +0100 Subject: [PATCH 124/133] fix(document): use bitwise OR to accumulate version mode flags (#15893) * chore: add .worktrees/ to .gitignore * test(document): add tests re #15888 * fix(document): fix version mode flag from being overwritten --- .gitignore | 1 + lib/document.js | 4 +-- test/versioning.test.js | 72 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 2 deletions(-) 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/lib/document.js b/lib/document.js index 4f8768e4bd0..c2ebf843c90 100644 --- a/lib/document.js +++ b/lib/document.js @@ -5174,7 +5174,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 @@ -5185,7 +5185,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; } } 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 }; + } + }); }); From 47ab06854225fe743ce607429ebfb75d7ed161ef Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 17 Dec 2025 13:55:45 -0500 Subject: [PATCH 125/133] chore: release 9.0.2 --- CHANGELOG.md | 12 ++++++++++++ package.json | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf350ce83e7..fe2fdaa681b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +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) diff --git a/package.json b/package.json index a1c6158b287..44df18c787e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "9.0.1", + "version": "9.0.2", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From d653f838328e8344c99b046edce94955f9e6acc3 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 17 Dec 2025 15:14:11 -0500 Subject: [PATCH 126/133] types: allow calling create() with TRawDocType for better generics support Fix #15902 --- test/types/create.test.ts | 15 ++++++++++++++- types/models.d.ts | 2 ++ 2 files changed, 16 insertions(+), 1 deletion(-) 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/types/models.d.ts b/types/models.d.ts index a8b2e37c289..6c540d36c2e 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -335,6 +335,8 @@ declare module 'mongoose' { /** Creates a new document or documents */ create(): Promise; + create(doc: TRawDocType): 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; From 98efb133615600b745580737a1d085150efa9285 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 17 Dec 2025 15:18:58 -0500 Subject: [PATCH 127/133] allow partial TRawDocType --- types/models.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/types/models.d.ts b/types/models.d.ts index 6c540d36c2e..876620a839a 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -335,8 +335,8 @@ declare module 'mongoose' { /** Creates a new document or documents */ create(): Promise; - create(doc: TRawDocType): Promise; - create(docs: Array): 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; From d96c18328f742150ba69fe9696719d6d684f0ca0 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 17 Dec 2025 16:59:07 -0500 Subject: [PATCH 128/133] fix: when cloning document with subdocuments, make sure the subdocuments parent is the cloned document Fix #15902 --- lib/document.js | 7 +++++-- lib/helpers/clone.js | 2 +- lib/types/arraySubdocument.js | 10 ++++++++++ lib/types/subdocument.js | 10 ++++++++++ test/document.test.js | 4 ++-- 5 files changed, 28 insertions(+), 5 deletions(-) diff --git a/lib/document.js b/lib/document.js index 98dd86da0c9..7ac483a6f4a 100644 --- a/lib/document.js +++ b/lib/document.js @@ -5301,7 +5301,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; @@ -5312,7 +5312,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; 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/types/arraySubdocument.js b/lib/types/arraySubdocument.js index a723bc51fe4..5c0bf9e139c 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 + */ + +Subdocument.prototype.$__setParent = function $__setParent(parent) { + this[documentArrayParent] = parent; +}; + /** * Returns this subdocument's parent array. * 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/test/document.test.js b/test/document.test.js index beb8c890244..32088522935 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -12348,8 +12348,8 @@ 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]); }); From 2cecd43e895dad103a3cf17cfdf0525f4bca6943 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 18 Dec 2025 12:38:54 -0500 Subject: [PATCH 129/133] Update test/document.test.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- test/document.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/document.test.js b/test/document.test.js index ce6fc3c2f1a..5b3d5b36753 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -151,7 +151,7 @@ describe('document', function() { assert.strictEqual(found, null); }); - it('sets $isDeleted to true after successful delete (gh-15878)', async function() { + it('sets $isDeleted to true after successful delete (gh-15858)', async function() { const schema = new Schema({ name: String }); const Product = db.model('Test', schema); From 5941c0ad671a2bad72d8069af6eb7a0899e7c5c7 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 18 Dec 2025 13:06:43 -0500 Subject: [PATCH 130/133] fix(document): when cloning a doc with subdocs, make sure the subdocs parent is the cloned doc (#15904) * fix(document): when cloning a doc with subdocs, make sure the subdocs parent is the cloned doc Fix #15901 * Update lib/types/arraySubdocument.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix(document): handle updating parent of cloned populated docs re: #15901 --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- lib/document.js | 21 +++++++++++-- lib/helpers/clone.js | 2 +- lib/types/arraySubdocument.js | 10 ++++++ lib/types/subdocument.js | 10 ++++++ test/document.test.js | 58 +++++++++++++++++++++++++++++++++-- 5 files changed, 96 insertions(+), 5 deletions(-) diff --git a/lib/document.js b/lib/document.js index 98dd86da0c9..63391b1e94f 100644 --- a/lib/document.js +++ b/lib/document.js @@ -4469,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 * @@ -5301,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; @@ -5312,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; 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/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/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/test/document.test.js b/test/document.test.js index beb8c890244..5a3a9954cd6 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -12348,11 +12348,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 } From 0f9b28de2384e001868dcb7303dfd1ecf37cf060 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 18 Dec 2025 13:07:32 -0500 Subject: [PATCH 131/133] Update lib/model.js Co-authored-by: Hafez --- lib/model.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/model.js b/lib/model.js index caf159181a4..3f6b07d0750 100644 --- a/lib/model.js +++ b/lib/model.js @@ -830,7 +830,7 @@ Model.prototype.deleteOne = function deleteOne(options) { self.constructor._middleware.execPost('deleteOne', self, [self], {}, cb); }); query.transform(function setIsDeleted(result) { - if (result && result.deletedCount != null && result.deletedCount > 0) { + if (result?.deletedCount > 0) { self.$isDeleted(true); } return result; From bca78fe3d992c468b40188e8f82f91da19470905 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 18 Dec 2025 16:56:21 -0500 Subject: [PATCH 132/133] chore: release 8.20.4 --- CHANGELOG.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0f9ee8e856..cd008e34187 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +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) + 8.20.3 / 2025-12-15 =================== * perf: use Object.hasOwn instead of Object#hasOwnProperty #15875 [AbdelrahmanHafez](https://github.com/AbdelrahmanHafez) diff --git a/package.json b/package.json index 147b74bab85..5d6628deefb 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "8.20.3", + "version": "8.20.4", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From 50a58103edf5d5247000f2e354dd1f3a60f4e48a Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 21 Dec 2025 12:09:03 -0500 Subject: [PATCH 133/133] style: fix lint --- test/model.findOneAndUpdate.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/model.findOneAndUpdate.test.js b/test/model.findOneAndUpdate.test.js index b7b69b3c23d..4349eeb5671 100644 --- a/test/model.findOneAndUpdate.test.js +++ b/test/model.findOneAndUpdate.test.js @@ -361,7 +361,7 @@ describe('model: findOneAndUpdate:', function() { await post.save(); - await Promise.all(Array(4).fill(null).map(async () => { + await Promise.all(Array(4).fill(null).map(async() => { await BlogPost.findOneAndUpdate({ _id: post._id }, { $inc: { 'meta.visitors': 1 } }); })); @@ -1341,7 +1341,7 @@ describe('model: findOneAndUpdate:', function() { assert.equal(called, 1); }); - it('single nested doc cast errors (gh-3602)', async () => { + it('single nested doc cast errors (gh-3602)', async() => { const AddressSchema = new Schema({ street: { type: Number