@vandenberghinc/volt 1.1.26 → 1.1.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/backend/dist/cjs/{blacklist.d.ts → backend/src/blacklist.d.ts} +5 -3
- package/backend/dist/cjs/{blacklist.js → backend/src/blacklist.js} +8 -5
- package/backend/dist/cjs/{cli.js → backend/src/cli.js} +29 -47
- package/backend/dist/cjs/backend/src/database/collection.d.ts +1543 -0
- package/backend/dist/cjs/backend/src/database/collection.js +3042 -0
- package/backend/dist/cjs/backend/src/database/database.d.ts +66 -0
- package/backend/dist/cjs/{database → backend/src/database}/database.js +48 -43
- package/backend/dist/cjs/backend/src/database/filters/filters.d.ts +6 -0
- package/backend/dist/cjs/backend/src/database/filters/filters.js +15 -0
- package/backend/dist/cjs/backend/src/database/filters/strict_filter.d.ts +223 -0
- package/backend/dist/cjs/backend/src/database/filters/strict_filter.js +15 -0
- package/backend/dist/cjs/backend/src/database/filters/strict_filter_test.js +443 -0
- package/backend/dist/cjs/backend/src/database/filters/strict_filter_test_v0.js +15 -0
- package/backend/dist/cjs/backend/src/database/filters/strict_filter_v0.d.ts +50 -0
- package/backend/dist/cjs/backend/src/database/filters/strict_filter_v0.js +15 -0
- package/backend/dist/cjs/backend/src/database/filters/strict_filter_v1.d.ts +76 -0
- package/backend/dist/cjs/backend/src/database/filters/strict_filter_v1.js +15 -0
- package/backend/dist/cjs/backend/src/database/filters/strict_filter_v2.d.ts +75 -0
- package/backend/dist/cjs/backend/src/database/filters/strict_filter_v2.js +15 -0
- package/backend/dist/cjs/backend/src/database/filters/strict_filter_v3.d.ts +219 -0
- package/backend/dist/cjs/backend/src/database/filters/strict_filter_v3.js +15 -0
- package/backend/dist/cjs/backend/src/database/filters/strict_update_filter.d.ts +165 -0
- package/backend/dist/cjs/backend/src/database/filters/strict_update_filter.js +15 -0
- package/backend/dist/cjs/backend/src/database/filters/strict_update_filter_test.d.ts +5 -0
- package/backend/dist/cjs/backend/src/database/filters/strict_update_filter_test.js +355 -0
- package/backend/dist/cjs/backend/src/database/flatten.d.ts +75 -0
- package/backend/dist/cjs/{logger.js → backend/src/database/flatten.js} +18 -7
- package/backend/dist/cjs/backend/src/database/flatten_test.js +175 -0
- package/backend/dist/cjs/backend/src/database/quota/quota.d.ts +461 -0
- package/backend/dist/cjs/backend/src/database/quota/quota.js +1014 -0
- package/backend/dist/cjs/backend/src/database/quota/quota_v1.d.ts +534 -0
- package/backend/dist/cjs/backend/src/database/quota/quota_v1.js +1087 -0
- package/backend/dist/cjs/backend/src/database/quota/safe_int.d.ts +293 -0
- package/backend/dist/cjs/backend/src/database/quota/safe_int.js +573 -0
- package/backend/dist/{esm → cjs/backend/src}/endpoint.d.ts +69 -46
- package/backend/dist/cjs/{endpoint.js → backend/src/endpoint.js} +87 -101
- package/backend/dist/cjs/backend/src/errors/index.d.ts +7 -0
- package/backend/dist/cjs/backend/src/errors/index.js +25 -0
- package/backend/dist/{esm/utils.d.ts → cjs/backend/src/errors/internal_external.d.ts} +14 -22
- package/backend/dist/cjs/backend/src/errors/internal_external.js +85 -0
- package/backend/dist/cjs/backend/src/errors/invalid_usage_error.d.ts +38 -0
- package/backend/dist/cjs/{mutex.js → backend/src/errors/invalid_usage_error.js} +20 -37
- package/backend/dist/cjs/backend/src/errors/system_error.d.ts +230 -0
- package/backend/dist/cjs/backend/src/errors/system_error.js +393 -0
- package/backend/dist/cjs/backend/src/events.d.ts +54 -0
- package/backend/dist/cjs/backend/src/events.js +15 -0
- package/backend/dist/cjs/{frontend.js → backend/src/frontend.js} +1 -1
- package/backend/dist/cjs/{image_endpoint.d.ts → backend/src/image_endpoint.d.ts} +16 -1
- package/backend/dist/cjs/{image_endpoint.js → backend/src/image_endpoint.js} +3 -5
- package/backend/dist/cjs/backend/src/logger.d.ts +5 -0
- package/backend/dist/cjs/backend/src/logger.js +15 -0
- package/backend/dist/cjs/backend/src/meta.d.ts +64 -0
- package/backend/dist/cjs/{meta.js → backend/src/meta.js} +9 -12
- package/backend/dist/cjs/backend/src/payments/paddle.d.ts +326 -0
- package/backend/dist/cjs/{payments → backend/src/payments}/paddle.js +377 -327
- package/backend/dist/cjs/backend/src/plugins/browser.d.ts +1 -0
- package/backend/dist/cjs/backend/src/plugins/browser.js +15 -0
- package/backend/dist/cjs/backend/src/plugins/mail/mail.d.ts +248 -0
- package/backend/dist/cjs/backend/src/plugins/mail/mail.js +379 -0
- package/backend/dist/{esm → cjs/backend/src}/plugins/mail/ui.d.ts +23 -0
- package/backend/dist/cjs/backend/src/plugins/pdf.d.ts +1 -0
- package/backend/dist/cjs/backend/src/rate_limit.d.ts +145 -0
- package/backend/dist/cjs/backend/src/rate_limit.js +549 -0
- package/backend/dist/cjs/{route.d.ts → backend/src/route.d.ts} +3 -10
- package/backend/dist/cjs/{route.js → backend/src/route.js} +23 -21
- package/backend/dist/cjs/backend/src/server.d.ts +485 -0
- package/backend/dist/cjs/{server.js → backend/src/server.js} +688 -873
- package/backend/dist/cjs/backend/src/splash_screen.d.ts +80 -0
- package/backend/dist/cjs/{splash_screen.js → backend/src/splash_screen.js} +24 -3
- package/backend/dist/cjs/backend/src/status.d.ts +74 -0
- package/backend/dist/cjs/{status.js → backend/src/status.js} +64 -64
- package/backend/dist/cjs/backend/src/stream.d.ts +376 -0
- package/backend/dist/cjs/{stream.js → backend/src/stream.js} +299 -276
- package/backend/dist/cjs/backend/src/users.d.ts +807 -0
- package/backend/dist/cjs/backend/src/users.js +1971 -0
- package/backend/dist/cjs/backend/src/utils.d.ts +16 -0
- package/backend/dist/cjs/{utils.js → backend/src/utils.js} +14 -77
- package/backend/dist/{esm → cjs/backend/src}/view.d.ts +33 -11
- package/backend/dist/cjs/backend/src/view.js +508 -0
- package/backend/dist/{esm → cjs/backend/src}/volt.d.ts +10 -1
- package/backend/dist/cjs/{volt.js → backend/src/volt.js} +8 -5
- package/backend/dist/cjs/frontend/src/modules/request.d.ts +70 -0
- package/backend/dist/cjs/frontend/src/modules/request.js +99 -0
- package/backend/dist/esm/{blacklist.d.ts → backend/src/blacklist.d.ts} +5 -3
- package/backend/dist/esm/{blacklist.js → backend/src/blacklist.js} +9 -6
- package/backend/dist/esm/{cli.js → backend/src/cli.js} +43 -60
- package/backend/dist/esm/backend/src/database/collection.d.ts +1543 -0
- package/backend/dist/esm/backend/src/database/collection.js +3510 -0
- package/backend/dist/esm/backend/src/database/database.d.ts +66 -0
- package/backend/dist/esm/{database → backend/src/database}/database.js +62 -103
- package/backend/dist/esm/backend/src/database/document.d.ts +1 -0
- package/backend/dist/esm/backend/src/database/document.js +558 -0
- package/backend/dist/esm/backend/src/database/filters/filters.d.ts +6 -0
- package/backend/dist/esm/backend/src/database/filters/filters.js +1 -0
- package/backend/dist/esm/backend/src/database/filters/strict_filter.d.ts +223 -0
- package/backend/dist/esm/backend/src/database/filters/strict_filter.js +3 -0
- package/backend/dist/esm/backend/src/database/filters/strict_filter_test.d.ts +1 -0
- package/backend/dist/esm/backend/src/database/filters/strict_filter_test.js +505 -0
- package/backend/dist/esm/backend/src/database/filters/strict_filter_test_v0.d.ts +1 -0
- package/backend/dist/esm/backend/src/database/filters/strict_filter_test_v0.js +712 -0
- package/backend/dist/esm/backend/src/database/filters/strict_filter_v0.d.ts +50 -0
- package/backend/dist/esm/backend/src/database/filters/strict_filter_v0.js +5 -0
- package/backend/dist/esm/backend/src/database/filters/strict_filter_v1.d.ts +76 -0
- package/backend/dist/esm/backend/src/database/filters/strict_filter_v1.js +44 -0
- package/backend/dist/esm/backend/src/database/filters/strict_filter_v2.d.ts +75 -0
- package/backend/dist/esm/backend/src/database/filters/strict_filter_v2.js +5 -0
- package/backend/dist/esm/backend/src/database/filters/strict_filter_v3.d.ts +219 -0
- package/backend/dist/esm/backend/src/database/filters/strict_filter_v3.js +1 -0
- package/backend/dist/esm/backend/src/database/filters/strict_update_filter.d.ts +165 -0
- package/backend/dist/esm/backend/src/database/filters/strict_update_filter.js +5 -0
- package/backend/dist/esm/backend/src/database/filters/strict_update_filter_test.d.ts +5 -0
- package/backend/dist/esm/backend/src/database/filters/strict_update_filter_test.js +405 -0
- package/backend/dist/esm/backend/src/database/flatten.d.ts +75 -0
- package/backend/dist/esm/backend/src/database/flatten.js +22 -0
- package/backend/dist/esm/backend/src/database/flatten_test.d.ts +1 -0
- package/backend/dist/esm/backend/src/database/flatten_test.js +174 -0
- package/backend/dist/esm/backend/src/database/quota/quota.d.ts +461 -0
- package/backend/dist/esm/backend/src/database/quota/quota.js +1118 -0
- package/backend/dist/esm/backend/src/database/quota/quota_v1.d.ts +534 -0
- package/backend/dist/esm/backend/src/database/quota/quota_v1.js +1242 -0
- package/backend/dist/esm/backend/src/database/quota/safe_int.d.ts +293 -0
- package/backend/dist/esm/backend/src/database/quota/safe_int.js +602 -0
- package/backend/dist/{cjs → esm/backend/src}/endpoint.d.ts +69 -46
- package/backend/dist/esm/{endpoint.js → backend/src/endpoint.js} +136 -127
- package/backend/dist/esm/backend/src/errors/index.d.ts +7 -0
- package/backend/dist/esm/backend/src/errors/index.js +7 -0
- package/backend/dist/{cjs/utils.d.ts → esm/backend/src/errors/internal_external.d.ts} +14 -22
- package/backend/dist/esm/backend/src/errors/internal_external.js +70 -0
- package/backend/dist/esm/backend/src/errors/invalid_usage_error.d.ts +38 -0
- package/backend/dist/esm/backend/src/errors/invalid_usage_error.js +30 -0
- package/backend/dist/esm/backend/src/errors/system_error.d.ts +230 -0
- package/backend/dist/esm/backend/src/errors/system_error.js +402 -0
- package/backend/dist/esm/backend/src/events.d.ts +54 -0
- package/backend/dist/esm/backend/src/events.js +5 -0
- package/backend/dist/esm/{frontend.js → backend/src/frontend.js} +1 -1
- package/backend/dist/esm/{image_endpoint.d.ts → backend/src/image_endpoint.d.ts} +16 -1
- package/backend/dist/esm/{image_endpoint.js → backend/src/image_endpoint.js} +16 -20
- package/backend/dist/esm/backend/src/logger.d.ts +5 -0
- package/backend/dist/esm/backend/src/logger.js +8 -0
- package/backend/dist/esm/backend/src/meta.d.ts +64 -0
- package/backend/dist/esm/{meta.js → backend/src/meta.js} +15 -54
- package/backend/dist/esm/backend/src/payments/paddle.d.ts +326 -0
- package/backend/dist/esm/{payments → backend/src/payments}/paddle.js +417 -452
- package/backend/dist/esm/backend/src/plugins/browser.d.ts +1 -0
- package/backend/dist/esm/backend/src/plugins/browser.js +170 -0
- package/backend/dist/esm/backend/src/plugins/mail/mail.d.ts +248 -0
- package/backend/dist/esm/backend/src/plugins/mail/mail.js +389 -0
- package/backend/dist/{cjs → esm/backend/src}/plugins/mail/ui.d.ts +23 -0
- package/backend/dist/esm/{plugins → backend/src/plugins}/mail/ui.js +3 -6
- package/backend/dist/esm/backend/src/plugins/pdf.d.ts +1 -0
- package/backend/dist/esm/{plugins → backend/src/plugins}/pdf.js +3 -3
- package/backend/dist/esm/backend/src/rate_limit.d.ts +145 -0
- package/backend/dist/esm/backend/src/rate_limit.js +667 -0
- package/backend/dist/esm/{route.d.ts → backend/src/route.d.ts} +3 -10
- package/backend/dist/esm/{route.js → backend/src/route.js} +26 -21
- package/backend/dist/esm/backend/src/server.d.ts +485 -0
- package/backend/dist/esm/{server.js → backend/src/server.js} +891 -1441
- package/backend/dist/esm/backend/src/splash_screen.d.ts +80 -0
- package/backend/dist/esm/{splash_screen.js → backend/src/splash_screen.js} +42 -55
- package/backend/dist/esm/backend/src/status.d.ts +74 -0
- package/backend/dist/esm/backend/src/status.js +199 -0
- package/backend/dist/esm/backend/src/stream.d.ts +376 -0
- package/backend/dist/esm/{stream.js → backend/src/stream.js} +327 -292
- package/backend/dist/esm/backend/src/users.d.ts +809 -0
- package/backend/dist/esm/backend/src/users.js +2140 -0
- package/backend/dist/esm/backend/src/utils.d.ts +16 -0
- package/backend/dist/esm/{utils.js → backend/src/utils.js} +20 -81
- package/backend/dist/{cjs → esm/backend/src}/view.d.ts +33 -11
- package/backend/dist/esm/{view.js → backend/src/view.js} +266 -86
- package/backend/dist/{cjs → esm/backend/src}/volt.d.ts +10 -1
- package/backend/dist/esm/{volt.js → backend/src/volt.js} +7 -4
- package/backend/dist/esm/frontend/src/modules/request.d.ts +70 -0
- package/backend/dist/esm/frontend/src/modules/request.js +117 -0
- package/frontend/dist/backend/src/database/collection.d.ts +1543 -0
- package/frontend/dist/backend/src/database/collection.js +3510 -0
- package/frontend/dist/backend/src/database/database.d.ts +66 -0
- package/frontend/dist/backend/src/database/database.js +196 -0
- package/frontend/dist/backend/src/database/filters/filters.d.ts +6 -0
- package/frontend/dist/backend/src/database/filters/filters.js +1 -0
- package/frontend/dist/backend/src/database/filters/strict_filter.d.ts +223 -0
- package/frontend/dist/backend/src/database/filters/strict_filter.js +3 -0
- package/frontend/dist/backend/src/database/filters/strict_update_filter.d.ts +165 -0
- package/frontend/dist/backend/src/database/filters/strict_update_filter.js +5 -0
- package/frontend/dist/backend/src/database/flatten.d.ts +75 -0
- package/frontend/dist/backend/src/database/flatten.js +22 -0
- package/frontend/dist/backend/src/endpoint.d.ts +204 -0
- package/frontend/dist/backend/src/endpoint.js +570 -0
- package/frontend/dist/backend/src/errors/index.d.ts +7 -0
- package/frontend/dist/backend/src/errors/index.js +7 -0
- package/frontend/dist/backend/src/errors/internal_external.d.ts +38 -0
- package/frontend/dist/backend/src/errors/internal_external.js +70 -0
- package/frontend/dist/backend/src/errors/invalid_usage_error.d.ts +38 -0
- package/frontend/dist/backend/src/errors/invalid_usage_error.js +30 -0
- package/frontend/dist/backend/src/errors/system_error.d.ts +230 -0
- package/frontend/dist/backend/src/errors/system_error.js +402 -0
- package/frontend/dist/backend/src/events.d.ts +54 -0
- package/frontend/dist/backend/src/events.js +5 -0
- package/frontend/dist/backend/src/frontend.d.ts +11 -0
- package/frontend/dist/backend/src/frontend.js +12 -0
- package/frontend/dist/backend/src/image_endpoint.d.ts +39 -0
- package/frontend/dist/backend/src/image_endpoint.js +202 -0
- package/frontend/dist/backend/src/meta.d.ts +64 -0
- package/frontend/dist/backend/src/meta.js +110 -0
- package/frontend/dist/backend/src/payments/paddle.d.ts +326 -0
- package/frontend/dist/backend/src/payments/paddle.js +2256 -0
- package/frontend/dist/backend/src/plugins/mail/mail.d.ts +248 -0
- package/frontend/dist/backend/src/plugins/mail/mail.js +389 -0
- package/{backend/dist/esm/plugins/mail.d.ts → frontend/dist/backend/src/plugins/mail/ui.d.ts} +23 -0
- package/{backend/dist/esm/plugins/mail.js → frontend/dist/backend/src/plugins/mail/ui.js} +3 -6
- package/frontend/dist/backend/src/rate_limit.d.ts +145 -0
- package/frontend/dist/backend/src/rate_limit.js +673 -0
- package/frontend/dist/backend/src/route.d.ts +35 -0
- package/frontend/dist/backend/src/route.js +212 -0
- package/frontend/dist/backend/src/server.d.ts +485 -0
- package/frontend/dist/backend/src/server.js +2670 -0
- package/frontend/dist/backend/src/splash_screen.d.ts +80 -0
- package/frontend/dist/backend/src/splash_screen.js +135 -0
- package/frontend/dist/backend/src/status.d.ts +74 -0
- package/frontend/dist/backend/src/status.js +199 -0
- package/frontend/dist/backend/src/stream.d.ts +376 -0
- package/frontend/dist/backend/src/stream.js +1007 -0
- package/frontend/dist/backend/src/users.d.ts +807 -0
- package/frontend/dist/backend/src/users.js +2118 -0
- package/frontend/dist/backend/src/utils.d.ts +16 -0
- package/frontend/dist/backend/src/utils.js +241 -0
- package/frontend/dist/backend/src/view.d.ts +162 -0
- package/frontend/dist/backend/src/view.js +720 -0
- package/frontend/dist/frontend/src/elements/base.d.ts +4414 -0
- package/frontend/dist/{elements → frontend/src/elements}/base.js +3624 -260
- package/frontend/dist/frontend/src/elements/module.d.ts +95 -0
- package/frontend/dist/{elements → frontend/src/elements}/module.js +53 -52
- package/frontend/dist/frontend/src/elements/types.d.ts +52 -0
- package/frontend/dist/frontend/src/elements/types.js +5 -0
- package/frontend/dist/frontend/src/modules/attachment.d.ts +126 -0
- package/frontend/dist/frontend/src/modules/attachment.js +306 -0
- package/frontend/dist/frontend/src/modules/auth.d.ts +44 -0
- package/frontend/dist/frontend/src/modules/auth.js +80 -0
- package/frontend/dist/{modules → frontend/src/modules}/color.js +2 -2
- package/frontend/dist/frontend/src/modules/compression.d.ts +39 -0
- package/frontend/dist/frontend/src/modules/compression.js +102 -0
- package/frontend/dist/frontend/src/modules/cookies.d.ts +44 -0
- package/frontend/dist/frontend/src/modules/cookies.js +143 -0
- package/frontend/dist/frontend/src/modules/events.d.ts +31 -0
- package/frontend/dist/frontend/src/modules/events.js +74 -0
- package/frontend/dist/frontend/src/modules/google.d.ts +23 -0
- package/frontend/dist/frontend/src/modules/google.js +52 -0
- package/frontend/dist/frontend/src/modules/meta.d.ts +14 -0
- package/frontend/dist/{modules → frontend/src/modules}/meta.js +9 -7
- package/frontend/dist/{modules → frontend/src/modules}/paddle.d.ts +37 -134
- package/frontend/dist/{modules → frontend/src/modules}/paddle.js +620 -568
- package/frontend/dist/frontend/src/modules/request.d.ts +70 -0
- package/frontend/dist/frontend/src/modules/request.js +117 -0
- package/frontend/dist/frontend/src/modules/settings.d.ts +3 -0
- package/frontend/dist/frontend/src/modules/settings.js +5 -0
- package/frontend/dist/frontend/src/modules/statics.d.ts +21 -0
- package/frontend/dist/{modules → frontend/src/modules}/statics.js +15 -18
- package/frontend/dist/frontend/src/modules/support.d.ts +30 -0
- package/frontend/dist/frontend/src/modules/support.js +53 -0
- package/frontend/dist/{modules → frontend/src/modules}/theme.d.ts +67 -0
- package/frontend/dist/{modules → frontend/src/modules}/theme.js +68 -38
- package/frontend/dist/frontend/src/modules/themes.d.ts +12 -0
- package/frontend/dist/frontend/src/modules/themes.js +22 -0
- package/frontend/dist/frontend/src/modules/user.d.ts +164 -0
- package/frontend/dist/frontend/src/modules/user.js +268 -0
- package/frontend/dist/frontend/src/modules/utils.d.ts +176 -0
- package/frontend/dist/frontend/src/modules/utils.js +569 -0
- package/frontend/dist/frontend/src/types/gradient.d.ts +29 -0
- package/frontend/dist/{types → frontend/src/types}/gradient.js +14 -18
- package/frontend/dist/frontend/src/ui/border_button.d.ts +94 -0
- package/frontend/dist/{ui → frontend/src/ui}/border_button.js +7 -13
- package/frontend/dist/frontend/src/ui/button.d.ts +28 -0
- package/frontend/dist/{ui → frontend/src/ui}/button.js +21 -12
- package/frontend/dist/frontend/src/ui/canvas.d.ts +138 -0
- package/frontend/dist/{ui → frontend/src/ui}/canvas.js +88 -55
- package/frontend/dist/frontend/src/ui/checkbox.d.ts +74 -0
- package/frontend/dist/{ui → frontend/src/ui}/checkbox.js +80 -41
- package/frontend/dist/{ui → frontend/src/ui}/code.d.ts +73 -6
- package/frontend/dist/{ui → frontend/src/ui}/code.js +55 -52
- package/frontend/dist/{ui → frontend/src/ui}/context_menu.d.ts +4 -0
- package/frontend/dist/{ui → frontend/src/ui}/context_menu.js +12 -17
- package/frontend/dist/{ui → frontend/src/ui}/css.d.ts +4 -0
- package/frontend/dist/{ui → frontend/src/ui}/css.js +3 -3
- package/frontend/dist/{ui → frontend/src/ui}/divider.d.ts +4 -0
- package/frontend/dist/{ui → frontend/src/ui}/divider.js +3 -3
- package/frontend/dist/{ui → frontend/src/ui}/dropdown.d.ts +57 -2
- package/frontend/dist/{ui → frontend/src/ui}/dropdown.js +87 -94
- package/frontend/dist/{ui → frontend/src/ui}/for_each.d.ts +4 -0
- package/frontend/dist/{ui → frontend/src/ui}/for_each.js +3 -3
- package/frontend/dist/{ui → frontend/src/ui}/form.d.ts +6 -2
- package/frontend/dist/{ui → frontend/src/ui}/form.js +10 -7
- package/frontend/dist/frontend/src/ui/frame_modes.d.ts +37 -0
- package/frontend/dist/{ui → frontend/src/ui}/frame_modes.js +16 -22
- package/frontend/dist/{ui → frontend/src/ui}/google_map.d.ts +4 -0
- package/frontend/dist/{ui → frontend/src/ui}/google_map.js +4 -4
- package/frontend/dist/{ui → frontend/src/ui}/gradient.d.ts +4 -0
- package/frontend/dist/{ui → frontend/src/ui}/gradient.js +3 -3
- package/frontend/dist/{ui → frontend/src/ui}/image.d.ts +4 -0
- package/frontend/dist/{ui → frontend/src/ui}/image.js +5 -5
- package/frontend/dist/frontend/src/ui/input.d.ts +392 -0
- package/frontend/dist/{ui → frontend/src/ui}/input.js +346 -360
- package/frontend/dist/{ui → frontend/src/ui}/link.d.ts +4 -0
- package/frontend/dist/{ui → frontend/src/ui}/link.js +3 -3
- package/frontend/dist/{ui → frontend/src/ui}/list.d.ts +4 -0
- package/frontend/dist/{ui → frontend/src/ui}/list.js +12 -6
- package/frontend/dist/frontend/src/ui/loader_button.d.ts +80 -0
- package/frontend/dist/{ui → frontend/src/ui}/loader_button.js +35 -47
- package/frontend/dist/frontend/src/ui/loaders.d.ts +57 -0
- package/frontend/dist/{ui → frontend/src/ui}/loaders.js +11 -11
- package/frontend/dist/{ui → frontend/src/ui}/popup.d.ts +11 -6
- package/frontend/dist/{ui → frontend/src/ui}/popup.js +32 -18
- package/frontend/dist/frontend/src/ui/pseudo.d.ts +44 -0
- package/frontend/dist/{ui → frontend/src/ui}/pseudo.js +84 -8
- package/frontend/dist/{ui → frontend/src/ui}/scroller.d.ts +14 -2
- package/frontend/dist/{ui → frontend/src/ui}/scroller.js +37 -43
- package/frontend/dist/{ui → frontend/src/ui}/slider.d.ts +5 -1
- package/frontend/dist/{ui → frontend/src/ui}/slider.js +4 -4
- package/frontend/dist/{ui → frontend/src/ui}/spacer.d.ts +4 -0
- package/frontend/dist/{ui → frontend/src/ui}/spacer.js +3 -3
- package/frontend/dist/{ui → frontend/src/ui}/span.d.ts +4 -0
- package/frontend/dist/{ui → frontend/src/ui}/span.js +3 -3
- package/frontend/dist/{ui → frontend/src/ui}/stack.d.ts +4 -0
- package/frontend/dist/{ui → frontend/src/ui}/stack.js +3 -9
- package/frontend/dist/frontend/src/ui/steps.d.ts +131 -0
- package/frontend/dist/{ui → frontend/src/ui}/steps.js +30 -45
- package/frontend/dist/{ui → frontend/src/ui}/style.d.ts +4 -0
- package/frontend/dist/{ui → frontend/src/ui}/style.js +3 -3
- package/frontend/dist/{ui → frontend/src/ui}/switch.d.ts +5 -1
- package/frontend/dist/{ui → frontend/src/ui}/switch.js +4 -4
- package/frontend/dist/{ui → frontend/src/ui}/table.d.ts +4 -0
- package/frontend/dist/{ui → frontend/src/ui}/table.js +6 -6
- package/frontend/dist/{ui → frontend/src/ui}/tabs.d.ts +45 -3
- package/frontend/dist/{ui → frontend/src/ui}/tabs.js +65 -40
- package/frontend/dist/{ui → frontend/src/ui}/text.d.ts +4 -0
- package/frontend/dist/{ui → frontend/src/ui}/text.js +3 -3
- package/frontend/dist/frontend/src/ui/title.d.ts +91 -0
- package/frontend/dist/frontend/src/ui/title.js +272 -0
- package/frontend/dist/{ui → frontend/src/ui}/view.d.ts +4 -0
- package/frontend/dist/{ui → frontend/src/ui}/view.js +3 -3
- package/frontend/dist/{volt.d.ts → frontend/src/volt.d.ts} +3 -0
- package/frontend/dist/{volt.js → frontend/src/volt.js} +4 -0
- package/frontend/tools/bundle_d_ts.js +71 -0
- package/frontend/tools/convert_to_jsdoc_input.txt +9452 -0
- package/frontend/tools/convert_to_jsdoc_output.txt +7626 -0
- package/frontend/tools/convert_to_jsdoc_tmp.js +345 -0
- package/package.json +11 -12
- package/backend/dist/cjs/database/collection.d.ts +0 -160
- package/backend/dist/cjs/database/collection.js +0 -842
- package/backend/dist/cjs/database/database.d.ts +0 -121
- package/backend/dist/cjs/database/document.d.ts +0 -131
- package/backend/dist/cjs/database/document.js +0 -224
- package/backend/dist/cjs/database.d.ts +0 -502
- package/backend/dist/cjs/database.js +0 -2248
- package/backend/dist/cjs/logger.d.ts +0 -3
- package/backend/dist/cjs/meta.d.ts +0 -50
- package/backend/dist/cjs/mutex.d.ts +0 -24
- package/backend/dist/cjs/payments/paddle.d.ts +0 -160
- package/backend/dist/cjs/plugins/browser.d.ts +0 -36
- package/backend/dist/cjs/plugins/browser.js +0 -198
- package/backend/dist/cjs/plugins/css.d.ts +0 -11
- package/backend/dist/cjs/plugins/css.js +0 -80
- package/backend/dist/cjs/plugins/mail.d.ts +0 -277
- package/backend/dist/cjs/plugins/mail.js +0 -1370
- package/backend/dist/cjs/plugins/ts/compiler.d.ts +0 -139
- package/backend/dist/cjs/plugins/ts/compiler.js +0 -750
- package/backend/dist/cjs/plugins/ts/preprocessing.d.ts +0 -14
- package/backend/dist/cjs/plugins/ts/preprocessing.js +0 -440
- package/backend/dist/cjs/rate_limit.d.ts +0 -63
- package/backend/dist/cjs/rate_limit.js +0 -348
- package/backend/dist/cjs/request.deprc.d.ts +0 -48
- package/backend/dist/cjs/request.deprc.js +0 -572
- package/backend/dist/cjs/response.deprc.d.ts +0 -55
- package/backend/dist/cjs/response.deprc.js +0 -275
- package/backend/dist/cjs/server.d.ts +0 -342
- package/backend/dist/cjs/splash_screen.d.ts +0 -35
- package/backend/dist/cjs/status.d.ts +0 -61
- package/backend/dist/cjs/stream.d.ts +0 -79
- package/backend/dist/cjs/users.d.ts +0 -111
- package/backend/dist/cjs/users.js +0 -1817
- package/backend/dist/cjs/view.js +0 -352
- package/backend/dist/cjs/vinc.dev.d.ts +0 -3
- package/backend/dist/cjs/vinc.dev.js +0 -7
- package/backend/dist/css/adyen.css +0 -92
- package/backend/dist/css/volt.css +0 -70
- package/backend/dist/esm/database/collection.d.ts +0 -160
- package/backend/dist/esm/database/collection.js +0 -1328
- package/backend/dist/esm/database/database.d.ts +0 -121
- package/backend/dist/esm/database/document.d.ts +0 -131
- package/backend/dist/esm/database/document.js +0 -247
- package/backend/dist/esm/database.d.ts +0 -502
- package/backend/dist/esm/database.js +0 -2423
- package/backend/dist/esm/file_watcher.js +0 -329
- package/backend/dist/esm/logger.d.ts +0 -3
- package/backend/dist/esm/logger.js +0 -11
- package/backend/dist/esm/meta.d.ts +0 -50
- package/backend/dist/esm/mutex.d.ts +0 -24
- package/backend/dist/esm/mutex.js +0 -48
- package/backend/dist/esm/payments/paddle.d.ts +0 -160
- package/backend/dist/esm/plugins/browser.d.ts +0 -36
- package/backend/dist/esm/plugins/browser.js +0 -176
- package/backend/dist/esm/plugins/css.d.ts +0 -11
- package/backend/dist/esm/plugins/css.js +0 -90
- package/backend/dist/esm/plugins/ts/compiler.d.ts +0 -139
- package/backend/dist/esm/plugins/ts/compiler.js +0 -1194
- package/backend/dist/esm/plugins/ts/preprocessing.d.ts +0 -14
- package/backend/dist/esm/plugins/ts/preprocessing.js +0 -726
- package/backend/dist/esm/rate_limit.d.ts +0 -63
- package/backend/dist/esm/rate_limit.js +0 -417
- package/backend/dist/esm/request.deprc.d.ts +0 -48
- package/backend/dist/esm/request.deprc.js +0 -572
- package/backend/dist/esm/response.deprc.d.ts +0 -55
- package/backend/dist/esm/response.deprc.js +0 -275
- package/backend/dist/esm/server.d.ts +0 -342
- package/backend/dist/esm/splash_screen.d.ts +0 -35
- package/backend/dist/esm/status.d.ts +0 -61
- package/backend/dist/esm/status.js +0 -197
- package/backend/dist/esm/stream.d.ts +0 -79
- package/backend/dist/esm/users.d.ts +0 -111
- package/backend/dist/esm/users.js +0 -1935
- package/backend/dist/esm/vinc.dev.d.ts +0 -3
- package/backend/dist/esm/vinc.dev.js +0 -7
- package/frontend/dist/elements/base.d.ts +0 -9889
- package/frontend/dist/elements/module.d.ts +0 -30
- package/frontend/dist/modules/array.d.ts +0 -94
- package/frontend/dist/modules/array.js +0 -634
- package/frontend/dist/modules/auth.d.ts +0 -46
- package/frontend/dist/modules/auth.js +0 -139
- package/frontend/dist/modules/colors.d.ts +0 -1
- package/frontend/dist/modules/colors.js +0 -417
- package/frontend/dist/modules/compression.d.ts +0 -6
- package/frontend/dist/modules/compression.js +0 -999
- package/frontend/dist/modules/cookies.d.ts +0 -18
- package/frontend/dist/modules/cookies.js +0 -167
- package/frontend/dist/modules/date.d.ts +0 -142
- package/frontend/dist/modules/date.js +0 -493
- package/frontend/dist/modules/events.d.ts +0 -8
- package/frontend/dist/modules/events.js +0 -91
- package/frontend/dist/modules/google.d.ts +0 -11
- package/frontend/dist/modules/google.js +0 -54
- package/frontend/dist/modules/meta.d.ts +0 -10
- package/frontend/dist/modules/mutex.d.ts +0 -7
- package/frontend/dist/modules/mutex.js +0 -51
- package/frontend/dist/modules/number.d.ts +0 -16
- package/frontend/dist/modules/number.js +0 -23
- package/frontend/dist/modules/object.d.ts +0 -52
- package/frontend/dist/modules/object.js +0 -383
- package/frontend/dist/modules/scheme.d.ts +0 -227
- package/frontend/dist/modules/scheme.js +0 -531
- package/frontend/dist/modules/settings.d.ts +0 -3
- package/frontend/dist/modules/settings.js +0 -4
- package/frontend/dist/modules/statics.d.ts +0 -5
- package/frontend/dist/modules/string.d.ts +0 -124
- package/frontend/dist/modules/string.js +0 -745
- package/frontend/dist/modules/support.d.ts +0 -19
- package/frontend/dist/modules/support.js +0 -103
- package/frontend/dist/modules/themes.d.ts +0 -8
- package/frontend/dist/modules/themes.js +0 -18
- package/frontend/dist/modules/user.d.ts +0 -59
- package/frontend/dist/modules/user.js +0 -280
- package/frontend/dist/modules/utils.d.ts +0 -87
- package/frontend/dist/modules/utils.js +0 -923
- package/frontend/dist/types/gradient.d.ts +0 -12
- package/frontend/dist/ui/border_button.d.ts +0 -152
- package/frontend/dist/ui/button.d.ts +0 -21
- package/frontend/dist/ui/canvas.d.ts +0 -56
- package/frontend/dist/ui/checkbox.d.ts +0 -52
- package/frontend/dist/ui/frame_modes.d.ts +0 -25
- package/frontend/dist/ui/input.d.ts +0 -241
- package/frontend/dist/ui/loader_button.d.ts +0 -93
- package/frontend/dist/ui/loaders.d.ts +0 -57
- package/frontend/dist/ui/pseudo.d.ts +0 -16
- package/frontend/dist/ui/steps.d.ts +0 -59
- package/frontend/dist/ui/title.d.ts +0 -21
- package/frontend/dist/ui/title.js +0 -121
- package/frontend/examples/dashboard/dashboard.ts +0 -776
- /package/backend/dist/cjs/{cli.d.ts → backend/src/cli.d.ts} +0 -0
- /package/backend/dist/cjs/{file_watcher.d.ts → backend/src/database/document.d.ts} +0 -0
- /package/backend/dist/cjs/{file_watcher.js → backend/src/database/document.js} +0 -0
- /package/backend/dist/cjs/{plugins/pdf.d.ts → backend/src/database/filters/strict_filter_test.d.ts} +0 -0
- /package/backend/dist/{esm/file_watcher.d.ts → cjs/backend/src/database/filters/strict_filter_test_v0.d.ts} +0 -0
- /package/backend/dist/{esm/plugins/pdf.d.ts → cjs/backend/src/database/flatten_test.d.ts} +0 -0
- /package/backend/dist/cjs/{frontend.d.ts → backend/src/frontend.d.ts} +0 -0
- /package/backend/dist/cjs/{plugins → backend/src/plugins}/communication.d.ts +0 -0
- /package/backend/dist/cjs/{plugins → backend/src/plugins}/communication.js +0 -0
- /package/backend/dist/cjs/{plugins → backend/src/plugins}/mail/ui.js +0 -0
- /package/backend/dist/cjs/{plugins → backend/src/plugins}/pdf.js +0 -0
- /package/backend/dist/cjs/{plugins → backend/src/plugins}/thread_monitor.d.ts +0 -0
- /package/backend/dist/cjs/{plugins → backend/src/plugins}/thread_monitor.js +0 -0
- /package/backend/dist/cjs/{vinc.d.ts → backend/src/vinc.d.ts} +0 -0
- /package/backend/dist/cjs/{vinc.js → backend/src/vinc.js} +0 -0
- /package/backend/dist/esm/{cli.d.ts → backend/src/cli.d.ts} +0 -0
- /package/backend/dist/esm/{frontend.d.ts → backend/src/frontend.d.ts} +0 -0
- /package/backend/dist/esm/{plugins → backend/src/plugins}/communication.d.ts +0 -0
- /package/backend/dist/esm/{plugins → backend/src/plugins}/communication.js +0 -0
- /package/backend/dist/esm/{plugins → backend/src/plugins}/thread_monitor.d.ts +0 -0
- /package/backend/dist/esm/{plugins → backend/src/plugins}/thread_monitor.js +0 -0
- /package/backend/dist/esm/{vinc.d.ts → backend/src/vinc.d.ts} +0 -0
- /package/backend/dist/esm/{vinc.js → backend/src/vinc.js} +0 -0
- /package/frontend/dist/{elements → frontend/src/elements}/register_element.d.ts +0 -0
- /package/frontend/dist/{elements → frontend/src/elements}/register_element.js +0 -0
- /package/frontend/dist/{modules → frontend/src/modules}/color.d.ts +0 -0
- /package/frontend/dist/{ui → frontend/src/ui}/ui.d.ts +0 -0
- /package/frontend/dist/{ui → frontend/src/ui}/ui.js +0 -0
|
@@ -0,0 +1,3510 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @author Daan van den Bergh
|
|
3
|
+
* @copyright © 2022 - 2025 Daan van den Bergh.
|
|
4
|
+
*/
|
|
5
|
+
import * as mongodb from 'mongodb';
|
|
6
|
+
import * as vlib from "@vandenberghinc/vlib";
|
|
7
|
+
import { flatten } from "./flatten.js";
|
|
8
|
+
import { InvalidUsageError } from '../errors/index.js';
|
|
9
|
+
// ---------------------------------------------------------
|
|
10
|
+
// The collection class.
|
|
11
|
+
// ---------------------------------------------------------
|
|
12
|
+
/**
|
|
13
|
+
* @todo Deprecate `document.ts: Ref & Document`
|
|
14
|
+
* AND add a `record_version` `transform_version` collection params
|
|
15
|
+
* That move the versioning logic to the collection layer.
|
|
16
|
+
* AND potentially other additional features implemented in the depr classes.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* A wrapper class for the MongoDB collection.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* const col1 = server.db.collection("col1");
|
|
23
|
+
* const col2 = server.db.collection({
|
|
24
|
+
* name: "col2",
|
|
25
|
+
* indexes: ["uid", "name"],
|
|
26
|
+
* ttl: 1000 * 60 * 60 * 24, // 1 day
|
|
27
|
+
* });
|
|
28
|
+
*/
|
|
29
|
+
export class Collection {
|
|
30
|
+
/** Collection name */
|
|
31
|
+
name;
|
|
32
|
+
/** The mongo collection. */
|
|
33
|
+
_col;
|
|
34
|
+
/**
|
|
35
|
+
* The Database parent class, used to initialize the collection on demand.
|
|
36
|
+
* So the user can define collections at root level before the database is initialized.
|
|
37
|
+
*/
|
|
38
|
+
db;
|
|
39
|
+
/** Is initialized. */
|
|
40
|
+
initialized = false;
|
|
41
|
+
/** Whether this collection instance is transaction-based. */
|
|
42
|
+
is_transaction = false;
|
|
43
|
+
/** Whether this transaction has been finalized (committed or aborted). */
|
|
44
|
+
is_finalized_transaction = false;
|
|
45
|
+
/** Time to live in msec for all documents. */
|
|
46
|
+
ttl;
|
|
47
|
+
/** Is ttl behaviour enabled? */
|
|
48
|
+
ttl_enabled;
|
|
49
|
+
/** Enable sliding ttl (refreshes ttl on update), or static ttl (sets ttl on insert) */
|
|
50
|
+
sliding_ttl;
|
|
51
|
+
/**
|
|
52
|
+
* The temporary indexes passed to the constructor for the init method.
|
|
53
|
+
* @note This is not private so it can be updated by {@link QuotaManager}.
|
|
54
|
+
*/
|
|
55
|
+
_init_indexes;
|
|
56
|
+
/** The MongoDB client session for transaction support. */
|
|
57
|
+
_session;
|
|
58
|
+
/**
|
|
59
|
+
* The record type version for the database.
|
|
60
|
+
* See {@link Collection.Opts.record_version} for more info.
|
|
61
|
+
*
|
|
62
|
+
* Ensure its always defined so we always set the version to `1`,
|
|
63
|
+
* in case the user decides later that it would need the transform version
|
|
64
|
+
* for older documents. Otherwise they would not have the old `1` version.
|
|
65
|
+
*/
|
|
66
|
+
record_version;
|
|
67
|
+
/**
|
|
68
|
+
* The function to transform an older document version to the current version.
|
|
69
|
+
* See {@link Collection.Opts.on_transform_version} for more info.
|
|
70
|
+
*/
|
|
71
|
+
on_transform_version;
|
|
72
|
+
/**
|
|
73
|
+
* Save fully transformed documents again to prevent unneeded future transformations.
|
|
74
|
+
* See {@link Collection.Opts.persist_transformed_on_load} for more info.
|
|
75
|
+
*/
|
|
76
|
+
persist_transformed_on_load;
|
|
77
|
+
/**
|
|
78
|
+
* The function to call when a document is loaded (also when a default value is used).
|
|
79
|
+
* See {@link Collection.Opts.on_load} for more info.
|
|
80
|
+
*/
|
|
81
|
+
on_load_cb;
|
|
82
|
+
/**
|
|
83
|
+
* Constructs a new Collection instance.
|
|
84
|
+
*
|
|
85
|
+
* @param opts The constructor options for the collection.
|
|
86
|
+
*
|
|
87
|
+
* @throws An error when attempting to initialize a transaction-based collection without initializing the derived collection first.
|
|
88
|
+
*/
|
|
89
|
+
constructor(opts) {
|
|
90
|
+
// Public constructor.
|
|
91
|
+
if (!opts.transaction_based) {
|
|
92
|
+
this.name = opts.name;
|
|
93
|
+
this._col = opts.col;
|
|
94
|
+
this.db = opts.db;
|
|
95
|
+
this._init_indexes = opts.indexes;
|
|
96
|
+
this.is_transaction = false;
|
|
97
|
+
// Set ttl behaviour.
|
|
98
|
+
let ttl_ms;
|
|
99
|
+
let ttl_sliding = true;
|
|
100
|
+
if (typeof opts.ttl === "number") {
|
|
101
|
+
ttl_ms = opts.ttl;
|
|
102
|
+
ttl_sliding = true;
|
|
103
|
+
}
|
|
104
|
+
else if (opts.ttl && typeof opts.ttl === "object") {
|
|
105
|
+
ttl_ms = opts.ttl.milliseconds;
|
|
106
|
+
ttl_sliding = opts.ttl.sliding ?? true;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
ttl_ms = undefined;
|
|
110
|
+
ttl_sliding = true;
|
|
111
|
+
}
|
|
112
|
+
this.ttl = ttl_ms;
|
|
113
|
+
this.ttl_enabled = this.ttl != null;
|
|
114
|
+
this.sliding_ttl = ttl_sliding;
|
|
115
|
+
// Versioning & load callbacks.
|
|
116
|
+
if (opts.on_transform_version != null && opts.record_version == null) {
|
|
117
|
+
throw new InvalidUsageError({
|
|
118
|
+
message: "Option 'on_transform_version' requires 'record_version' to be defined.",
|
|
119
|
+
reason: "missing_record_version",
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
if (opts.record_version != null && (!Number.isInteger(opts.record_version) || opts.record_version < 1)) {
|
|
123
|
+
throw new InvalidUsageError({
|
|
124
|
+
message: "Option 'record_version' must be a positive integer.",
|
|
125
|
+
reason: "invalid_record_version",
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
const version = opts.record_version ?? 1;
|
|
129
|
+
if (version !== 1 && opts.on_transform_version == null) {
|
|
130
|
+
throw new InvalidUsageError({
|
|
131
|
+
message: "Option 'on_transform_version' must be set when 'record_version' is not 1.",
|
|
132
|
+
reason: "missing_transform_version",
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
this.record_version = opts.record_version ?? 1;
|
|
136
|
+
this.on_transform_version = opts.on_transform_version;
|
|
137
|
+
this.on_load_cb = opts.on_load;
|
|
138
|
+
this.persist_transformed_on_load = opts.persist_transformed_on_load ?? true;
|
|
139
|
+
}
|
|
140
|
+
// Private constructor for transaction based collections.
|
|
141
|
+
else {
|
|
142
|
+
// Ensure the derived collection is initialized, so we can skip this step in `init()`.
|
|
143
|
+
if (!opts.derived_collection.initialized) {
|
|
144
|
+
throw new InvalidUsageError({
|
|
145
|
+
message: `Derived collection "${opts.derived_collection.name}" is not yet initialized, this is required in order to construct a transaction based collection.`,
|
|
146
|
+
reason: "collection_not_initialized",
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
// Copy properties from the derived collection.
|
|
150
|
+
this.name = opts.derived_collection.name;
|
|
151
|
+
this._col = opts.derived_collection._col;
|
|
152
|
+
this.ttl = opts.derived_collection.ttl;
|
|
153
|
+
this.sliding_ttl = opts.derived_collection.sliding_ttl;
|
|
154
|
+
this.ttl_enabled = opts.derived_collection.ttl_enabled;
|
|
155
|
+
this.db = opts.derived_collection.db;
|
|
156
|
+
// indexes are not checked nor created in transaction mode.
|
|
157
|
+
// this._init_indexes = opts.derived_collection._init_indexes;
|
|
158
|
+
this.is_transaction = true;
|
|
159
|
+
// Copy versioning & load callbacks from derived collection.
|
|
160
|
+
this.record_version = opts.derived_collection.record_version;
|
|
161
|
+
this.on_transform_version = opts.derived_collection.on_transform_version;
|
|
162
|
+
this.on_load_cb = opts.derived_collection.on_load_cb;
|
|
163
|
+
this.persist_transformed_on_load = opts.derived_collection.persist_transformed_on_load;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// -------------------------------------------------------------------
|
|
167
|
+
// Private methods.
|
|
168
|
+
// -------------------------------------------------------------------
|
|
169
|
+
/**
|
|
170
|
+
* Initialize a database query from path or object.
|
|
171
|
+
* @throws An error if the input type is incorrect, and optionally if the query is empty.
|
|
172
|
+
*/
|
|
173
|
+
_init_query(query, allow_empty, param_name) {
|
|
174
|
+
if (!query || typeof query !== "object" || Array.isArray(query)) {
|
|
175
|
+
throw new InvalidUsageError({
|
|
176
|
+
message: `Parameter "${param_name}" is not a valid query.`,
|
|
177
|
+
reason: "invalid_query",
|
|
178
|
+
field: param_name,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
if (!allow_empty && Object.keys(query).length === 0) {
|
|
182
|
+
throw new InvalidUsageError({
|
|
183
|
+
message: `Parameter "${param_name}" is an empty object.`,
|
|
184
|
+
reason: "empty_query",
|
|
185
|
+
field: param_name,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
return query;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Setup the ttl configuration.
|
|
192
|
+
*
|
|
193
|
+
* @note When transaction mode is enabled, the session option will not be used.
|
|
194
|
+
*/
|
|
195
|
+
async _setup_ttl() {
|
|
196
|
+
//
|
|
197
|
+
// WE DONT USE THE TRANSACTION SESSION IN THIS METHOD.
|
|
198
|
+
//
|
|
199
|
+
// This function is not accessible on transaction based collections.
|
|
200
|
+
this.assert_not_transaction_based();
|
|
201
|
+
// Check init.
|
|
202
|
+
if (!this.initialized) {
|
|
203
|
+
await this.init();
|
|
204
|
+
}
|
|
205
|
+
this.assert_init();
|
|
206
|
+
if (!this.ttl_enabled || this.ttl == null) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const desired_seconds = Math.max(1, Math.ceil(this.ttl / 1000));
|
|
210
|
+
// 1) Get all indexes
|
|
211
|
+
const indexes = await this._col.indexes(); // [{ key: { __ttl_timestamp: 1 }, expireAfterSeconds: 3600 }, ...]
|
|
212
|
+
// 2) Find the TTL index
|
|
213
|
+
const ttl_index = indexes.find(ix => ix && typeof ix.key === "object" && ix.key.__ttl_timestamp === 1);
|
|
214
|
+
// 3a) Doesn't exist → create it
|
|
215
|
+
if (!ttl_index) {
|
|
216
|
+
await this._col.createIndex({ __ttl_timestamp: 1 }, { expireAfterSeconds: desired_seconds });
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
// 3b) Exists but wrong TTL → drop & recreate
|
|
220
|
+
if (ttl_index.expireAfterSeconds !== desired_seconds) {
|
|
221
|
+
let coll_mod_succeeded = false;
|
|
222
|
+
try {
|
|
223
|
+
await this.db._db.command({
|
|
224
|
+
collMod: this.name,
|
|
225
|
+
index: {
|
|
226
|
+
name: ttl_index.name,
|
|
227
|
+
expireAfterSeconds: desired_seconds
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
coll_mod_succeeded = true;
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
}
|
|
234
|
+
if (!coll_mod_succeeded) {
|
|
235
|
+
try {
|
|
236
|
+
await this._col.dropIndex(ttl_index.name ?? "__ttl_timestamp_1");
|
|
237
|
+
}
|
|
238
|
+
catch { /* ignore */ }
|
|
239
|
+
await this._col.createIndex({ __ttl_timestamp: 1 }, { expireAfterSeconds: desired_seconds });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// 3c) Exists and correct → nothing to do
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Apply the ttl timestamp to a database operation (update doc or pipeline).
|
|
246
|
+
* Do not upsert if the user explicitly sets `upsert: false` in the operation.
|
|
247
|
+
*/
|
|
248
|
+
_apply_ttl_to_operation(operation, upsert) {
|
|
249
|
+
if (!this.ttl_enabled)
|
|
250
|
+
return;
|
|
251
|
+
const now = new Date();
|
|
252
|
+
// Pipeline updates: append a $set stage
|
|
253
|
+
if (Array.isArray(operation)) {
|
|
254
|
+
if (this.sliding_ttl) {
|
|
255
|
+
operation.push({ $set: { __ttl_timestamp: now } });
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
// Static TTL: set only if missing to avoid refreshing on normal updates
|
|
259
|
+
operation.push({ $set: { __ttl_timestamp: { $ifNull: ["$__ttl_timestamp", now] } } });
|
|
260
|
+
}
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
// Classic update document with operators
|
|
264
|
+
const opKey = this.sliding_ttl ? "$set" : "$setOnInsert";
|
|
265
|
+
// For static TTL, only relevant if upsert is not explicitly false.
|
|
266
|
+
if (this.sliding_ttl || upsert !== false) {
|
|
267
|
+
const bucket = operation[opKey];
|
|
268
|
+
if (bucket == null) {
|
|
269
|
+
operation[opKey] = { __ttl_timestamp: now };
|
|
270
|
+
}
|
|
271
|
+
else if (typeof bucket === "object") {
|
|
272
|
+
bucket.__ttl_timestamp = now;
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
throw new InvalidUsageError({
|
|
276
|
+
message: `Invalid update operator object for TTL control at "${opKey}".`,
|
|
277
|
+
reason: "bad_ttl_operator",
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Injects `__record_version` into an update **only on insert paths**.
|
|
284
|
+
*
|
|
285
|
+
* Rules:
|
|
286
|
+
* - **Pipeline updates** (`update: Document[]`): no-op here (MongoDB has no `$setOnInsert` in pipelines).
|
|
287
|
+
* If you rely on upsert+pipeline, set `__record_version` explicitly in your pipeline.
|
|
288
|
+
* - **Replacement doc** (no operators):
|
|
289
|
+
* - When `upsert === true`, set `__record_version` **only if missing**.
|
|
290
|
+
* - When `upsert !== true`, do nothing (don’t mask older stored versions).
|
|
291
|
+
* - **Operator doc**:
|
|
292
|
+
* - Respect any user-provided `__record_version` in `$set` or `$setOnInsert`.
|
|
293
|
+
* - When `upsert === true` and the user didn’t provide a value, set it via `$setOnInsert`.
|
|
294
|
+
*
|
|
295
|
+
* Rationale:
|
|
296
|
+
* This avoids bumping `__record_version` during normal updates (which would mask older versions)
|
|
297
|
+
* while still stamping newly inserted documents.
|
|
298
|
+
*/
|
|
299
|
+
_apply_record_version_to_operation(operation, upsert) {
|
|
300
|
+
const current = this.record_version;
|
|
301
|
+
if (current == null)
|
|
302
|
+
return;
|
|
303
|
+
// 1) Pipeline update: we cannot reliably $setOnInsert in aggregation pipelines.
|
|
304
|
+
// Do nothing here. If you rely on upsert+pipeline, set __record_version in user pipeline.
|
|
305
|
+
if (Array.isArray(operation))
|
|
306
|
+
return;
|
|
307
|
+
const op = operation;
|
|
308
|
+
const hasDollar = Object.keys(op).some(k => k[0] === "$");
|
|
309
|
+
// 2) Replacement doc
|
|
310
|
+
if (!hasDollar) {
|
|
311
|
+
if (!upsert)
|
|
312
|
+
return; // normal replace of existing doc → do not stamp
|
|
313
|
+
// upsert replacement → insert path; stamp unless user provided a different value
|
|
314
|
+
if (op.__record_version == null) {
|
|
315
|
+
op.__record_version = current;
|
|
316
|
+
}
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
// 3) Operator doc
|
|
320
|
+
// Respect any user-provided versions
|
|
321
|
+
const userSet = op?.$set?.__record_version;
|
|
322
|
+
const userOnIns = op?.$setOnInsert?.__record_version;
|
|
323
|
+
if (userSet != null || userOnIns != null)
|
|
324
|
+
return;
|
|
325
|
+
// Only set on insert path (true upsert); never set on $set for existing docs
|
|
326
|
+
if (upsert) {
|
|
327
|
+
op.$setOnInsert = { ...(op.$setOnInsert ?? {}), __record_version: current };
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Decide if an error is worth a bounded retry.
|
|
332
|
+
* Prefers label-based detection and adds well-known transient/network surfaces.
|
|
333
|
+
*
|
|
334
|
+
* @param unknown_err The thrown error.
|
|
335
|
+
* @returns True for retryable/transient errors; false otherwise.
|
|
336
|
+
*/
|
|
337
|
+
_should_retry_error(unknown_err) {
|
|
338
|
+
if (typeof unknown_err !== "object" || !unknown_err || Array.isArray(unknown_err)) {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
const err = unknown_err;
|
|
342
|
+
const name = err?.name;
|
|
343
|
+
const code_name = err?.codeName;
|
|
344
|
+
/** Safely check MongoDB error labels (driver-provided). */
|
|
345
|
+
const has_label = (label) => {
|
|
346
|
+
if (typeof err?.hasErrorLabel === "function") {
|
|
347
|
+
try {
|
|
348
|
+
return !!err.hasErrorLabel(label);
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const labels = err?.errorLabels;
|
|
355
|
+
return Array.isArray(labels) && labels.includes(label);
|
|
356
|
+
};
|
|
357
|
+
/** Normalize numeric error code when available. */
|
|
358
|
+
const raw_code = err?.code;
|
|
359
|
+
const numeric_code = typeof raw_code === "number" ? raw_code :
|
|
360
|
+
(typeof raw_code === "string" && /^\d+$/.test(raw_code)) ? Number(raw_code) :
|
|
361
|
+
undefined;
|
|
362
|
+
/** Common Node.js system error codes that indicate transient I/O. */
|
|
363
|
+
const sys_code = typeof raw_code === "string" && isNaN(Number(raw_code)) ? raw_code : undefined;
|
|
364
|
+
const transient_sys = new Set([
|
|
365
|
+
"ECONNRESET", "ETIMEDOUT", "EPIPE", "ECONNREFUSED",
|
|
366
|
+
"ENETUNREACH", "ENETDOWN", "EHOSTUNREACH", "EAI_AGAIN"
|
|
367
|
+
]);
|
|
368
|
+
// Do NOT retry an intentional/explicit abort
|
|
369
|
+
if (name === "AbortError")
|
|
370
|
+
return false;
|
|
371
|
+
// Prefer official labels
|
|
372
|
+
if (has_label("TransientTransactionError") ||
|
|
373
|
+
has_label("UnknownTransactionCommitResult") ||
|
|
374
|
+
has_label("RetryableWriteError")) {
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
// Classic driver surfaces that usually resolve on retry
|
|
378
|
+
if (name === "MongoNetworkError" ||
|
|
379
|
+
name === "MongoNetworkTimeoutError" ||
|
|
380
|
+
name === "MongoServerSelectionError" ||
|
|
381
|
+
name === "MongoTopologyClosedError" ||
|
|
382
|
+
(sys_code && transient_sys.has(sys_code))) {
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
// Common network/replication/server transient codes
|
|
386
|
+
switch (numeric_code) {
|
|
387
|
+
case 6: /* HostUnreachable */ return true;
|
|
388
|
+
case 7: /* HostNotFound */ return true;
|
|
389
|
+
case 50: /* ExceededTimeLimit / MaxTimeMSExpired */ return true;
|
|
390
|
+
case 89: /* NetworkTimeout */ return true;
|
|
391
|
+
case 91: /* ShutdownInProgress */ return true;
|
|
392
|
+
case 112: /* WriteConflict */ return true;
|
|
393
|
+
case 189: /* PrimarySteppedDown */ return true;
|
|
394
|
+
case 262: /* ExceededTimeLimit (variant) */ return true;
|
|
395
|
+
case 10107: /* NotWritablePrimary / NotMaster */ return true;
|
|
396
|
+
case 11600: /* InterruptedAtShutdown */ return true;
|
|
397
|
+
case 11602: /* InterruptedDueToReplStateChange */ return true;
|
|
398
|
+
case 13435: /* NotPrimaryNoSecondaryOk */ return true;
|
|
399
|
+
case 13436: /* NotPrimaryOrSecondary */ return true;
|
|
400
|
+
case 9001: /* SocketException */ return true;
|
|
401
|
+
default: break;
|
|
402
|
+
}
|
|
403
|
+
// Some deployments bubble pool-cleared as codeName only
|
|
404
|
+
if (code_name === "PoolClearedError")
|
|
405
|
+
return true;
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Execute an async function with bounded, exponential backoff retries for retryable errors.
|
|
410
|
+
*
|
|
411
|
+
* - attempts: 1 ⇒ no retry (single execution).
|
|
412
|
+
* - Uses small bounded jitter to smooth load (see Collection.Retry).
|
|
413
|
+
*
|
|
414
|
+
* @param fn The async operation to execute.
|
|
415
|
+
* @param retry Number of attempts (1 = no retries) or {@link Collection.Retry.Opts}.
|
|
416
|
+
* @returns The function result when successful.
|
|
417
|
+
* @throws The last error if not retryable or retries exhausted.
|
|
418
|
+
*/
|
|
419
|
+
async _with_retry(fn, retry) {
|
|
420
|
+
const opts = Collection.Retry.normalize(retry);
|
|
421
|
+
if (opts.attempts <= 1) {
|
|
422
|
+
return await Promise.resolve().then(fn);
|
|
423
|
+
}
|
|
424
|
+
const last_index = opts.attempts - 1;
|
|
425
|
+
for (let i = 0; i < opts.attempts; i++) {
|
|
426
|
+
try {
|
|
427
|
+
return await Promise.resolve().then(fn);
|
|
428
|
+
}
|
|
429
|
+
catch (err) {
|
|
430
|
+
// Not retryable or out of attempts → rethrow immediately.
|
|
431
|
+
if (!this._should_retry_error(err) || i >= last_index) {
|
|
432
|
+
throw err;
|
|
433
|
+
}
|
|
434
|
+
const delay = Collection.Retry.compute_backoff_delay(i, opts);
|
|
435
|
+
if (delay > 0) {
|
|
436
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
437
|
+
}
|
|
438
|
+
// Retry next loop iteration.
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
// Type safety — logically unreachable.
|
|
442
|
+
throw new Error("Unexpected retry loop termination in _with_retry");
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Ensure `__record_version` is properly included for projections so version
|
|
446
|
+
* transformation can determine the original version reliably.
|
|
447
|
+
*
|
|
448
|
+
* @param projection The user-specified projection (if any).
|
|
449
|
+
* @returns A projection with `__record_version` enforced where needed.
|
|
450
|
+
*/
|
|
451
|
+
_ensure_version_in_projection(projection) {
|
|
452
|
+
if (!projection)
|
|
453
|
+
return projection;
|
|
454
|
+
// Is inclusion based array.
|
|
455
|
+
if (Array.isArray(projection)) {
|
|
456
|
+
return projection.includes("__record_version")
|
|
457
|
+
? projection
|
|
458
|
+
: [...projection, "__record_version"];
|
|
459
|
+
}
|
|
460
|
+
// Is exclusion based.
|
|
461
|
+
if (Object.values(projection).some(v => v === 0 || v === false)) {
|
|
462
|
+
if (projection["__record_version"] != null) {
|
|
463
|
+
const clone = { ...projection };
|
|
464
|
+
delete clone["__record_version"];
|
|
465
|
+
return clone;
|
|
466
|
+
}
|
|
467
|
+
return projection;
|
|
468
|
+
}
|
|
469
|
+
// Is inclusion based.
|
|
470
|
+
if (projection["__record_version"] !== 1 && projection["__record_version"] !== true) {
|
|
471
|
+
return { ...projection, __record_version: 1 };
|
|
472
|
+
}
|
|
473
|
+
return projection;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Determine whether a projection should be considered partial.
|
|
477
|
+
* @param projection The user-specified projection (if any).
|
|
478
|
+
* @returns True when a non-empty projection was provided.
|
|
479
|
+
*/
|
|
480
|
+
_is_partial_projection(projection) {
|
|
481
|
+
if (!projection)
|
|
482
|
+
return false;
|
|
483
|
+
if (Array.isArray(projection))
|
|
484
|
+
return projection.length > 0;
|
|
485
|
+
return Object.keys(projection).length > 0;
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Check whether the given update is operator-style (or a pipeline).
|
|
489
|
+
* - Aggregation pipeline: Array → valid.
|
|
490
|
+
* - Operator update: at least one top-level key starts with '$' → valid.
|
|
491
|
+
* - Plain object without '$' keys → NOT valid for updateOne/findOneAndUpdate.
|
|
492
|
+
*/
|
|
493
|
+
_is_operator_update_or_pipeline(operation) {
|
|
494
|
+
return Array.isArray(operation) || (operation && typeof operation === "object" && Object.keys(operation).some(k => k[0] === "$"));
|
|
495
|
+
}
|
|
496
|
+
// -------------------------------------------------------------------
|
|
497
|
+
// Public methods.
|
|
498
|
+
// -------------------------------------------------------------------
|
|
499
|
+
/**
|
|
500
|
+
* Initialize the collection, creating indexes and setting up TTL if needed.
|
|
501
|
+
* @returns The initialized collection instance.
|
|
502
|
+
*/
|
|
503
|
+
async init() {
|
|
504
|
+
if (this.initialized === false) {
|
|
505
|
+
// Initialize NON transaction based.
|
|
506
|
+
if (!this.is_transaction) {
|
|
507
|
+
// Create collection.
|
|
508
|
+
if (this._col == null) {
|
|
509
|
+
// Start connection in dev mode.
|
|
510
|
+
if (!this.db.server.production) {
|
|
511
|
+
await this.db.ensure_connection();
|
|
512
|
+
}
|
|
513
|
+
// Not connected.
|
|
514
|
+
if (!this.db.connected || !this.db._db) {
|
|
515
|
+
throw new InvalidUsageError({
|
|
516
|
+
message: `Database client is not connected.`,
|
|
517
|
+
reason: "client_not_connected",
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
// Check if the collection exists
|
|
521
|
+
if (this.db._listed_cols == null) {
|
|
522
|
+
this.db._listed_cols = await this.db._db.listCollections().toArray();
|
|
523
|
+
}
|
|
524
|
+
// Create collection with retry logic for race conditions
|
|
525
|
+
if (!this.db._listed_cols.find(x => x.name === this.name)) {
|
|
526
|
+
let create_col_retries = 3;
|
|
527
|
+
let last_error = null;
|
|
528
|
+
let collection_created = false;
|
|
529
|
+
while (create_col_retries > 0 && !collection_created) {
|
|
530
|
+
try {
|
|
531
|
+
await this.db._db.createCollection(this.name);
|
|
532
|
+
collection_created = true;
|
|
533
|
+
}
|
|
534
|
+
catch (error) {
|
|
535
|
+
last_error = error;
|
|
536
|
+
if (error.codeName === "NamespaceExists") {
|
|
537
|
+
collection_created = true; // Collection exists, that's ok
|
|
538
|
+
}
|
|
539
|
+
else if (create_col_retries > 1 && (error.code === 11000 || error.code === 48)) {
|
|
540
|
+
create_col_retries--;
|
|
541
|
+
await new Promise(r => setTimeout(r, 100));
|
|
542
|
+
}
|
|
543
|
+
else {
|
|
544
|
+
throw error;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
if (!collection_created && last_error) {
|
|
549
|
+
throw last_error;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
// Create collection.
|
|
553
|
+
this._col = this.db._db.collection(this.name);
|
|
554
|
+
}
|
|
555
|
+
// Assign as initialized when the column is created.
|
|
556
|
+
// Also since next used methods are checking for this attribute.
|
|
557
|
+
this.initialized = true;
|
|
558
|
+
// Create ttl index.
|
|
559
|
+
if (this.ttl_enabled) {
|
|
560
|
+
await this._setup_ttl();
|
|
561
|
+
}
|
|
562
|
+
// Create indexes.
|
|
563
|
+
if (this._init_indexes?.length) {
|
|
564
|
+
for (const item of this._init_indexes) {
|
|
565
|
+
await this.create_index(item);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Initialize transaction based.
|
|
571
|
+
* @note This assumes the derived collection has already been initialized.
|
|
572
|
+
*/
|
|
573
|
+
else {
|
|
574
|
+
// Start a new transaction.
|
|
575
|
+
if (!this.db.client) {
|
|
576
|
+
throw new InvalidUsageError({
|
|
577
|
+
message: "Database client is not initialized, this is likely because "
|
|
578
|
+
+ "you did not initialize the transaction based collection through 'Collection.start_transaction'.",
|
|
579
|
+
reason: "client_not_connected",
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
if (!this._col) {
|
|
583
|
+
throw new InvalidUsageError({
|
|
584
|
+
message: "Derived collection is not initialized, this should have been initialized before passing it to a transaction based collection constructor.",
|
|
585
|
+
reason: "derived_collection_not_initialized",
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
// Create the session.
|
|
589
|
+
this._session = this.db.client.startSession();
|
|
590
|
+
// Start the transaction.
|
|
591
|
+
this._session.startTransaction();
|
|
592
|
+
// Set as initialized.
|
|
593
|
+
this.initialized = true;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return this;
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Assert that the collection is initialized and has a valid MongoDB collection.
|
|
600
|
+
* @throws {Error} Throws if the collection is not initialized or _col is null
|
|
601
|
+
* @returns An initialized collection type assertion
|
|
602
|
+
*/
|
|
603
|
+
assert_init() {
|
|
604
|
+
if (!this.initialized || this._col == null) {
|
|
605
|
+
throw new InvalidUsageError({
|
|
606
|
+
message: `Collection "${this.name}" is not initialized.`,
|
|
607
|
+
reason: "collection_not_initialized",
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Assert that if this is a transaction, it has not been finalized.
|
|
613
|
+
* @throws Error if this is a finalized transaction.
|
|
614
|
+
*/
|
|
615
|
+
assert_not_finalized() {
|
|
616
|
+
if (this.is_transaction && this.is_finalized_transaction) {
|
|
617
|
+
throw new InvalidUsageError({
|
|
618
|
+
message: `Transaction has already been finalized (committed or aborted).`,
|
|
619
|
+
reason: "transaction_finalized",
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Assert that this collection is not transaction based.
|
|
625
|
+
*/
|
|
626
|
+
assert_not_transaction_based() {
|
|
627
|
+
if (this.is_transaction) {
|
|
628
|
+
throw new InvalidUsageError({
|
|
629
|
+
message: `Collection "${this.name}" is transaction based.`,
|
|
630
|
+
reason: "collection_is_transaction",
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Get operation options with session if this is a transaction.
|
|
636
|
+
* @returns Options object with session if applicable.
|
|
637
|
+
*/
|
|
638
|
+
get_operation_options(opts) {
|
|
639
|
+
if (this.is_transaction && this._session) {
|
|
640
|
+
return { ...opts, session: this._session };
|
|
641
|
+
}
|
|
642
|
+
return opts ?? {};
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Get the raw and initialized MongoDB collection.
|
|
646
|
+
* @returns The MongoDB collection instance.
|
|
647
|
+
*/
|
|
648
|
+
async col() {
|
|
649
|
+
await this.init();
|
|
650
|
+
return this._col;
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Check if an index exists.
|
|
654
|
+
* @note Not supported for transaction based collections.
|
|
655
|
+
* @param index The name of the index to check.
|
|
656
|
+
* @returns True if the index exists, false otherwise.
|
|
657
|
+
*/
|
|
658
|
+
async has_index(index) {
|
|
659
|
+
if (!this.initialized) {
|
|
660
|
+
await this.init();
|
|
661
|
+
}
|
|
662
|
+
this.assert_init();
|
|
663
|
+
this.assert_not_finalized();
|
|
664
|
+
this.assert_not_transaction_based();
|
|
665
|
+
// No need to pass session obj here.
|
|
666
|
+
return (await this._col.listIndexes().toArray()).some(x => x.name === index);
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Creates indexes on collections.
|
|
670
|
+
*
|
|
671
|
+
* @note When transaction mode is enabled, the session option will not be used.
|
|
672
|
+
*
|
|
673
|
+
* @param opts The index create options.
|
|
674
|
+
*/
|
|
675
|
+
async create_index(opts) {
|
|
676
|
+
// Not supported on transaction-based collections.
|
|
677
|
+
this.assert_not_transaction_based();
|
|
678
|
+
// Ensure initialized
|
|
679
|
+
if (!this.initialized) {
|
|
680
|
+
await this.init();
|
|
681
|
+
}
|
|
682
|
+
this.assert_init();
|
|
683
|
+
// ---- Normalize inputs ----
|
|
684
|
+
let key;
|
|
685
|
+
let keys;
|
|
686
|
+
let options;
|
|
687
|
+
let unique;
|
|
688
|
+
let sparse;
|
|
689
|
+
let forced = false;
|
|
690
|
+
if (typeof opts === "string") {
|
|
691
|
+
key = opts;
|
|
692
|
+
unique = undefined;
|
|
693
|
+
sparse = undefined;
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
({ key, keys, forced = false } = opts);
|
|
697
|
+
const options = opts.options;
|
|
698
|
+
// Conflict guard between `unique` and `options.unique`
|
|
699
|
+
if (opts.unique != null && options?.unique != null && opts.unique !== options.unique) {
|
|
700
|
+
throw new InvalidUsageError({
|
|
701
|
+
message: `Encountered different values for attribute 'unique': ${opts.unique} and 'options.unique': ${options.unique}.`,
|
|
702
|
+
reason: "invalid_unique_option",
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
unique = opts.unique ?? options?.unique;
|
|
706
|
+
// Conflict guard between `sparse` and `options.sparse`
|
|
707
|
+
if (opts.sparse != null && options?.sparse != null && opts.sparse !== options.sparse) {
|
|
708
|
+
throw new InvalidUsageError({
|
|
709
|
+
message: `Encountered different values for attribute 'sparse': ${opts.sparse} and 'options.sparse': ${options.sparse}.`,
|
|
710
|
+
reason: "invalid_sparse_option",
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
sparse = opts.sparse ?? options?.sparse;
|
|
714
|
+
}
|
|
715
|
+
// Ensure `unique` in options when provided
|
|
716
|
+
if (unique) {
|
|
717
|
+
options = options || {};
|
|
718
|
+
options.unique = unique;
|
|
719
|
+
}
|
|
720
|
+
// Ensure `sparse` in options when provided
|
|
721
|
+
if (sparse) {
|
|
722
|
+
options = options || {};
|
|
723
|
+
options.sparse = sparse;
|
|
724
|
+
}
|
|
725
|
+
// Build keys object
|
|
726
|
+
let keys_obj;
|
|
727
|
+
if (key) {
|
|
728
|
+
keys_obj = { [key]: 1 };
|
|
729
|
+
}
|
|
730
|
+
else if (Array.isArray(keys) && keys.length > 0) {
|
|
731
|
+
keys_obj = {};
|
|
732
|
+
for (const k of keys)
|
|
733
|
+
keys_obj[k] = 1;
|
|
734
|
+
}
|
|
735
|
+
else if (keys != null && typeof keys === "object") {
|
|
736
|
+
keys_obj = keys;
|
|
737
|
+
}
|
|
738
|
+
else {
|
|
739
|
+
throw new InvalidUsageError({
|
|
740
|
+
message: "Define one of the following parameters: [key, keys].",
|
|
741
|
+
reason: "invalid_index_definition",
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
const drop_index = async () => {
|
|
745
|
+
try {
|
|
746
|
+
const existing = await this._col.listIndexes().toArray();
|
|
747
|
+
const match = existing.find(ix => {
|
|
748
|
+
const ix_key = ix?.key;
|
|
749
|
+
if (!ix_key)
|
|
750
|
+
return false;
|
|
751
|
+
const a = Object.entries(ix_key);
|
|
752
|
+
const b = Object.entries(keys_obj);
|
|
753
|
+
if (a.length !== b.length)
|
|
754
|
+
return false;
|
|
755
|
+
// exact key-value equality (order-insensitive)
|
|
756
|
+
const as = new Map(a);
|
|
757
|
+
for (const [kk, vv] of b) {
|
|
758
|
+
if (as.get(kk) !== vv)
|
|
759
|
+
return false;
|
|
760
|
+
}
|
|
761
|
+
return true;
|
|
762
|
+
});
|
|
763
|
+
// Prefer matched key's real name
|
|
764
|
+
if (match?.name) {
|
|
765
|
+
try {
|
|
766
|
+
await this._col.dropIndex(match.name);
|
|
767
|
+
}
|
|
768
|
+
catch (err) {
|
|
769
|
+
if (err?.codeName !== "IndexNotFound")
|
|
770
|
+
throw err;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
else if (options?.name) {
|
|
774
|
+
try {
|
|
775
|
+
await this._col.dropIndex(options.name);
|
|
776
|
+
}
|
|
777
|
+
catch (err) {
|
|
778
|
+
if (err?.codeName !== "IndexNotFound")
|
|
779
|
+
throw err;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
else {
|
|
783
|
+
// last-resort synthesized name (simple cases)
|
|
784
|
+
const synthesized = Object.entries(keys_obj).map(([k, v]) => `${k}_${v}`).join("_");
|
|
785
|
+
try {
|
|
786
|
+
await this._col.dropIndex(synthesized);
|
|
787
|
+
}
|
|
788
|
+
catch (err) {
|
|
789
|
+
if (err?.codeName !== "IndexNotFound")
|
|
790
|
+
throw err;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
catch (err) {
|
|
795
|
+
// If listIndexes itself fails for some reason, do not hide the error
|
|
796
|
+
throw new Error(`Failed to create index on collection "${this.name}": ${err}`, { cause: err });
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
try {
|
|
800
|
+
// Create (or re-create)
|
|
801
|
+
try {
|
|
802
|
+
return await this._col.createIndex(keys_obj, options);
|
|
803
|
+
}
|
|
804
|
+
// Retry once on IndexKeySpecsConflict when forced=true
|
|
805
|
+
catch (err) {
|
|
806
|
+
if (forced && err && typeof err === "object" && (err.codeName === "IndexKeySpecsConflict")) {
|
|
807
|
+
await drop_index();
|
|
808
|
+
return await this._col.createIndex(keys_obj, options);
|
|
809
|
+
}
|
|
810
|
+
throw err;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
catch (err) {
|
|
814
|
+
throw new Error(`Failed to create index on collection "${this.name}": ${err}`, { cause: err });
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Standalone helper: merge `source` into `target` for missing keys only.
|
|
819
|
+
* Clones assigned nested objects/arrays/dates once (when `clone` is true).
|
|
820
|
+
*
|
|
821
|
+
* @throws An error if the max depth recursion depth has been exceeded.
|
|
822
|
+
*/
|
|
823
|
+
static insert_defaults(target, source, opts = {}) {
|
|
824
|
+
const max_depth = opts.max_depth ?? 1_000;
|
|
825
|
+
const depth = opts.depth ?? 0;
|
|
826
|
+
const should_clone = opts.clone ?? true;
|
|
827
|
+
const isPlainObject = (v) => v != null && typeof v === "object" && Object.getPrototypeOf(v) === Object.prototype;
|
|
828
|
+
const cloneAssigned = (val, d) => {
|
|
829
|
+
if (!should_clone)
|
|
830
|
+
return val;
|
|
831
|
+
if (d > max_depth)
|
|
832
|
+
return val;
|
|
833
|
+
if (Array.isArray(val)) {
|
|
834
|
+
return val.map(item => cloneAssigned(item, d + 1));
|
|
835
|
+
}
|
|
836
|
+
if (val instanceof Date) {
|
|
837
|
+
return new Date(val.getTime());
|
|
838
|
+
}
|
|
839
|
+
if (isPlainObject(val)) {
|
|
840
|
+
const out = {};
|
|
841
|
+
for (const k of Object.keys(val)) {
|
|
842
|
+
out[k] = cloneAssigned(val[k], d + 1);
|
|
843
|
+
}
|
|
844
|
+
return out;
|
|
845
|
+
}
|
|
846
|
+
// Map/Set/custom instances: keep by reference
|
|
847
|
+
return val;
|
|
848
|
+
};
|
|
849
|
+
if (depth > max_depth) {
|
|
850
|
+
throw new Error(`Maximum recursion depth (${max_depth}) exceeded in 'insert_defaults'`);
|
|
851
|
+
}
|
|
852
|
+
for (const key of Object.keys(source)) {
|
|
853
|
+
const v = target[key];
|
|
854
|
+
const d = source[key];
|
|
855
|
+
if (v === undefined) {
|
|
856
|
+
target[key] = cloneAssigned(d, depth + 1);
|
|
857
|
+
}
|
|
858
|
+
else if (isPlainObject(v) && isPlainObject(d)) {
|
|
859
|
+
Collection.insert_defaults(v, d, { depth: depth + 1, max_depth, clone: should_clone });
|
|
860
|
+
}
|
|
861
|
+
// Existing non-plain objects/arrays/primitives are left as-is.
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
flatten(obj, prefix = "") {
|
|
865
|
+
return flatten(obj, prefix);
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Execute `on_transform_version` and `on_load_cb` on a loaded document.
|
|
869
|
+
* Ensures `__record_version` is set when {@link record_version} is defined.
|
|
870
|
+
*
|
|
871
|
+
* @param data The loaded document.
|
|
872
|
+
* @param opts Additional options.
|
|
873
|
+
*
|
|
874
|
+
* @returns The transformed document.
|
|
875
|
+
*
|
|
876
|
+
* @throws {Collection.OnTransformError} When an error occurs during the {@link Collection.Opts.on_transform_version} callback.
|
|
877
|
+
* @throws {Collection.OnLoadError} When an error occurs during the {@link Collection.Opts.on_load} callback.
|
|
878
|
+
*/
|
|
879
|
+
async apply_on_load(data, opts) {
|
|
880
|
+
let transformed = false;
|
|
881
|
+
const is_partial = this._is_partial_projection(opts.projection);
|
|
882
|
+
// Transform from older version to current (unchanged), but track if we did it.
|
|
883
|
+
if (this.record_version != null &&
|
|
884
|
+
this.on_transform_version != null &&
|
|
885
|
+
data &&
|
|
886
|
+
data.__record_version !== this.record_version) {
|
|
887
|
+
try {
|
|
888
|
+
data = await this.on_transform_version(data, {
|
|
889
|
+
from_version: data.__record_version,
|
|
890
|
+
to_version: this.record_version,
|
|
891
|
+
projection: opts.projection,
|
|
892
|
+
is_partial: is_partial,
|
|
893
|
+
});
|
|
894
|
+
transformed = true;
|
|
895
|
+
}
|
|
896
|
+
catch (error) {
|
|
897
|
+
throw new Collection.OnTransformError({
|
|
898
|
+
message: `Failed to transform document from version '${data.__record_version}' to '${this.record_version}'.`,
|
|
899
|
+
query: {},
|
|
900
|
+
reason: "callback_error",
|
|
901
|
+
cause: error,
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
data.__record_version = this.record_version;
|
|
905
|
+
}
|
|
906
|
+
// Keep existing on_load invocation
|
|
907
|
+
if (this.on_load_cb) {
|
|
908
|
+
try {
|
|
909
|
+
data = await this.on_load_cb(data, {
|
|
910
|
+
projection: opts.projection,
|
|
911
|
+
is_partial: is_partial,
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
catch (error) {
|
|
915
|
+
throw new Collection.OnLoadError({
|
|
916
|
+
message: `Encountered an error during the 'on_load' callback.`,
|
|
917
|
+
query: {},
|
|
918
|
+
reason: "callback_error",
|
|
919
|
+
cause: error,
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
// Persist document once when safe.
|
|
924
|
+
if (transformed &&
|
|
925
|
+
this.persist_transformed_on_load &&
|
|
926
|
+
opts.persist && // only persist if doc came from DB (not a default)
|
|
927
|
+
!is_partial && // only when we have a full document
|
|
928
|
+
data?._id != null // we can target by _id
|
|
929
|
+
) {
|
|
930
|
+
try {
|
|
931
|
+
// Use $replace to replace the entire document
|
|
932
|
+
if (this.persist_transformed_on_load === "replace") {
|
|
933
|
+
const replace_doc = { ...data };
|
|
934
|
+
if (this.ttl_enabled && replace_doc.__ttl_timestamp == null) {
|
|
935
|
+
replace_doc.__ttl_timestamp = new Date();
|
|
936
|
+
}
|
|
937
|
+
if (this.record_version != null && replace_doc.__record_version == null) {
|
|
938
|
+
replace_doc.__record_version = this.record_version;
|
|
939
|
+
}
|
|
940
|
+
const res = this.replace({ _id: data._id }, replace_doc, { upsert: false, throw: false, apply_ttl: false } // do not create on read
|
|
941
|
+
);
|
|
942
|
+
if (opts.await_persist) {
|
|
943
|
+
await res;
|
|
944
|
+
}
|
|
945
|
+
else {
|
|
946
|
+
void res;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
// Use $set to avoid converting replacement to operator by TTL injection,
|
|
950
|
+
// and to avoid unsetting unknown fields.
|
|
951
|
+
else {
|
|
952
|
+
const set_doc = { ...data };
|
|
953
|
+
delete set_doc._id;
|
|
954
|
+
delete set_doc.__ttl_timestamp; // keep TTL untouched
|
|
955
|
+
const res = this.save({ _id: data._id }, { $set: set_doc }, { upsert: false, throw: false, apply_ttl: false } // do not create on read
|
|
956
|
+
);
|
|
957
|
+
if (opts.await_persist) {
|
|
958
|
+
await res;
|
|
959
|
+
}
|
|
960
|
+
else {
|
|
961
|
+
void res;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
catch {
|
|
966
|
+
// ignore any failure on read-path persistence
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
return data;
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Count documents accurately using MongoDB's `countDocuments`.
|
|
973
|
+
*
|
|
974
|
+
* @param query An optional filter to count matching documents. When omitted, counts all documents.
|
|
975
|
+
* @param opts Additional options, see {@link Collection.CountOpts}.
|
|
976
|
+
*
|
|
977
|
+
* @note The `opts.throw` option defaults to `true`.
|
|
978
|
+
*
|
|
979
|
+
* @returns
|
|
980
|
+
* - A number representing the accurate count when successful.
|
|
981
|
+
* - A {@link Collection.CountError} when `opts.throw === false` and an error occurs.
|
|
982
|
+
*
|
|
983
|
+
* @throws {Collection.CountError} When `throw !== false` and the count fails.
|
|
984
|
+
* @throws {InvalidUsageError} (always) When the provided argument(s) are invalid or if the collection was not used properly.
|
|
985
|
+
*/
|
|
986
|
+
async count(query, opts) {
|
|
987
|
+
// Asserts.
|
|
988
|
+
if (!this.initialized) {
|
|
989
|
+
await this.init();
|
|
990
|
+
}
|
|
991
|
+
this.assert_init();
|
|
992
|
+
this.assert_not_finalized();
|
|
993
|
+
// Normalize/validate query; allow empty when omitted.
|
|
994
|
+
const query_op = this._init_query(query ?? {}, true, "query");
|
|
995
|
+
// Unpack opts.
|
|
996
|
+
const throw_errors = opts?.throw ?? true;
|
|
997
|
+
try {
|
|
998
|
+
const n = await this._with_retry(() => this._col.countDocuments(query_op, this.get_operation_options(typeof opts?.timeout === "number" ? { maxTimeMS: opts.timeout } : {})), opts?.retry);
|
|
999
|
+
return n;
|
|
1000
|
+
}
|
|
1001
|
+
catch (e) {
|
|
1002
|
+
const err = new Collection.CountError({
|
|
1003
|
+
message: "Count operation failed due to an unexpected error.",
|
|
1004
|
+
query: query_op,
|
|
1005
|
+
reason: this._should_retry_error(e)
|
|
1006
|
+
? (Collection.Retry.get_attempts(opts?.retry) > 1 ? "retries_exhausted" : "retryable")
|
|
1007
|
+
: "unknown",
|
|
1008
|
+
cause: e,
|
|
1009
|
+
});
|
|
1010
|
+
if (throw_errors)
|
|
1011
|
+
throw err;
|
|
1012
|
+
return err;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Return a fast, approximate count of the entire collection using
|
|
1017
|
+
* MongoDB's `estimatedDocumentCount`. This method does **not** accept
|
|
1018
|
+
* a filter and may be off under heavy churn.
|
|
1019
|
+
*
|
|
1020
|
+
* @param opts Additional options, see {@link Collection.CountOpts}.
|
|
1021
|
+
*
|
|
1022
|
+
* @note The `opts.throw` option defaults to `true`.
|
|
1023
|
+
*
|
|
1024
|
+
* @returns
|
|
1025
|
+
* - A number representing the estimated total number of documents when successful.
|
|
1026
|
+
* - A {@link Collection.CountError} when `opts.throw === false` and an error occurs.
|
|
1027
|
+
*
|
|
1028
|
+
* @throws {Collection.CountError} When `throw !== false` and the count fails.
|
|
1029
|
+
* @throws {InvalidUsageError} (always) When the collection was not used properly.
|
|
1030
|
+
*/
|
|
1031
|
+
async count_estimated(opts) {
|
|
1032
|
+
// Asserts.
|
|
1033
|
+
if (!this.initialized) {
|
|
1034
|
+
await this.init();
|
|
1035
|
+
}
|
|
1036
|
+
this.assert_init();
|
|
1037
|
+
this.assert_not_finalized();
|
|
1038
|
+
// Unpack opts.
|
|
1039
|
+
const throw_errors = opts?.throw ?? true;
|
|
1040
|
+
try {
|
|
1041
|
+
const n = await this._with_retry(() => this._col.estimatedDocumentCount(this.get_operation_options(typeof opts?.timeout === "number" ? { maxTimeMS: opts.timeout } : {})), opts?.retry);
|
|
1042
|
+
return n;
|
|
1043
|
+
}
|
|
1044
|
+
catch (e) {
|
|
1045
|
+
const err = new Collection.CountError({
|
|
1046
|
+
message: "Estimated count operation failed due to an unexpected error.",
|
|
1047
|
+
query: {}, // no filter for estimatedDocumentCount
|
|
1048
|
+
reason: this._should_retry_error(e)
|
|
1049
|
+
? (Collection.Retry.get_attempts(opts?.retry) > 1 ? "retries_exhausted" : "retryable")
|
|
1050
|
+
: "unknown",
|
|
1051
|
+
cause: e,
|
|
1052
|
+
});
|
|
1053
|
+
if (throw_errors)
|
|
1054
|
+
throw err;
|
|
1055
|
+
return err;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* List all documents for a specific query.
|
|
1060
|
+
*
|
|
1061
|
+
* @param query The database directory path.
|
|
1062
|
+
* @param opts The list options, see {@link Collection.ListOpts}.
|
|
1063
|
+
* @param allow_empty_query When `true`, allows an empty query (i.e. `{}`) to be passed, which would otherwise throw an error.
|
|
1064
|
+
*
|
|
1065
|
+
* @note The `opts.throw` option defaults to `true`.
|
|
1066
|
+
* @note The {@link Collection.Opts.on_load} and {@link Collection.Opts.on_transform_version} callbacks
|
|
1067
|
+
* are not executed when `opts.cursor === true`.
|
|
1068
|
+
* @note When `opts.callback` is a function (and `opts.cursor !== true`), this method streams documents and
|
|
1069
|
+
* invokes the callback for each processed document, then returns `undefined` on success.
|
|
1070
|
+
* This mode is memory-friendly and avoids accumulating the entire result set.
|
|
1071
|
+
*
|
|
1072
|
+
* @returns
|
|
1073
|
+
* - An error if `opts.throw === false` and a {@link Collection.ListError} has occurred.
|
|
1074
|
+
* - The find cursor when `opts.cursor === true`.
|
|
1075
|
+
* - When `opts.callback && !opts.cursor` is provided, `undefined` on success.
|
|
1076
|
+
* - When `opts.page_info === true && !opts.cursor && !opts.callback`, returns {@link Collection.ListedPage}.
|
|
1077
|
+
* - Otherwise, an array of documents matching the path.
|
|
1078
|
+
*
|
|
1079
|
+
* @throws {Collection.ListError} When `throw !== false` if an error occurred during the operation, in which case {@link Collection.ListError.cause} is defined.
|
|
1080
|
+
* @throws {InvalidUsageError} (always) When the provided argument(s) are invalid or if the collection was not used properly.
|
|
1081
|
+
*/
|
|
1082
|
+
async list(query, opts, allow_empty_query = false) {
|
|
1083
|
+
// Assert.
|
|
1084
|
+
if (!this.initialized) {
|
|
1085
|
+
await this.init();
|
|
1086
|
+
}
|
|
1087
|
+
this.assert_init();
|
|
1088
|
+
this.assert_not_finalized();
|
|
1089
|
+
// Unpack opts.
|
|
1090
|
+
const throw_errors = opts?.throw ?? true;
|
|
1091
|
+
const has_callback = typeof opts?.callback === "function";
|
|
1092
|
+
const page_info_requested = opts?.page_info === true && opts?.cursor !== true && !has_callback;
|
|
1093
|
+
// Invalid combinations.
|
|
1094
|
+
if (has_callback && opts?.cursor === true) {
|
|
1095
|
+
throw new InvalidUsageError({
|
|
1096
|
+
message: "Option 'callback' cannot be combined with 'cursor: true'.",
|
|
1097
|
+
reason: "invalid_option_combination",
|
|
1098
|
+
field: "opts.callback",
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
if (has_callback && opts?.page_info === true) {
|
|
1102
|
+
throw new InvalidUsageError({
|
|
1103
|
+
message: "Option 'callback' cannot be combined with 'page_info: true'.",
|
|
1104
|
+
reason: "invalid_option_combination",
|
|
1105
|
+
field: "opts.callback",
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
// Capture explicit user limit; if undefined, we will stream all documents
|
|
1109
|
+
// Add +1 for finite check since we may need to probe (page_info only).
|
|
1110
|
+
const user_limit = opts?.limit;
|
|
1111
|
+
if (typeof user_limit === "number") {
|
|
1112
|
+
const effective_user_limit = page_info_requested ? user_limit + 1 : user_limit;
|
|
1113
|
+
const is_integer = Number.isInteger(user_limit);
|
|
1114
|
+
const is_valid = user_limit >= 0 && Number.isFinite(effective_user_limit);
|
|
1115
|
+
if (!is_integer || !is_valid) {
|
|
1116
|
+
throw new InvalidUsageError({
|
|
1117
|
+
message: `Option 'limit' must be a non-negative finite integer${page_info_requested ? " (including +1 for pagination)." : "."}`,
|
|
1118
|
+
reason: "invalid_limit",
|
|
1119
|
+
field: "opts.limit",
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
// Driver limit; +1 when probing for has_more.
|
|
1124
|
+
const probing_limit = (typeof user_limit === "number" && page_info_requested)
|
|
1125
|
+
? user_limit + 1
|
|
1126
|
+
: user_limit;
|
|
1127
|
+
// Validate skip.
|
|
1128
|
+
if (opts?.skip != null) {
|
|
1129
|
+
if (!Number.isInteger(opts.skip) || opts.skip < 0) {
|
|
1130
|
+
throw new InvalidUsageError({
|
|
1131
|
+
message: "Option 'skip' must be a non-negative integer.",
|
|
1132
|
+
reason: "invalid_skip",
|
|
1133
|
+
field: "opts.skip",
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
// Early return on zero limit (no round trip); respect page_info + callback shapes.
|
|
1138
|
+
if (user_limit === 0 && !opts?.cursor) {
|
|
1139
|
+
if (has_callback) {
|
|
1140
|
+
return undefined;
|
|
1141
|
+
}
|
|
1142
|
+
return (page_info_requested
|
|
1143
|
+
? { items: [], has_more: false }
|
|
1144
|
+
: []);
|
|
1145
|
+
}
|
|
1146
|
+
// Batch size for server-to-client pulls; larger values reduce round trips
|
|
1147
|
+
let batch_size = typeof opts?.pagination?.batch_size === "number" ? Math.floor(opts.pagination.batch_size) : 1000;
|
|
1148
|
+
if (!Number.isFinite(batch_size) || batch_size < 1 || batch_size > 10000) {
|
|
1149
|
+
throw new InvalidUsageError({
|
|
1150
|
+
message: "Option `pagination.batch_size` must be an integer between '1' and '10000'.",
|
|
1151
|
+
reason: "invalid_pagination_batch_size",
|
|
1152
|
+
field: "opts.pagination.batch_size",
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
// If a finite user limit is set and smaller than 10k, match the batch size to it
|
|
1156
|
+
// (this reduces round-trips and aligns with probing when page_info is requested).
|
|
1157
|
+
if (typeof probing_limit === "number" && probing_limit > 0 && probing_limit < 10000) {
|
|
1158
|
+
batch_size = Math.min(batch_size, probing_limit);
|
|
1159
|
+
}
|
|
1160
|
+
// validate/normalize the user query (and guard against empty queries unless explicitly allowed)
|
|
1161
|
+
const query_op = this._init_query(query, allow_empty_query, "query");
|
|
1162
|
+
// Build driver find options
|
|
1163
|
+
const find_options = {
|
|
1164
|
+
projection: opts?.projection
|
|
1165
|
+
? Collection.Projection.init(this._ensure_version_in_projection(opts.projection))
|
|
1166
|
+
: undefined,
|
|
1167
|
+
sort: opts?.sort,
|
|
1168
|
+
skip: opts?.skip,
|
|
1169
|
+
// no default so we can stream all docs if no limit was set.
|
|
1170
|
+
// allow +1 probe for page_info
|
|
1171
|
+
limit: probing_limit,
|
|
1172
|
+
};
|
|
1173
|
+
// Only set maxTimeMS when a timeout is explicitly provided
|
|
1174
|
+
if (typeof opts?.timeout === "number") {
|
|
1175
|
+
find_options.maxTimeMS = opts.timeout;
|
|
1176
|
+
}
|
|
1177
|
+
try {
|
|
1178
|
+
// Create a find cursor.
|
|
1179
|
+
const cursor = await this._with_retry(() => this._col.find(query_op, this.get_operation_options(find_options)), opts?.retry);
|
|
1180
|
+
// Set batch size here, so its used for all subsequent fetches instead of only the first if it was defined in find_options.
|
|
1181
|
+
cursor.batchSize(batch_size);
|
|
1182
|
+
// Only set maxTimeMS when a timeout is explicitly provided
|
|
1183
|
+
if (typeof opts?.timeout === "number") {
|
|
1184
|
+
cursor.maxTimeMS(opts.timeout);
|
|
1185
|
+
}
|
|
1186
|
+
// Return cursor.
|
|
1187
|
+
if (opts?.cursor)
|
|
1188
|
+
return cursor;
|
|
1189
|
+
// Streaming callback path (memory-friendly).
|
|
1190
|
+
if (has_callback) {
|
|
1191
|
+
const max_docs = user_limit ?? Number.POSITIVE_INFINITY;
|
|
1192
|
+
let processed_count = 0;
|
|
1193
|
+
try {
|
|
1194
|
+
while (processed_count < max_docs) {
|
|
1195
|
+
const first = await this._with_retry(() => cursor.next(), opts?.retry);
|
|
1196
|
+
if (first == null)
|
|
1197
|
+
break;
|
|
1198
|
+
let processed = first;
|
|
1199
|
+
if (processed && typeof processed === "object") {
|
|
1200
|
+
processed = await this.apply_on_load(processed, {
|
|
1201
|
+
projection: opts?.projection,
|
|
1202
|
+
persist: true,
|
|
1203
|
+
await_persist: false,
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
try {
|
|
1207
|
+
await opts.callback(processed);
|
|
1208
|
+
}
|
|
1209
|
+
catch (cb_err) {
|
|
1210
|
+
// Surface callback failure with a dedicated reason
|
|
1211
|
+
throw new Collection.ListError({
|
|
1212
|
+
message: "List callback failed for a streamed document.",
|
|
1213
|
+
query: query_op,
|
|
1214
|
+
reason: "callback_error",
|
|
1215
|
+
cause: cb_err,
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
processed_count++;
|
|
1219
|
+
if (processed_count >= max_docs)
|
|
1220
|
+
break;
|
|
1221
|
+
// Drain current batch without network calls.
|
|
1222
|
+
let drained = 1;
|
|
1223
|
+
while (drained < batch_size && processed_count < max_docs) {
|
|
1224
|
+
const next_in_buffer = await cursor.tryNext();
|
|
1225
|
+
if (next_in_buffer == null)
|
|
1226
|
+
break;
|
|
1227
|
+
let processed2 = next_in_buffer;
|
|
1228
|
+
if (processed2 && typeof processed2 === "object") {
|
|
1229
|
+
processed2 = await this.apply_on_load(processed2, {
|
|
1230
|
+
projection: opts?.projection,
|
|
1231
|
+
persist: true,
|
|
1232
|
+
await_persist: false,
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
try {
|
|
1236
|
+
await opts.callback(processed2);
|
|
1237
|
+
}
|
|
1238
|
+
catch (cb_err) {
|
|
1239
|
+
throw new Collection.ListError({
|
|
1240
|
+
message: "List callback failed for a streamed document.",
|
|
1241
|
+
query: query_op,
|
|
1242
|
+
reason: "callback_error",
|
|
1243
|
+
cause: cb_err,
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
processed_count++;
|
|
1247
|
+
drained++;
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
finally {
|
|
1252
|
+
if (!cursor.closed) {
|
|
1253
|
+
await cursor.close().catch(() => { });
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
return undefined;
|
|
1257
|
+
}
|
|
1258
|
+
// -------- Original array / page_info path (no callback) --------
|
|
1259
|
+
const max_docs = user_limit ?? Number.POSITIVE_INFINITY;
|
|
1260
|
+
const target = page_info_requested && typeof user_limit === "number" ? user_limit + 1 : max_docs;
|
|
1261
|
+
const docs = [];
|
|
1262
|
+
let fetched = 0;
|
|
1263
|
+
try {
|
|
1264
|
+
while (fetched < target) {
|
|
1265
|
+
const first = await this._with_retry(() => cursor.next(), opts?.retry);
|
|
1266
|
+
if (first == null) {
|
|
1267
|
+
// cursor exhausted
|
|
1268
|
+
break;
|
|
1269
|
+
}
|
|
1270
|
+
// Execute on_load / on_transform_version here.
|
|
1271
|
+
let processed = first;
|
|
1272
|
+
if (processed && typeof processed === "object") {
|
|
1273
|
+
processed = await this.apply_on_load(processed, {
|
|
1274
|
+
projection: opts?.projection,
|
|
1275
|
+
persist: true,
|
|
1276
|
+
await_persist: false,
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
docs.push(processed);
|
|
1280
|
+
fetched++;
|
|
1281
|
+
if (fetched >= target) {
|
|
1282
|
+
break;
|
|
1283
|
+
}
|
|
1284
|
+
// Drain the rest of the currently buffered batch WITHOUT retry
|
|
1285
|
+
let drained = 1;
|
|
1286
|
+
while (drained < batch_size && fetched < target) {
|
|
1287
|
+
const next_in_buffer = await cursor.tryNext();
|
|
1288
|
+
if (next_in_buffer == null) {
|
|
1289
|
+
break;
|
|
1290
|
+
}
|
|
1291
|
+
let processed2 = next_in_buffer;
|
|
1292
|
+
if (processed2 && typeof processed2 === "object") {
|
|
1293
|
+
processed2 = await this.apply_on_load(processed2, {
|
|
1294
|
+
projection: opts?.projection,
|
|
1295
|
+
persist: true,
|
|
1296
|
+
await_persist: false,
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
docs.push(processed2);
|
|
1300
|
+
fetched++;
|
|
1301
|
+
drained++;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
finally {
|
|
1306
|
+
if (!cursor.closed) {
|
|
1307
|
+
await cursor.close().catch(() => { });
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
// Return page info.
|
|
1311
|
+
if (page_info_requested) {
|
|
1312
|
+
let has_more = false;
|
|
1313
|
+
let out = docs;
|
|
1314
|
+
if (typeof user_limit === "number" && docs.length > user_limit) {
|
|
1315
|
+
has_more = true;
|
|
1316
|
+
out = docs.slice(0, user_limit);
|
|
1317
|
+
}
|
|
1318
|
+
return { items: out, has_more };
|
|
1319
|
+
}
|
|
1320
|
+
// Return documents.
|
|
1321
|
+
if (docs.length > max_docs && max_docs !== Number.POSITIVE_INFINITY) {
|
|
1322
|
+
return docs.slice(0, max_docs);
|
|
1323
|
+
}
|
|
1324
|
+
return docs;
|
|
1325
|
+
}
|
|
1326
|
+
catch (e) {
|
|
1327
|
+
// If a callback already wrapped the error as a ListError, pass it through unchanged.
|
|
1328
|
+
if (e instanceof Collection.ListError) {
|
|
1329
|
+
if (throw_errors)
|
|
1330
|
+
throw e;
|
|
1331
|
+
return e;
|
|
1332
|
+
}
|
|
1333
|
+
const error = new Collection.ListError({
|
|
1334
|
+
message: "Encountered an error while listing documents.",
|
|
1335
|
+
query: query_op,
|
|
1336
|
+
reason: this._should_retry_error(e)
|
|
1337
|
+
? (Collection.Retry.get_attempts(opts?.retry) > 1 ? "retries_exhausted" : "retryable")
|
|
1338
|
+
: "unknown",
|
|
1339
|
+
cause: e,
|
|
1340
|
+
});
|
|
1341
|
+
if (throw_errors)
|
|
1342
|
+
throw error;
|
|
1343
|
+
return error;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
/**
|
|
1347
|
+
* List all documents of the collection.
|
|
1348
|
+
*
|
|
1349
|
+
* @param opts The list options, see {@link Collection.ListOpts}.
|
|
1350
|
+
*
|
|
1351
|
+
* @note The `opts.throw` option defaults to `true`.
|
|
1352
|
+
* @note The {@link Collection.Opts.on_load} and {@link Collection.Opts.on_transform_version} callbacks
|
|
1353
|
+
* are not executed when `opts.cursor === true`.
|
|
1354
|
+
* @note When `opts.callback` is a function (and `opts.cursor !== true`), this method streams documents and
|
|
1355
|
+
* invokes the callback for each processed document, then returns `undefined` on success.
|
|
1356
|
+
*
|
|
1357
|
+
* @returns
|
|
1358
|
+
* - Array of all documents in the collection.
|
|
1359
|
+
* - The find cursor when `opts.cursor === true`.
|
|
1360
|
+
* - `undefined` when `opts.callback && !opts.cursor`.
|
|
1361
|
+
* - An error if `opts.throw === false` and a {@link Collection.ListError} has occurred.
|
|
1362
|
+
*
|
|
1363
|
+
* @throws {Collection.ListError} When `throw !== false` if an error occurred during the operation, in which case {@link Collection.ListError.cause} is defined.
|
|
1364
|
+
* @throws {InvalidUsageError} (always) When the provided argument(s) are invalid or if the collection was not used properly.
|
|
1365
|
+
*/
|
|
1366
|
+
async list_all(opts) {
|
|
1367
|
+
return this.list({}, opts, true);
|
|
1368
|
+
}
|
|
1369
|
+
/**
|
|
1370
|
+
* Check if a document exists by only loading the document's id.
|
|
1371
|
+
*
|
|
1372
|
+
* @param query The database path to the document.
|
|
1373
|
+
* @param opts The exists options, see {@link Collection.ExistsOpts}.
|
|
1374
|
+
*
|
|
1375
|
+
* @note The `opts.throw` option defaults to `true`.
|
|
1376
|
+
* @note This method does not execute the {@link Collection.Opts.on_load}
|
|
1377
|
+
* and {@link Collection.Opts.on_transform_version} callbacks.
|
|
1378
|
+
*
|
|
1379
|
+
* @returns
|
|
1380
|
+
* - An error if `opts.throw === false` and a {@link Collection.ExistsError} has occurred.
|
|
1381
|
+
* - True if the document exists, false otherwise.
|
|
1382
|
+
*
|
|
1383
|
+
* @throws {Collection.ExistsError} When `throw !== false` if an error occurred during the operation, in which case {@link Collection.ExistsError.cause} is defined.
|
|
1384
|
+
* @throws {InvalidUsageError} (always) When the provided argument(s) are invalid or if the collection was not used properly.
|
|
1385
|
+
*/
|
|
1386
|
+
async exists(query, opts) {
|
|
1387
|
+
// Asserts.
|
|
1388
|
+
if (!this.initialized) {
|
|
1389
|
+
await this.init();
|
|
1390
|
+
}
|
|
1391
|
+
this.assert_init();
|
|
1392
|
+
this.assert_not_finalized();
|
|
1393
|
+
// Init query.
|
|
1394
|
+
const query_op = this._init_query(query, false, "query");
|
|
1395
|
+
// Unpack opts.
|
|
1396
|
+
const throw_errors = opts?.throw ?? true; // Warning: NEVER change this default
|
|
1397
|
+
// Apply operation.
|
|
1398
|
+
try {
|
|
1399
|
+
const find_opts = {
|
|
1400
|
+
projection: { _id: 1 },
|
|
1401
|
+
};
|
|
1402
|
+
if (typeof opts?.timeout === "number") {
|
|
1403
|
+
find_opts.maxTimeMS = opts.timeout;
|
|
1404
|
+
}
|
|
1405
|
+
const doc = await this._with_retry(() => this._col.findOne(query_op, this.get_operation_options(find_opts)), opts?.retry);
|
|
1406
|
+
return doc != null;
|
|
1407
|
+
// Catch error.
|
|
1408
|
+
}
|
|
1409
|
+
catch (e) {
|
|
1410
|
+
// Encountered a non retryable error or no retries (left).
|
|
1411
|
+
const err = new Collection.ExistsError({
|
|
1412
|
+
message: 'Failed to check if the queried document exists due to an unexpected error.',
|
|
1413
|
+
query: query_op,
|
|
1414
|
+
reason: this._should_retry_error(e)
|
|
1415
|
+
? (Collection.Retry.get_attempts(opts?.retry) > 1 ? 'retries_exhausted' : 'retryable')
|
|
1416
|
+
: 'unknown',
|
|
1417
|
+
cause: e,
|
|
1418
|
+
});
|
|
1419
|
+
if (throw_errors)
|
|
1420
|
+
throw err;
|
|
1421
|
+
return err;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
/**
|
|
1425
|
+
* Load a single document by query.
|
|
1426
|
+
*
|
|
1427
|
+
* Applies an optional projection and, if a `default` is provided, inserts any
|
|
1428
|
+
* missing keys from the default into the loaded document (values are deep-cloned).
|
|
1429
|
+
*
|
|
1430
|
+
* @note The `default` value is deep-cloned if it is returned or inserted.
|
|
1431
|
+
* @note The `opts.throw` option defaults to `true`.
|
|
1432
|
+
*
|
|
1433
|
+
* @param query The database query.
|
|
1434
|
+
* @param opts Additional load options {@link Collection.LoadOpts}.
|
|
1435
|
+
*
|
|
1436
|
+
* @returns
|
|
1437
|
+
* - When `opts.throw === false`:
|
|
1438
|
+
* - If found: the loaded (projected) document.
|
|
1439
|
+
* - If not found and `opts.default` is provided: the deep-cloned default data.
|
|
1440
|
+
* - If not found and no default: a {@link Collection.NotFoundError}.
|
|
1441
|
+
* - On load failure: a {@link Collection.LoadError}.
|
|
1442
|
+
* - When `opts.throw !== false` (default):
|
|
1443
|
+
* - If found: the loaded (projected) document.
|
|
1444
|
+
* - If not found and `opts.default` is provided: the deep-cloned default data.
|
|
1445
|
+
* - If not found and no default: a {@link Collection.NotFoundError} is **thrown**.
|
|
1446
|
+
* - On load failure: a {@link Collection.LoadError} is **thrown**.
|
|
1447
|
+
*
|
|
1448
|
+
* @throws {Collection.LoadError} Only when `opts.throw !== false` and the load fails.
|
|
1449
|
+
* @throws {Collection.NotFoundError} When the document is not found and `opts.throw !== false && opts.default == null`.
|
|
1450
|
+
* @throws {InvalidUsageError} When the provided arguments are invalid or if the collection was not used properly.
|
|
1451
|
+
*/
|
|
1452
|
+
async load(query, opts) {
|
|
1453
|
+
// Checks.
|
|
1454
|
+
if (!this.initialized) {
|
|
1455
|
+
await this.init();
|
|
1456
|
+
}
|
|
1457
|
+
this.assert_init();
|
|
1458
|
+
this.assert_not_finalized();
|
|
1459
|
+
// Unpack opts.
|
|
1460
|
+
const retry = opts?.retry;
|
|
1461
|
+
const throw_errors = opts?.throw ?? true; // Warning: NEVER change this default
|
|
1462
|
+
// Init query.
|
|
1463
|
+
const find_query = this._init_query(query, false, "query");
|
|
1464
|
+
// Create options.
|
|
1465
|
+
const base_find = {};
|
|
1466
|
+
if (opts?.projection)
|
|
1467
|
+
base_find.projection = Collection.Projection.init(this._ensure_version_in_projection(opts.projection));
|
|
1468
|
+
if (typeof opts?.timeout === "number")
|
|
1469
|
+
base_find.maxTimeMS = opts.timeout;
|
|
1470
|
+
const find_opts = this.get_operation_options(base_find);
|
|
1471
|
+
// Load doc.
|
|
1472
|
+
try {
|
|
1473
|
+
const doc = await this._with_retry(() => this._col.findOne(find_query, find_opts), opts?.retry);
|
|
1474
|
+
// Handle default.
|
|
1475
|
+
if (!doc) {
|
|
1476
|
+
if (opts?.default) {
|
|
1477
|
+
let default_doc;
|
|
1478
|
+
if (typeof opts.default === "function") {
|
|
1479
|
+
default_doc = vlib.Object.deep_copy(opts.default());
|
|
1480
|
+
}
|
|
1481
|
+
else {
|
|
1482
|
+
default_doc = vlib.Object.deep_copy(opts.default);
|
|
1483
|
+
}
|
|
1484
|
+
if (default_doc._id == null) {
|
|
1485
|
+
default_doc._id = new mongodb.ObjectId();
|
|
1486
|
+
}
|
|
1487
|
+
if (this.record_version != null) {
|
|
1488
|
+
default_doc.__record_version = this.record_version;
|
|
1489
|
+
}
|
|
1490
|
+
// Execute on_load for defaults as well.
|
|
1491
|
+
let out = default_doc;
|
|
1492
|
+
const is_partial = this._is_partial_projection(opts?.projection);
|
|
1493
|
+
out = await this.apply_on_load(out, {
|
|
1494
|
+
projection: opts?.projection,
|
|
1495
|
+
persist: false, // do not persist defaults.
|
|
1496
|
+
await_persist: true,
|
|
1497
|
+
});
|
|
1498
|
+
return out;
|
|
1499
|
+
}
|
|
1500
|
+
const err = new Collection.NotFoundError({
|
|
1501
|
+
message: 'Document not found.',
|
|
1502
|
+
query: find_query,
|
|
1503
|
+
reason: "not_found",
|
|
1504
|
+
});
|
|
1505
|
+
if (throw_errors)
|
|
1506
|
+
throw err;
|
|
1507
|
+
return err;
|
|
1508
|
+
}
|
|
1509
|
+
// Insert default keys.
|
|
1510
|
+
let working = doc;
|
|
1511
|
+
if (opts?.default) {
|
|
1512
|
+
if (typeof opts.default === "function") {
|
|
1513
|
+
Collection.insert_defaults(working, opts.default(), { clone: true });
|
|
1514
|
+
}
|
|
1515
|
+
else {
|
|
1516
|
+
Collection.insert_defaults(working, opts.default, { clone: true });
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
// Execute on_transform_version/on_load callbacks.
|
|
1520
|
+
working = await this.apply_on_load(working, {
|
|
1521
|
+
projection: opts?.projection,
|
|
1522
|
+
persist: true,
|
|
1523
|
+
await_persist: true,
|
|
1524
|
+
});
|
|
1525
|
+
return working;
|
|
1526
|
+
}
|
|
1527
|
+
catch (e) {
|
|
1528
|
+
if (e instanceof Collection.NotFoundError) {
|
|
1529
|
+
if (throw_errors)
|
|
1530
|
+
throw e;
|
|
1531
|
+
return e;
|
|
1532
|
+
}
|
|
1533
|
+
// Encountered a non retryable error or no retries (left).
|
|
1534
|
+
const err = new Collection.LoadError({
|
|
1535
|
+
message: 'Load failed due to an unexpected error.',
|
|
1536
|
+
query: find_query,
|
|
1537
|
+
reason: this._should_retry_error(e)
|
|
1538
|
+
? (Collection.Retry.get_attempts(retry) > 1 ? 'retries_exhausted' : 'retryable')
|
|
1539
|
+
: 'unknown',
|
|
1540
|
+
cause: e,
|
|
1541
|
+
});
|
|
1542
|
+
if (throw_errors)
|
|
1543
|
+
throw err;
|
|
1544
|
+
return err;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
/**
|
|
1548
|
+
* Save data with predefined `$set` behaviour.
|
|
1549
|
+
* When the document already exists this function only updates the specified content attributes.
|
|
1550
|
+
* When a document does not exist it will automatically be created, unless `opts.upsert !== false`.
|
|
1551
|
+
*
|
|
1552
|
+
* @param query The database query / path to the document.
|
|
1553
|
+
* @param content The data to save.
|
|
1554
|
+
* @param opts Additional options, see {@link Collection.SetOpts}.
|
|
1555
|
+
*
|
|
1556
|
+
* @note The `opts.throw` option defaults to `true`.
|
|
1557
|
+
* @note The `opts.upsert` option defaults to `true`.
|
|
1558
|
+
*
|
|
1559
|
+
* @returns
|
|
1560
|
+
* - When `opts.bulk === true`: an unexecuted bulk operation.
|
|
1561
|
+
* - When `opts.return === true`: the **updated** document; or a {@link Collection.SaveError} when `throw:false` and a write failure occurs.
|
|
1562
|
+
* - Otherwise: `undefined` on success; or a {@link Collection.SaveError} when `throw:false` and a write failure occurs.
|
|
1563
|
+
*
|
|
1564
|
+
* @throws {Collection.SaveError} Only when `opts.throw !== false` and the write fails.
|
|
1565
|
+
* @throws {InvalidUsageError} (always) When the provided argument(s) are invalid or if the collection was not used properly.
|
|
1566
|
+
*/
|
|
1567
|
+
async set(query, content, opts) {
|
|
1568
|
+
// Flatten.
|
|
1569
|
+
if (opts?.flatten)
|
|
1570
|
+
content = this.flatten(content);
|
|
1571
|
+
// Create op.
|
|
1572
|
+
const operation = { $set: content };
|
|
1573
|
+
// Save.
|
|
1574
|
+
return await this.save(query, operation, opts);
|
|
1575
|
+
}
|
|
1576
|
+
/**
|
|
1577
|
+
* Save a single document without performing any default `$set` or `$inc` like operations.
|
|
1578
|
+
* When a document does not exist it will automatically be created unless `opts.upsert === false`.
|
|
1579
|
+
*
|
|
1580
|
+
* @param query The database query / path to the document.
|
|
1581
|
+
* @param operation The MongoDB update document or pipeline (e.g. `{ $set: { key: value } }`).
|
|
1582
|
+
* @param opts Additional options, see {@link Collection.SaveOpts}.
|
|
1583
|
+
*
|
|
1584
|
+
* @note The `opts.throw` option defaults to `true`.
|
|
1585
|
+
* @note The `opts.upsert` option defaults to `true`.
|
|
1586
|
+
* @note Replacement documents are not allowed here. An update operator
|
|
1587
|
+
* document (e.g. `$set`, `$inc`) or an aggregation pipeline is required.
|
|
1588
|
+
* To replace a document use {@link replace}.
|
|
1589
|
+
*
|
|
1590
|
+
* @returns
|
|
1591
|
+
* - When `opts.bulk === true`: an unexecuted bulk operation.
|
|
1592
|
+
* - When `opts.return === true`: the **updated** document; or a {@link Collection.SaveError} when `throw:false` and a write failure occurs.
|
|
1593
|
+
* - Otherwise: {@link mongodb.UpdateResult} on success; or a {@link Collection.SaveError} when `throw:false` and a write failure occurs.
|
|
1594
|
+
*
|
|
1595
|
+
* @throws {Collection.SaveError} Only when `opts.throw !== false` and the write fails.
|
|
1596
|
+
* @throws {InvalidUsageError} (always) When the provided argument(s) are invalid or if the collection was not used properly.
|
|
1597
|
+
*/
|
|
1598
|
+
async save(query, operation, // @todo add strict pipeline type.
|
|
1599
|
+
opts) {
|
|
1600
|
+
// Checks.
|
|
1601
|
+
if (!this.initialized) {
|
|
1602
|
+
await this.init();
|
|
1603
|
+
}
|
|
1604
|
+
this.assert_init();
|
|
1605
|
+
this.assert_not_finalized();
|
|
1606
|
+
// Validate update shape BEFORE we mutate it with TTL/version logic.
|
|
1607
|
+
// Plain replacement docs are not supported with updateOne/findOneAndUpdate.
|
|
1608
|
+
if (!this._is_operator_update_or_pipeline(operation)) {
|
|
1609
|
+
throw new InvalidUsageError({
|
|
1610
|
+
message: "Plain replacement documents are not allowed for 'save()' (uses updateOne/findOneAndUpdate). " +
|
|
1611
|
+
"Pass an operator update or aggregation pipeline. " +
|
|
1612
|
+
"To replace a document, call 'replace()'.",
|
|
1613
|
+
reason: "invalid_update_document",
|
|
1614
|
+
field: "operation",
|
|
1615
|
+
});
|
|
1616
|
+
}
|
|
1617
|
+
// Init query.
|
|
1618
|
+
const query_op = this._init_query(query, false, "query");
|
|
1619
|
+
// Unpack opts.
|
|
1620
|
+
const throw_errors = opts?.throw ?? true; // NEVER change this default.
|
|
1621
|
+
const retry = opts?.retry;
|
|
1622
|
+
const upsert = opts?.upsert ?? true; // NEVER change this default.
|
|
1623
|
+
// Apply TTL.
|
|
1624
|
+
if (this.ttl_enabled && opts?.apply_ttl !== false)
|
|
1625
|
+
this._apply_ttl_to_operation(operation, upsert);
|
|
1626
|
+
// Apply record versioning.
|
|
1627
|
+
if (this.record_version != null)
|
|
1628
|
+
this._apply_record_version_to_operation(operation, upsert);
|
|
1629
|
+
// Bulk operation.
|
|
1630
|
+
if (opts?.bulk) {
|
|
1631
|
+
const b_op = {
|
|
1632
|
+
updateOne: {
|
|
1633
|
+
filter: query_op,
|
|
1634
|
+
update: operation,
|
|
1635
|
+
upsert: upsert,
|
|
1636
|
+
},
|
|
1637
|
+
};
|
|
1638
|
+
return b_op;
|
|
1639
|
+
}
|
|
1640
|
+
// Return document.
|
|
1641
|
+
if (opts?.return) {
|
|
1642
|
+
let res;
|
|
1643
|
+
try {
|
|
1644
|
+
res = await this._with_retry(() => this._col.findOneAndUpdate(query_op, operation, this.get_operation_options({
|
|
1645
|
+
upsert,
|
|
1646
|
+
returnDocument: mongodb.ReturnDocument.AFTER,
|
|
1647
|
+
includeResultMetadata: false,
|
|
1648
|
+
...(typeof opts?.timeout === "number" ? { maxTimeMS: opts.timeout } : {}),
|
|
1649
|
+
})), retry);
|
|
1650
|
+
}
|
|
1651
|
+
catch (e) {
|
|
1652
|
+
// Encountered a non retryable error or no retries (left).
|
|
1653
|
+
const err = new Collection.SaveError({
|
|
1654
|
+
message: 'Update failed due to an unexpected error.',
|
|
1655
|
+
query: query_op,
|
|
1656
|
+
reason: this._should_retry_error(e)
|
|
1657
|
+
? (Collection.Retry.get_attempts(retry) > 1 ? 'retries_exhausted' : 'retryable')
|
|
1658
|
+
: 'unknown',
|
|
1659
|
+
cause: e,
|
|
1660
|
+
});
|
|
1661
|
+
if (throw_errors)
|
|
1662
|
+
throw err;
|
|
1663
|
+
return err;
|
|
1664
|
+
}
|
|
1665
|
+
if (!res) {
|
|
1666
|
+
const err = new Collection.SaveError({
|
|
1667
|
+
message: 'Document write was not acknowledged.',
|
|
1668
|
+
query: query_op,
|
|
1669
|
+
reason: 'not_acknowledged',
|
|
1670
|
+
});
|
|
1671
|
+
if (throw_errors)
|
|
1672
|
+
throw err;
|
|
1673
|
+
return err;
|
|
1674
|
+
}
|
|
1675
|
+
// Apply on_load / on_transform_version on returned document.
|
|
1676
|
+
try {
|
|
1677
|
+
const processed = await this.apply_on_load(res, {
|
|
1678
|
+
projection: undefined,
|
|
1679
|
+
persist: true,
|
|
1680
|
+
await_persist: true,
|
|
1681
|
+
});
|
|
1682
|
+
return processed;
|
|
1683
|
+
}
|
|
1684
|
+
catch (e) {
|
|
1685
|
+
const err = new Collection.SaveError({
|
|
1686
|
+
message: 'Update succeeded but post-load processing failed.',
|
|
1687
|
+
query: query_op,
|
|
1688
|
+
reason: 'post_process_failed',
|
|
1689
|
+
cause: e,
|
|
1690
|
+
});
|
|
1691
|
+
if (throw_errors)
|
|
1692
|
+
throw err;
|
|
1693
|
+
return err;
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
// Dont return document.
|
|
1697
|
+
else {
|
|
1698
|
+
let res;
|
|
1699
|
+
try {
|
|
1700
|
+
res = await this._with_retry(() => this._col.updateOne(query_op, operation, this.get_operation_options({
|
|
1701
|
+
upsert,
|
|
1702
|
+
...(typeof opts?.timeout === "number" ? { maxTimeMS: opts.timeout } : {}),
|
|
1703
|
+
})), retry);
|
|
1704
|
+
}
|
|
1705
|
+
catch (e) {
|
|
1706
|
+
// Encountered a non retryable error or no retries (left).
|
|
1707
|
+
const err = new Collection.SaveError({
|
|
1708
|
+
message: 'Update failed due to an unexpected error.',
|
|
1709
|
+
query: query_op,
|
|
1710
|
+
reason: this._should_retry_error(e)
|
|
1711
|
+
? (Collection.Retry.get_attempts(retry) > 1 ? 'retries_exhausted' : 'retryable')
|
|
1712
|
+
: 'unknown',
|
|
1713
|
+
cause: e,
|
|
1714
|
+
});
|
|
1715
|
+
if (throw_errors)
|
|
1716
|
+
throw err;
|
|
1717
|
+
return err;
|
|
1718
|
+
}
|
|
1719
|
+
if (!res.acknowledged || (res.matchedCount === 0 && res.upsertedCount === 0)) {
|
|
1720
|
+
const err = new Collection.SaveError({
|
|
1721
|
+
message: !res.acknowledged
|
|
1722
|
+
? 'Document write was not acknowledged.'
|
|
1723
|
+
: 'No document matched the filter and no upsert occurred.',
|
|
1724
|
+
query: query_op,
|
|
1725
|
+
reason: !res.acknowledged ?
|
|
1726
|
+
'not_acknowledged' :
|
|
1727
|
+
'no_match'
|
|
1728
|
+
});
|
|
1729
|
+
if (throw_errors)
|
|
1730
|
+
throw err;
|
|
1731
|
+
return err;
|
|
1732
|
+
}
|
|
1733
|
+
return res;
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
/**
|
|
1737
|
+
* Save multiple documents without performing any default `$set` or `$inc` operations.
|
|
1738
|
+
* Uses MongoDB `updateMany` (unlike {@link save}, which uses `updateOne`).
|
|
1739
|
+
*
|
|
1740
|
+
* @param query The database query / path to the documents.
|
|
1741
|
+
* @param operation The MongoDB update document or pipeline (e.g. `{ $set: { ... } }`).
|
|
1742
|
+
* @param opts Additional options, see {@link Collection.SaveManyOpts}.
|
|
1743
|
+
*
|
|
1744
|
+
* @note The `opts.throw` option defaults to `true`.
|
|
1745
|
+
* @note The `opts.upsert` option defaults to `false` (unlike {@link save}, which defaults to `true`).
|
|
1746
|
+
* @note When `opts.return` is truthy, this performs a **follow-up** {@link list} with the same `query`
|
|
1747
|
+
* to return the (post-update) documents. This is **less efficient** than `save(..., { return: true })`
|
|
1748
|
+
* because it requires an additional list query after the write.
|
|
1749
|
+
* @note If the follow-up `list()` fails:
|
|
1750
|
+
* - with `opts.throw !== false`, it will throw a {@link Collection.ListError};
|
|
1751
|
+
* - with `opts.throw === false`, it will return a {@link Collection.ListError}.
|
|
1752
|
+
*
|
|
1753
|
+
* @returns
|
|
1754
|
+
* - When `opts.bulk === true`: an unexecuted bulk operation (`{ updateMany: ... }`).
|
|
1755
|
+
* - When `opts.return` is falsy: {@link mongodb.UpdateResult} on success; or a {@link Collection.SaveError} when `throw:false`.
|
|
1756
|
+
* - When `opts.return` is truthy: the matched/updated docs (via `list()`); or
|
|
1757
|
+
* - a {@link Collection.SaveError} when the write fails and `throw:false`, or
|
|
1758
|
+
* - a {@link Collection.ListError} when the follow-up read fails and `throw:false`.
|
|
1759
|
+
*
|
|
1760
|
+
* @throws {Collection.SaveError} Only when `opts.throw !== false` and the write fails.
|
|
1761
|
+
* @throws {Collection.ListError} Only when `opts.throw !== false` and the follow-up list fails.
|
|
1762
|
+
* @throws {InvalidUsageError} (always) When the provided argument(s) are invalid or the collection was misused.
|
|
1763
|
+
*/
|
|
1764
|
+
async save_many(query, operation, opts) {
|
|
1765
|
+
// Asserts / init.
|
|
1766
|
+
if (!this.initialized) {
|
|
1767
|
+
await this.init();
|
|
1768
|
+
}
|
|
1769
|
+
this.assert_init();
|
|
1770
|
+
this.assert_not_finalized();
|
|
1771
|
+
// Validate update shape BEFORE we mutate it with TTL/version logic.
|
|
1772
|
+
// Plain replacement docs are not supported with updateMany
|
|
1773
|
+
if (!this._is_operator_update_or_pipeline(operation)) {
|
|
1774
|
+
throw new InvalidUsageError({
|
|
1775
|
+
message: "Plain replacement documents are not allowed for 'save_many()' (uses updateMany). " +
|
|
1776
|
+
"Pass an operator update or aggregation pipeline. " +
|
|
1777
|
+
"To replace documents, call 'replace_many()'.",
|
|
1778
|
+
reason: "invalid_update_document",
|
|
1779
|
+
field: "operation",
|
|
1780
|
+
});
|
|
1781
|
+
}
|
|
1782
|
+
const query_op = this._init_query(query, false, "query");
|
|
1783
|
+
const throw_errors = opts?.throw ?? true; // default true
|
|
1784
|
+
const retry = opts?.retry;
|
|
1785
|
+
const upsert = opts?.upsert ?? false; // default false for save_many
|
|
1786
|
+
// Apply TTL
|
|
1787
|
+
if (this.ttl_enabled && opts?.apply_ttl !== false) {
|
|
1788
|
+
this._apply_ttl_to_operation(operation, upsert);
|
|
1789
|
+
}
|
|
1790
|
+
// Apply record versioning.
|
|
1791
|
+
if (this.record_version != null) {
|
|
1792
|
+
this._apply_record_version_to_operation(operation, upsert);
|
|
1793
|
+
}
|
|
1794
|
+
// Bulk path.
|
|
1795
|
+
if (opts?.bulk) {
|
|
1796
|
+
const b_op = {
|
|
1797
|
+
updateMany: {
|
|
1798
|
+
filter: query_op,
|
|
1799
|
+
update: operation,
|
|
1800
|
+
upsert,
|
|
1801
|
+
},
|
|
1802
|
+
};
|
|
1803
|
+
return b_op;
|
|
1804
|
+
}
|
|
1805
|
+
// Perform write operation.
|
|
1806
|
+
let write;
|
|
1807
|
+
try {
|
|
1808
|
+
write = await this._with_retry(() => this._col.updateMany(query_op, operation, this.get_operation_options({
|
|
1809
|
+
upsert,
|
|
1810
|
+
...(typeof opts?.timeout === "number" ? { maxTimeMS: opts.timeout } : {}),
|
|
1811
|
+
})), retry);
|
|
1812
|
+
}
|
|
1813
|
+
catch (e) {
|
|
1814
|
+
const err = new Collection.SaveError({
|
|
1815
|
+
message: "Update-many failed due to an unexpected error.",
|
|
1816
|
+
query: query_op,
|
|
1817
|
+
reason: this._should_retry_error(e)
|
|
1818
|
+
? (Collection.Retry.get_attempts(retry) > 1 ? 'retries_exhausted' : 'retryable')
|
|
1819
|
+
: 'unknown',
|
|
1820
|
+
cause: e,
|
|
1821
|
+
});
|
|
1822
|
+
if (throw_errors)
|
|
1823
|
+
throw err;
|
|
1824
|
+
return err;
|
|
1825
|
+
}
|
|
1826
|
+
// Acknowledgement / match check (mirror `save` semantics)
|
|
1827
|
+
if (!write.acknowledged || (write.matchedCount === 0 && write.upsertedCount === 0)) {
|
|
1828
|
+
const err = new Collection.SaveError({
|
|
1829
|
+
message: !write.acknowledged
|
|
1830
|
+
? "Document write was not acknowledged."
|
|
1831
|
+
: "No document matched the filter and no upsert occurred.",
|
|
1832
|
+
query: query_op,
|
|
1833
|
+
reason: !write.acknowledged ? "not_acknowledged" : "no_match",
|
|
1834
|
+
});
|
|
1835
|
+
if (throw_errors)
|
|
1836
|
+
throw err;
|
|
1837
|
+
return err;
|
|
1838
|
+
}
|
|
1839
|
+
// No follow-up read requested
|
|
1840
|
+
if (!opts?.return) {
|
|
1841
|
+
return write;
|
|
1842
|
+
}
|
|
1843
|
+
// --- Follow-up read phase (list): keep ListError semantics intact ---
|
|
1844
|
+
const follow = typeof opts.return === "object" ? opts.return : {};
|
|
1845
|
+
// Let ListError bubble (throw:true) or be returned (throw:false) unchanged.
|
|
1846
|
+
const out = await this.list(query, {
|
|
1847
|
+
...follow,
|
|
1848
|
+
// copy control fields from the write options
|
|
1849
|
+
throw: opts.throw,
|
|
1850
|
+
retry: opts.retry,
|
|
1851
|
+
timeout: opts.timeout,
|
|
1852
|
+
// Note: we intentionally do NOT set cursor/page_info (they're excluded in SaveManyReturnOpts).
|
|
1853
|
+
});
|
|
1854
|
+
return out;
|
|
1855
|
+
}
|
|
1856
|
+
/**
|
|
1857
|
+
* Build an aggregation replacement pipeline that preserves _id on matches and
|
|
1858
|
+
* applies versioning/TTL consistently with non-pipeline paths.
|
|
1859
|
+
*
|
|
1860
|
+
* - On matches: preserve stored `__record_version` and (for static TTL) stored `__ttl_timestamp`.
|
|
1861
|
+
* - On upserts:
|
|
1862
|
+
* - `__record_version`: respect user value if provided, else stamp `this.record_version`.
|
|
1863
|
+
* - `__ttl_timestamp`:
|
|
1864
|
+
* • sliding TTL → always set to "now"
|
|
1865
|
+
* • static TTL → respect user value if provided, else set to "now"
|
|
1866
|
+
*
|
|
1867
|
+
* @param base_replacement A shallow clone of the user replacement. For replace_many, pass without `_id`.
|
|
1868
|
+
* @param upsert Whether the write is an upsert.
|
|
1869
|
+
* @param apply_ttl Whether TTL logic should be applied (`this.ttl_enabled && opts?.apply_ttl !== false`).
|
|
1870
|
+
* @returns A MongoDB aggregation pipeline that performs the replacement.
|
|
1871
|
+
*/
|
|
1872
|
+
_build_replace_pipeline(base_replacement, upsert, apply_ttl) {
|
|
1873
|
+
const now = new Date();
|
|
1874
|
+
// Merge order matters (later overrides earlier):
|
|
1875
|
+
// 1) user replacement
|
|
1876
|
+
// 2) existing _id on matches
|
|
1877
|
+
// 3) carry stored values we may need to preserve
|
|
1878
|
+
const merge_objects = [
|
|
1879
|
+
base_replacement,
|
|
1880
|
+
{
|
|
1881
|
+
$cond: [
|
|
1882
|
+
{ $ne: ["$_id", null] },
|
|
1883
|
+
{ _id: "$_id" },
|
|
1884
|
+
{}
|
|
1885
|
+
]
|
|
1886
|
+
},
|
|
1887
|
+
];
|
|
1888
|
+
if (this.record_version != null) {
|
|
1889
|
+
// capture stored version (only present on matches)
|
|
1890
|
+
merge_objects.push({ __old_rv: "$__record_version" });
|
|
1891
|
+
}
|
|
1892
|
+
if (apply_ttl) {
|
|
1893
|
+
// capture stored TTL for static TTL preservation on matches
|
|
1894
|
+
merge_objects.push({ __old_ttl: "$__ttl_timestamp" });
|
|
1895
|
+
}
|
|
1896
|
+
else {
|
|
1897
|
+
// explicit preservation when TTL logic is disabled
|
|
1898
|
+
merge_objects.push({ __ttl_timestamp: "$__ttl_timestamp" });
|
|
1899
|
+
}
|
|
1900
|
+
const pipeline = [
|
|
1901
|
+
{ $replaceWith: { $mergeObjects: merge_objects } },
|
|
1902
|
+
];
|
|
1903
|
+
// ----- Record versioning -----
|
|
1904
|
+
if (this.record_version != null) {
|
|
1905
|
+
pipeline.push({
|
|
1906
|
+
$set: {
|
|
1907
|
+
/**
|
|
1908
|
+
* Matches:
|
|
1909
|
+
* Prefer stored version (`__old_rv`), otherwise keep any user-provided value.
|
|
1910
|
+
* Upserts:
|
|
1911
|
+
* Respect user-provided value if present; otherwise default to `this.record_version`.
|
|
1912
|
+
*/
|
|
1913
|
+
__record_version: {
|
|
1914
|
+
$cond: [
|
|
1915
|
+
{ $ne: ["$__old_rv", null] },
|
|
1916
|
+
"$__old_rv",
|
|
1917
|
+
upsert
|
|
1918
|
+
? { $ifNull: ["$__record_version", this.record_version] }
|
|
1919
|
+
: "$__record_version"
|
|
1920
|
+
]
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
});
|
|
1924
|
+
}
|
|
1925
|
+
// ----- TTL stamping/preservation -----
|
|
1926
|
+
if (apply_ttl) {
|
|
1927
|
+
pipeline.push({
|
|
1928
|
+
$set: this.sliding_ttl
|
|
1929
|
+
// Always refresh on any write
|
|
1930
|
+
? { __ttl_timestamp: now }
|
|
1931
|
+
// Static TTL: preserve on matches; on upsert, respect user value if any, else stamp now
|
|
1932
|
+
: {
|
|
1933
|
+
__ttl_timestamp: {
|
|
1934
|
+
$cond: [
|
|
1935
|
+
{ $ne: ["$__old_ttl", null] },
|
|
1936
|
+
"$__old_ttl",
|
|
1937
|
+
{ $ifNull: ["$__ttl_timestamp", now] }
|
|
1938
|
+
]
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
});
|
|
1942
|
+
}
|
|
1943
|
+
// Cleanup temp carriers
|
|
1944
|
+
pipeline.push({ $unset: ["__old_rv", "__old_ttl"] });
|
|
1945
|
+
return pipeline;
|
|
1946
|
+
}
|
|
1947
|
+
/**
|
|
1948
|
+
* Replace a single document.
|
|
1949
|
+
* Accepts a replacement document only (no update operators/pipelines).
|
|
1950
|
+
*
|
|
1951
|
+
* Internally uses an aggregation pipeline to emulate a full replacement while preserving `_id`
|
|
1952
|
+
* for matched documents and applying record-version/TTL semantics consistently.
|
|
1953
|
+
*
|
|
1954
|
+
* @param query The match filter.
|
|
1955
|
+
* @param replacement The replacement document, no `$` operators.
|
|
1956
|
+
* @param opts Options, see {@link Collection.ReplaceOpts}.
|
|
1957
|
+
*
|
|
1958
|
+
* @note The `opts.throw` option defaults to `true`.
|
|
1959
|
+
* @note The `opts.upsert` option defaults to `true`.
|
|
1960
|
+
* @note TTL semantics:
|
|
1961
|
+
* - When `opts.apply_ttl === false` (or TTL is disabled), the existing TTL is preserved for matched docs.
|
|
1962
|
+
* - With sliding TTL, `__ttl_timestamp` is refreshed on every write.
|
|
1963
|
+
* - With static TTL, matched docs keep their original TTL; upserts receive a fresh timestamp.
|
|
1964
|
+
*
|
|
1965
|
+
* @warning Updating the document id `_id` will cause undefined behaviour on matches. On matched documents,
|
|
1966
|
+
* a user-supplied `_id` is ignored and the existing `_id` is preserved. On true upserts, a
|
|
1967
|
+
* user-supplied `_id` is allowed and will be used by the server.
|
|
1968
|
+
*
|
|
1969
|
+
* @returns
|
|
1970
|
+
* - When `opts.bulk === true`: an unexecuted bulk operation.
|
|
1971
|
+
* - When `opts.return === true`: the **updated** document; or a {@link Collection.SaveError} when `throw:false` and a write failure occurs.
|
|
1972
|
+
* - Otherwise: {@link mongodb.UpdateResult} on success; or a {@link Collection.SaveError} when `throw:false` and a write failure occurs.
|
|
1973
|
+
*
|
|
1974
|
+
* @throws {Collection.SaveError} Only when `opts.throw !== false` and the write fails.
|
|
1975
|
+
* @throws {InvalidUsageError} (always) When the provided argument(s) are invalid or if the collection was not used properly.
|
|
1976
|
+
*/
|
|
1977
|
+
async replace(query, replacement, opts) {
|
|
1978
|
+
// Asserts / init.
|
|
1979
|
+
if (!this.initialized) {
|
|
1980
|
+
await this.init();
|
|
1981
|
+
}
|
|
1982
|
+
this.assert_init();
|
|
1983
|
+
this.assert_not_finalized();
|
|
1984
|
+
// Validate "replacement-only".
|
|
1985
|
+
if (this._is_operator_update_or_pipeline(replacement)) {
|
|
1986
|
+
throw new InvalidUsageError({
|
|
1987
|
+
message: "The 'replace()' method accepts a replacement document only (no update operators or pipelines).",
|
|
1988
|
+
reason: "invalid_replacement_document",
|
|
1989
|
+
field: "replacement",
|
|
1990
|
+
});
|
|
1991
|
+
}
|
|
1992
|
+
const query_op = this._init_query(query, false, "query");
|
|
1993
|
+
// Unpack opts.
|
|
1994
|
+
const throw_errors = opts?.throw ?? true; // NEVER change this.
|
|
1995
|
+
const retry = opts?.retry;
|
|
1996
|
+
const upsert = opts?.upsert ?? true; // default true (mirrors save)
|
|
1997
|
+
const apply_ttl = this.ttl_enabled && opts?.apply_ttl !== false;
|
|
1998
|
+
// Prepare replacement for the pipeline.
|
|
1999
|
+
const base_replacement = { ...replacement };
|
|
2000
|
+
// For matched docs we always preserve existing _id via the pipeline; for upsert:false we can
|
|
2001
|
+
// proactively drop user _id to avoid accidental immutable-field issues in case of driver quirks.
|
|
2002
|
+
if (upsert === false) {
|
|
2003
|
+
delete base_replacement._id;
|
|
2004
|
+
}
|
|
2005
|
+
// Build pipeline that emulates a full replacement.
|
|
2006
|
+
const pipeline = this._build_replace_pipeline(base_replacement, upsert, apply_ttl);
|
|
2007
|
+
// Bulk path.
|
|
2008
|
+
if (opts?.bulk) {
|
|
2009
|
+
const b_op = {
|
|
2010
|
+
updateOne: {
|
|
2011
|
+
filter: query_op,
|
|
2012
|
+
update: pipeline,
|
|
2013
|
+
upsert,
|
|
2014
|
+
}
|
|
2015
|
+
};
|
|
2016
|
+
return b_op;
|
|
2017
|
+
}
|
|
2018
|
+
// Return the replaced document (post-state) via findOneAndUpdate with pipeline.
|
|
2019
|
+
if (opts?.return) {
|
|
2020
|
+
let res;
|
|
2021
|
+
try {
|
|
2022
|
+
res = await this._with_retry(() => this._col.findOneAndUpdate(query_op, pipeline, this.get_operation_options({
|
|
2023
|
+
upsert,
|
|
2024
|
+
returnDocument: mongodb.ReturnDocument.AFTER,
|
|
2025
|
+
includeResultMetadata: false,
|
|
2026
|
+
...(typeof opts?.timeout === "number" ? { maxTimeMS: opts.timeout } : {}),
|
|
2027
|
+
})), retry);
|
|
2028
|
+
}
|
|
2029
|
+
catch (e) {
|
|
2030
|
+
const err = new Collection.SaveError({
|
|
2031
|
+
message: "Replace failed due to an unexpected error.",
|
|
2032
|
+
query: query_op,
|
|
2033
|
+
reason: this._should_retry_error(e)
|
|
2034
|
+
? (Collection.Retry.get_attempts(retry) > 1 ? "retries_exhausted" : "retryable")
|
|
2035
|
+
: "unknown",
|
|
2036
|
+
cause: e,
|
|
2037
|
+
});
|
|
2038
|
+
if (throw_errors)
|
|
2039
|
+
throw err;
|
|
2040
|
+
return err;
|
|
2041
|
+
}
|
|
2042
|
+
if (!res) {
|
|
2043
|
+
const err = new Collection.SaveError({
|
|
2044
|
+
message: "Document write was not acknowledged.",
|
|
2045
|
+
query: query_op,
|
|
2046
|
+
reason: "not_acknowledged",
|
|
2047
|
+
});
|
|
2048
|
+
if (throw_errors)
|
|
2049
|
+
throw err;
|
|
2050
|
+
return err;
|
|
2051
|
+
}
|
|
2052
|
+
// Apply post-load processing.
|
|
2053
|
+
try {
|
|
2054
|
+
const processed = await this.apply_on_load(res, {
|
|
2055
|
+
projection: undefined,
|
|
2056
|
+
persist: true,
|
|
2057
|
+
await_persist: true,
|
|
2058
|
+
});
|
|
2059
|
+
return processed;
|
|
2060
|
+
}
|
|
2061
|
+
catch (e) {
|
|
2062
|
+
const err = new Collection.SaveError({
|
|
2063
|
+
message: "Replace succeeded but post-load processing failed.",
|
|
2064
|
+
query: query_op,
|
|
2065
|
+
reason: "post_process_failed",
|
|
2066
|
+
cause: e,
|
|
2067
|
+
});
|
|
2068
|
+
if (throw_errors)
|
|
2069
|
+
throw err;
|
|
2070
|
+
return err;
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
// Replace without returning the document (updateOne with pipeline).
|
|
2074
|
+
let write;
|
|
2075
|
+
try {
|
|
2076
|
+
write = await this._with_retry(() => this._col.updateOne(query_op, pipeline, this.get_operation_options({
|
|
2077
|
+
upsert,
|
|
2078
|
+
...(typeof opts?.timeout === "number" ? { maxTimeMS: opts.timeout } : {}),
|
|
2079
|
+
})), retry);
|
|
2080
|
+
}
|
|
2081
|
+
catch (e) {
|
|
2082
|
+
const err = new Collection.SaveError({
|
|
2083
|
+
message: "Replace failed due to an unexpected error.",
|
|
2084
|
+
query: query_op,
|
|
2085
|
+
reason: this._should_retry_error(e)
|
|
2086
|
+
? (Collection.Retry.get_attempts(retry) > 1 ? "retries_exhausted" : "retryable")
|
|
2087
|
+
: "unknown",
|
|
2088
|
+
cause: e,
|
|
2089
|
+
});
|
|
2090
|
+
if (throw_errors)
|
|
2091
|
+
throw err;
|
|
2092
|
+
return err;
|
|
2093
|
+
}
|
|
2094
|
+
if (!write.acknowledged || (write.matchedCount === 0 && write.upsertedCount === 0)) {
|
|
2095
|
+
const err = new Collection.SaveError({
|
|
2096
|
+
message: !write.acknowledged
|
|
2097
|
+
? "Document write was not acknowledged."
|
|
2098
|
+
: "No document matched the filter and no upsert occurred.",
|
|
2099
|
+
query: query_op,
|
|
2100
|
+
reason: !write.acknowledged ? "not_acknowledged" : "no_match",
|
|
2101
|
+
});
|
|
2102
|
+
if (throw_errors)
|
|
2103
|
+
throw err;
|
|
2104
|
+
return err;
|
|
2105
|
+
}
|
|
2106
|
+
return write;
|
|
2107
|
+
}
|
|
2108
|
+
/**
|
|
2109
|
+
* Replace multiple documents matched by `query`.
|
|
2110
|
+
* Accepts a **replacement document only** (no update operators or pipelines).
|
|
2111
|
+
*
|
|
2112
|
+
* Internally uses an aggregation pipeline to emulate a full replacement while preserving `_id`
|
|
2113
|
+
* for matched documents and applying record-version/TTL semantics consistently.
|
|
2114
|
+
*
|
|
2115
|
+
* @param query The match filter.
|
|
2116
|
+
* @param replacement The replacement document, no `$` operators.
|
|
2117
|
+
* @param opts Options, see {@link Collection.ReplaceManyOpts}.
|
|
2118
|
+
*
|
|
2119
|
+
* @note The `opts.throw` option defaults to `true`.
|
|
2120
|
+
* @note The `opts.upsert` option defaults to `false` (unlike {@link replace}, which defaults to `true`).
|
|
2121
|
+
* @note When `opts.return` is truthy, this performs a **follow-up** {@link list} with the same `query`
|
|
2122
|
+
* to return the (post-update) documents. This is **less efficient** than `replace(..., { return: true })`
|
|
2123
|
+
* because it requires an additional list query after the write.
|
|
2124
|
+
* @note TTL semantics:
|
|
2125
|
+
* - When `opts.apply_ttl === false` (or TTL is disabled), the existing TTL is preserved for matched docs.
|
|
2126
|
+
* - With sliding TTL, `__ttl_timestamp` is refreshed on every write.
|
|
2127
|
+
* - With static TTL, matched docs keep their original TTL; upserts receive a fresh timestamp.
|
|
2128
|
+
*
|
|
2129
|
+
* @warning The `_id` field is handled with special care:
|
|
2130
|
+
* - Any `_id` present in the `replacement` is **ignored/stripped** for `replace_many`.
|
|
2131
|
+
* This prevents attempts to change immutable ids across multiple documents.
|
|
2132
|
+
* - For matched documents, the existing `_id` is always preserved.
|
|
2133
|
+
* - For true upserts (`opts.upsert === true` when no match occurs), the server will
|
|
2134
|
+
* generate a new `_id`. If you need to upsert with a caller-chosen `_id`, use
|
|
2135
|
+
* {@link replace} (single-document) instead.
|
|
2136
|
+
*
|
|
2137
|
+
* @returns
|
|
2138
|
+
* - When `opts.bulk === true`: an unexecuted bulk operation (`{ updateMany: ... }`).
|
|
2139
|
+
* - When `opts.return` is falsy: {@link mongodb.UpdateResult} on success; or a
|
|
2140
|
+
* {@link Collection.SaveError} when `throw:false` and a write failure occurs.
|
|
2141
|
+
* - When `opts.return` is truthy: the matched/updated docs (via a follow-up {@link list});
|
|
2142
|
+
* or a {@link Collection.SaveError} / {@link Collection.ListError} when `throw:false`.
|
|
2143
|
+
*
|
|
2144
|
+
* @throws {Collection.SaveError} Only when `opts.throw !== false` and the write fails.
|
|
2145
|
+
* @throws {Collection.ListError} Only when `opts.throw !== false` and the follow-up list fails.
|
|
2146
|
+
* @throws {InvalidUsageError} (always) When arguments are invalid or the collection was misused.
|
|
2147
|
+
*/
|
|
2148
|
+
async replace_many(query, replacement, opts) {
|
|
2149
|
+
// Asserts / init.
|
|
2150
|
+
if (!this.initialized) {
|
|
2151
|
+
await this.init();
|
|
2152
|
+
}
|
|
2153
|
+
this.assert_init();
|
|
2154
|
+
this.assert_not_finalized();
|
|
2155
|
+
// Validate "replacement-only".
|
|
2156
|
+
if (this._is_operator_update_or_pipeline(replacement)) {
|
|
2157
|
+
throw new InvalidUsageError({
|
|
2158
|
+
message: "The 'replace_many()' method accepts a replacement document only (no update operators or pipelines).",
|
|
2159
|
+
reason: "invalid_replacement_document",
|
|
2160
|
+
field: "replacement",
|
|
2161
|
+
});
|
|
2162
|
+
}
|
|
2163
|
+
const query_op = this._init_query(query, false, "query");
|
|
2164
|
+
const throw_errors = opts?.throw ?? true; // NEVER change this.
|
|
2165
|
+
const retry = opts?.retry;
|
|
2166
|
+
const upsert = opts?.upsert ?? false; // default false (mirrors save_many)
|
|
2167
|
+
const apply_ttl = this.ttl_enabled && opts?.apply_ttl !== false;
|
|
2168
|
+
// Sanitize: never accept user-supplied _id in replace_many
|
|
2169
|
+
const base_replacement = { ...replacement };
|
|
2170
|
+
delete base_replacement._id;
|
|
2171
|
+
// Build pipeline once and reuse.
|
|
2172
|
+
const pipeline = this._build_replace_pipeline(base_replacement, upsert, apply_ttl);
|
|
2173
|
+
// Bulk path
|
|
2174
|
+
if (opts?.bulk) {
|
|
2175
|
+
const b_op = {
|
|
2176
|
+
updateMany: {
|
|
2177
|
+
filter: query_op,
|
|
2178
|
+
update: pipeline,
|
|
2179
|
+
upsert,
|
|
2180
|
+
},
|
|
2181
|
+
};
|
|
2182
|
+
return b_op;
|
|
2183
|
+
}
|
|
2184
|
+
// Perform write
|
|
2185
|
+
let write;
|
|
2186
|
+
try {
|
|
2187
|
+
write = await this._with_retry(() => this._col.updateMany(query_op, pipeline, this.get_operation_options({
|
|
2188
|
+
upsert,
|
|
2189
|
+
...(typeof opts?.timeout === "number" ? { maxTimeMS: opts.timeout } : {}),
|
|
2190
|
+
})), retry);
|
|
2191
|
+
}
|
|
2192
|
+
catch (e) {
|
|
2193
|
+
const err = new Collection.SaveError({
|
|
2194
|
+
message: "Replace-many failed due to an unexpected error.",
|
|
2195
|
+
query: query_op,
|
|
2196
|
+
reason: this._should_retry_error(e)
|
|
2197
|
+
? (Collection.Retry.get_attempts(retry) > 1 ? 'retries_exhausted' : 'retryable')
|
|
2198
|
+
: 'unknown',
|
|
2199
|
+
cause: e,
|
|
2200
|
+
});
|
|
2201
|
+
if (throw_errors)
|
|
2202
|
+
throw err;
|
|
2203
|
+
return err;
|
|
2204
|
+
}
|
|
2205
|
+
// Acknowledgement / match check
|
|
2206
|
+
if (!write.acknowledged || (write.matchedCount === 0 && write.upsertedCount === 0)) {
|
|
2207
|
+
const err = new Collection.SaveError({
|
|
2208
|
+
message: !write.acknowledged
|
|
2209
|
+
? "Document write was not acknowledged."
|
|
2210
|
+
: "No document matched the filter and no upsert occurred.",
|
|
2211
|
+
query: query_op,
|
|
2212
|
+
reason: !write.acknowledged ? "not_acknowledged" : "no_match",
|
|
2213
|
+
});
|
|
2214
|
+
if (throw_errors)
|
|
2215
|
+
throw err;
|
|
2216
|
+
return err;
|
|
2217
|
+
}
|
|
2218
|
+
// No follow-up read requested
|
|
2219
|
+
if (!opts?.return) {
|
|
2220
|
+
return write;
|
|
2221
|
+
}
|
|
2222
|
+
// Follow-up read (same as save_many)
|
|
2223
|
+
const follow = typeof opts.return === "object" ? opts.return : {};
|
|
2224
|
+
const out = await this.list(query, {
|
|
2225
|
+
...follow,
|
|
2226
|
+
throw: opts.throw,
|
|
2227
|
+
retry: opts.retry,
|
|
2228
|
+
timeout: opts.timeout,
|
|
2229
|
+
});
|
|
2230
|
+
return out;
|
|
2231
|
+
}
|
|
2232
|
+
/**
|
|
2233
|
+
* Delete a document of the collection.
|
|
2234
|
+
*
|
|
2235
|
+
* @param query The database query to the document.
|
|
2236
|
+
* @param opts Additional options, see {@link Collection.DeleteOpts}.
|
|
2237
|
+
*
|
|
2238
|
+
* @note The `opts.throw` option defaults to `true`.
|
|
2239
|
+
*
|
|
2240
|
+
* @returns
|
|
2241
|
+
* - An unexecuted bulk operation object if `bulk === true`.
|
|
2242
|
+
* - A {@link Collection.DeleteError} when occurred and `opts.throw === false`.
|
|
2243
|
+
* - A {@link mongodb.DeleteResult}.
|
|
2244
|
+
*
|
|
2245
|
+
* @throws {Collection.DeleteError} When `opts.throw !== false` and if the deletion was not acknowledged, this does not check against the deleted document count.
|
|
2246
|
+
* @throws {InvalidUsageError} (always) When the provided argument(s) are invalid or if the collection was not used properly.
|
|
2247
|
+
*/
|
|
2248
|
+
async delete(query, opts) {
|
|
2249
|
+
// Asserts.
|
|
2250
|
+
if (!this.initialized) {
|
|
2251
|
+
await this.init();
|
|
2252
|
+
}
|
|
2253
|
+
this.assert_init();
|
|
2254
|
+
this.assert_not_finalized();
|
|
2255
|
+
// Init opts.
|
|
2256
|
+
const throw_errors = opts?.throw ?? true; // NEVER change this default.
|
|
2257
|
+
// Init query.
|
|
2258
|
+
const query_op = this._init_query(query, false, "query");
|
|
2259
|
+
// Bulk operation.
|
|
2260
|
+
if (opts != null && opts.bulk) {
|
|
2261
|
+
const b_op = {
|
|
2262
|
+
deleteOne: {
|
|
2263
|
+
filter: query_op,
|
|
2264
|
+
}
|
|
2265
|
+
};
|
|
2266
|
+
return b_op;
|
|
2267
|
+
// Execute operation.
|
|
2268
|
+
}
|
|
2269
|
+
else {
|
|
2270
|
+
let res;
|
|
2271
|
+
try {
|
|
2272
|
+
res = await this._with_retry(() => this._col.deleteOne(query_op, this.get_operation_options(typeof opts?.timeout === "number" ? { maxTimeMS: opts.timeout } : {})), opts?.retry);
|
|
2273
|
+
}
|
|
2274
|
+
catch (e) {
|
|
2275
|
+
const err = new Collection.DeleteError({
|
|
2276
|
+
message: `Failed to delete document(s) in collection "${this.name}".`,
|
|
2277
|
+
query: query_op,
|
|
2278
|
+
reason: this._should_retry_error(e)
|
|
2279
|
+
? (Collection.Retry.get_attempts(opts?.retry) > 1 ? 'retries_exhausted' : 'retryable')
|
|
2280
|
+
: 'unknown',
|
|
2281
|
+
cause: e,
|
|
2282
|
+
});
|
|
2283
|
+
if (throw_errors)
|
|
2284
|
+
throw err;
|
|
2285
|
+
return err;
|
|
2286
|
+
}
|
|
2287
|
+
if (!res.acknowledged) {
|
|
2288
|
+
const err = new Collection.DeleteError({
|
|
2289
|
+
message: `Failed to delete document(s) in collection "${this.name}".`,
|
|
2290
|
+
query: query_op,
|
|
2291
|
+
reason: "not_acknowledged",
|
|
2292
|
+
});
|
|
2293
|
+
if (throw_errors)
|
|
2294
|
+
throw err;
|
|
2295
|
+
return err;
|
|
2296
|
+
}
|
|
2297
|
+
return res;
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
/**
|
|
2301
|
+
* Delete multiple documents matching the query.
|
|
2302
|
+
*
|
|
2303
|
+
* @param query The database query to the document(s).
|
|
2304
|
+
* @param opts Additional options, see {@link Collection.DeleteOpts}.
|
|
2305
|
+
* @param allow_empty_query When `true`, allows an empty query (i.e. `{}`) to be passed, which would otherwise throw an error.
|
|
2306
|
+
*
|
|
2307
|
+
* @note The `opts.throw` option defaults to `true`.
|
|
2308
|
+
*
|
|
2309
|
+
* @returns
|
|
2310
|
+
* - An unexecuted bulk operation object if `bulk === true`.
|
|
2311
|
+
* - A {@link Collection.DeleteError} when occurred and `opts.throw == false`.
|
|
2312
|
+
* - A {@link mongodb.DeleteResult}.
|
|
2313
|
+
*
|
|
2314
|
+
* @throws {Collection.DeleteError} When `opts.throw !== false` and if the deletion was not acknowledged, this does not check against the deleted document count.
|
|
2315
|
+
* @throws {InvalidUsageError} (always) When the provided argument(s) are invalid or if the collection was not used properly.
|
|
2316
|
+
*/
|
|
2317
|
+
async delete_many(query, opts, allow_empty_query = false) {
|
|
2318
|
+
// Asserts.
|
|
2319
|
+
if (!this.initialized) {
|
|
2320
|
+
await this.init();
|
|
2321
|
+
}
|
|
2322
|
+
this.assert_init();
|
|
2323
|
+
this.assert_not_finalized();
|
|
2324
|
+
// Init opts.
|
|
2325
|
+
const throw_errors = opts?.throw ?? true; // NEVER change this default.
|
|
2326
|
+
// Init query.
|
|
2327
|
+
const query_op = this._init_query(query, allow_empty_query, "query");
|
|
2328
|
+
// Bulk operation.
|
|
2329
|
+
if (opts != null && opts.bulk) {
|
|
2330
|
+
const b_op = {
|
|
2331
|
+
deleteMany: {
|
|
2332
|
+
filter: query_op,
|
|
2333
|
+
}
|
|
2334
|
+
};
|
|
2335
|
+
return b_op;
|
|
2336
|
+
// Execute operation.
|
|
2337
|
+
}
|
|
2338
|
+
else {
|
|
2339
|
+
let res;
|
|
2340
|
+
try {
|
|
2341
|
+
res = await this._with_retry(() => this._col.deleteMany(query_op, this.get_operation_options(typeof opts?.timeout === "number" ? { maxTimeMS: opts.timeout } : {})), opts?.retry);
|
|
2342
|
+
}
|
|
2343
|
+
catch (e) {
|
|
2344
|
+
const err = new Collection.DeleteError({
|
|
2345
|
+
message: `Failed to delete document(s) in collection "${this.name}".`,
|
|
2346
|
+
query: query_op,
|
|
2347
|
+
reason: this._should_retry_error(e)
|
|
2348
|
+
? (Collection.Retry.get_attempts(opts?.retry) > 1 ? 'retries_exhausted' : 'retryable')
|
|
2349
|
+
: 'unknown',
|
|
2350
|
+
cause: e,
|
|
2351
|
+
});
|
|
2352
|
+
if (throw_errors)
|
|
2353
|
+
throw err;
|
|
2354
|
+
return err;
|
|
2355
|
+
}
|
|
2356
|
+
if (!res.acknowledged) {
|
|
2357
|
+
const err = new Collection.DeleteError({
|
|
2358
|
+
message: `Failed to delete document(s) in collection "${this.name}".`,
|
|
2359
|
+
query: query_op,
|
|
2360
|
+
reason: "not_acknowledged",
|
|
2361
|
+
});
|
|
2362
|
+
if (throw_errors)
|
|
2363
|
+
throw err;
|
|
2364
|
+
return err;
|
|
2365
|
+
}
|
|
2366
|
+
return res;
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
/**
|
|
2370
|
+
* Delete all documents in the collection.
|
|
2371
|
+
*
|
|
2372
|
+
* @param opts Additional options, see {@link Collection.DeleteOpts}.
|
|
2373
|
+
*
|
|
2374
|
+
* @note The `opts.throw` option defaults to `true`.
|
|
2375
|
+
*
|
|
2376
|
+
* @returns
|
|
2377
|
+
* - An unexecuted bulk operation object if `bulk === true`.
|
|
2378
|
+
* - A {@link Collection.DeleteError} when occurred and `opts.throw == false`.
|
|
2379
|
+
* - A {@link mongodb.DeleteResult}.
|
|
2380
|
+
*
|
|
2381
|
+
* @throws {Collection.DeleteError} When `opts.throw !== false` and if the deletion was not acknowledged, this does not check against the deleted document count.
|
|
2382
|
+
* @throws {InvalidUsageError} (always) When the provided argument(s) are invalid or if the collection was not used properly.
|
|
2383
|
+
*/
|
|
2384
|
+
async delete_all(opts) {
|
|
2385
|
+
return this.delete_many({}, opts, true);
|
|
2386
|
+
}
|
|
2387
|
+
/**
|
|
2388
|
+
* Delete all documents from the collection and drop the collection.
|
|
2389
|
+
*
|
|
2390
|
+
* @note This function is not supported for transaction based collections.
|
|
2391
|
+
*
|
|
2392
|
+
* @param opts Additional options, see {@link Collection.DeleteOpts}.
|
|
2393
|
+
*
|
|
2394
|
+
* @note The `opts.throw` option defaults to `true`.
|
|
2395
|
+
*
|
|
2396
|
+
* @returns
|
|
2397
|
+
* - A {@link Collection.DeleteError} when occurred and `opts.throw === false`.
|
|
2398
|
+
* - Undefined upon success.
|
|
2399
|
+
*
|
|
2400
|
+
* @throws {Collection.DeleteError} When `opts.throw !== false` and if the deletion was not acknowledged, this does not check against the deleted document count.
|
|
2401
|
+
* @throws {InvalidUsageError} (always) When the provided argument(s) are invalid or if the collection was not used properly.
|
|
2402
|
+
*/
|
|
2403
|
+
async delete_collection(opts) {
|
|
2404
|
+
// Asserts.
|
|
2405
|
+
if (!this.initialized) {
|
|
2406
|
+
await this.init();
|
|
2407
|
+
}
|
|
2408
|
+
this.assert_init();
|
|
2409
|
+
this.assert_not_finalized();
|
|
2410
|
+
this.assert_not_transaction_based();
|
|
2411
|
+
// Init opts.
|
|
2412
|
+
const throw_errors = opts?.throw ?? true; // NEVER change this default.
|
|
2413
|
+
// Drop collection.
|
|
2414
|
+
let res;
|
|
2415
|
+
try {
|
|
2416
|
+
res = await this._with_retry(() => this._col.drop(this.get_operation_options(typeof opts?.timeout === "number" ? { maxTimeMS: opts.timeout } : {})), opts?.retry);
|
|
2417
|
+
}
|
|
2418
|
+
catch (e) {
|
|
2419
|
+
// Make it idempotent: "namespace not found" means already dropped.
|
|
2420
|
+
if (e && typeof e === "object" && (e?.code === 26 || e?.codeName === "NamespaceNotFound")) {
|
|
2421
|
+
return undefined;
|
|
2422
|
+
}
|
|
2423
|
+
const err = new Collection.DeleteError({
|
|
2424
|
+
message: `Failed to drop collection "${this.name}".`,
|
|
2425
|
+
query: {},
|
|
2426
|
+
reason: this._should_retry_error(e)
|
|
2427
|
+
? (Collection.Retry.get_attempts(opts?.retry) > 1 ? 'retries_exhausted' : 'retryable')
|
|
2428
|
+
: 'unknown',
|
|
2429
|
+
cause: e,
|
|
2430
|
+
});
|
|
2431
|
+
if (throw_errors)
|
|
2432
|
+
throw err;
|
|
2433
|
+
return err;
|
|
2434
|
+
}
|
|
2435
|
+
// Handle response.
|
|
2436
|
+
if (!res) {
|
|
2437
|
+
const err = new Collection.DeleteError({
|
|
2438
|
+
message: `Failed to drop collection "${this.name}", detected by a falsy return.`,
|
|
2439
|
+
query: {},
|
|
2440
|
+
reason: "not_acknowledged",
|
|
2441
|
+
});
|
|
2442
|
+
if (throw_errors)
|
|
2443
|
+
throw err;
|
|
2444
|
+
return err;
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
// /**
|
|
2448
|
+
// * @todo implement
|
|
2449
|
+
// * Enhanced bulk operations with retry logic for failed operations
|
|
2450
|
+
// * @param operations - Array of bulk write operations
|
|
2451
|
+
// * @param retries - Number of retry attempts for failed operations. Set to -1 to disable retries. Default is 3.
|
|
2452
|
+
// * @returns Simplified BulkWriteResult with aggregated counts from all attempts
|
|
2453
|
+
// */
|
|
2454
|
+
// async bulk_operations(
|
|
2455
|
+
// operations: any[] = [],
|
|
2456
|
+
// retries: number = 3
|
|
2457
|
+
// ): Promise<{
|
|
2458
|
+
// ok: boolean;
|
|
2459
|
+
// inserted_count: number;
|
|
2460
|
+
// matched_count: number;
|
|
2461
|
+
// modified_count: number;
|
|
2462
|
+
// deleted_count: number;
|
|
2463
|
+
// upserted_count: number;
|
|
2464
|
+
// upserted_ids: { [key: number]: any };
|
|
2465
|
+
// inserted_ids: { [key: number]: any };
|
|
2466
|
+
// failed_operations: number[];
|
|
2467
|
+
// errors?: any[];
|
|
2468
|
+
// }> {
|
|
2469
|
+
// if (!this.initialized) { await this.init(); }
|
|
2470
|
+
// this.assert_init();
|
|
2471
|
+
// // Validate operations
|
|
2472
|
+
// if (!Array.isArray(operations)) {
|
|
2473
|
+
// throw new TypeError('Operations must be an array');
|
|
2474
|
+
// }
|
|
2475
|
+
// // Return early for empty operations
|
|
2476
|
+
// if (operations.length === 0) {
|
|
2477
|
+
// return {
|
|
2478
|
+
// ok: true,
|
|
2479
|
+
// inserted_count: 0,
|
|
2480
|
+
// matched_count: 0,
|
|
2481
|
+
// modified_count: 0,
|
|
2482
|
+
// deleted_count: 0,
|
|
2483
|
+
// upserted_count: 0,
|
|
2484
|
+
// upserted_ids: {},
|
|
2485
|
+
// inserted_ids: {},
|
|
2486
|
+
// failed_operations: []
|
|
2487
|
+
// };
|
|
2488
|
+
// }
|
|
2489
|
+
// // MongoDB bulk write limit
|
|
2490
|
+
// const MAX_BATCH_SIZE = 100000;
|
|
2491
|
+
// if (operations.length > MAX_BATCH_SIZE) {
|
|
2492
|
+
// throw new Error(`Bulk operations exceed MongoDB limit of ${MAX_BATCH_SIZE}. Please batch your operations.`);
|
|
2493
|
+
// }
|
|
2494
|
+
// // Initialize aggregated results
|
|
2495
|
+
// const aggregated_result = {
|
|
2496
|
+
// ok: true,
|
|
2497
|
+
// inserted_count: 0,
|
|
2498
|
+
// matched_count: 0,
|
|
2499
|
+
// modified_count: 0,
|
|
2500
|
+
// deleted_count: 0,
|
|
2501
|
+
// upserted_count: 0,
|
|
2502
|
+
// upserted_ids: {} as { [key: number]: any },
|
|
2503
|
+
// inserted_ids: {} as { [key: number]: any },
|
|
2504
|
+
// failed_operations: [] as number[],
|
|
2505
|
+
// errors: [] as any[]
|
|
2506
|
+
// };
|
|
2507
|
+
// // Track operation status (true = succeeded, false = failed/pending)
|
|
2508
|
+
// const operation_status: Map<number, boolean> = new Map();
|
|
2509
|
+
// operations.forEach((_, index) => operation_status.set(index, false));
|
|
2510
|
+
// // Track latest errors for each operation (will be cleared if operation succeeds)
|
|
2511
|
+
// const latest_errors: Map<number, any> = new Map();
|
|
2512
|
+
// // Track operations that need to be executed
|
|
2513
|
+
// let pending_operations = operations.map((op, index) => ({ op, original_index: index }));
|
|
2514
|
+
// let attempt_count = 0;
|
|
2515
|
+
// const max_attempts = retries < 0 ? 1 : retries + 1;
|
|
2516
|
+
// while (pending_operations.length > 0 && attempt_count < max_attempts) {
|
|
2517
|
+
// attempt_count++;
|
|
2518
|
+
// try {
|
|
2519
|
+
// // Execute bulk operations
|
|
2520
|
+
// const result = await this._col.bulkWrite(
|
|
2521
|
+
// pending_operations.map(item => item.op),
|
|
2522
|
+
// { ordered: false } // Use unordered for better error handling
|
|
2523
|
+
// );
|
|
2524
|
+
// // Track which operations succeeded in this attempt
|
|
2525
|
+
// const succeeded_in_this_attempt = new Set<number>();
|
|
2526
|
+
// // Aggregate successful results
|
|
2527
|
+
// aggregated_result.inserted_count += result.insertedCount;
|
|
2528
|
+
// aggregated_result.matched_count += result.matchedCount;
|
|
2529
|
+
// aggregated_result.modified_count += result.modifiedCount;
|
|
2530
|
+
// aggregated_result.deleted_count += result.deletedCount;
|
|
2531
|
+
// aggregated_result.upserted_count += result.upsertedCount;
|
|
2532
|
+
// // Map inserted/upserted IDs back to original indices
|
|
2533
|
+
// if (result.insertedIds && typeof result.insertedIds === 'object') {
|
|
2534
|
+
// for (const [key, value] of Object.entries(result.insertedIds)) {
|
|
2535
|
+
// const idx = parseInt(key);
|
|
2536
|
+
// if (!isNaN(idx) && pending_operations[idx]) {
|
|
2537
|
+
// const original_index = pending_operations[idx].original_index;
|
|
2538
|
+
// aggregated_result.inserted_ids[original_index] = value;
|
|
2539
|
+
// succeeded_in_this_attempt.add(original_index);
|
|
2540
|
+
// }
|
|
2541
|
+
// }
|
|
2542
|
+
// }
|
|
2543
|
+
// if (result.upsertedIds && typeof result.upsertedIds === 'object') {
|
|
2544
|
+
// for (const [key, value] of Object.entries(result.upsertedIds)) {
|
|
2545
|
+
// const idx = parseInt(key);
|
|
2546
|
+
// if (!isNaN(idx) && pending_operations[idx]) {
|
|
2547
|
+
// const original_index = pending_operations[idx].original_index;
|
|
2548
|
+
// aggregated_result.upserted_ids[original_index] = value;
|
|
2549
|
+
// succeeded_in_this_attempt.add(original_index);
|
|
2550
|
+
// }
|
|
2551
|
+
// }
|
|
2552
|
+
// }
|
|
2553
|
+
// // Check for write errors
|
|
2554
|
+
// const write_errors = result.hasWriteErrors?.() ? result.getWriteErrors() : [];
|
|
2555
|
+
// if (write_errors.length > 0) {
|
|
2556
|
+
// aggregated_result.ok = false;
|
|
2557
|
+
// // Track failed operations by their indices in current batch
|
|
2558
|
+
// const failed_indices_in_batch = new Set(write_errors.map(err => err.index));
|
|
2559
|
+
// // Update errors for failed operations
|
|
2560
|
+
// for (const error of write_errors) {
|
|
2561
|
+
// if (error.index < pending_operations.length) {
|
|
2562
|
+
// const original_index = pending_operations[error.index].original_index;
|
|
2563
|
+
// latest_errors.set(original_index, {
|
|
2564
|
+
// ...error,
|
|
2565
|
+
// index: original_index,
|
|
2566
|
+
// attempt: attempt_count,
|
|
2567
|
+
// timestamp: new Date().toISOString()
|
|
2568
|
+
// });
|
|
2569
|
+
// }
|
|
2570
|
+
// }
|
|
2571
|
+
// // Mark operations as succeeded if they weren't in the error list
|
|
2572
|
+
// pending_operations.forEach((item, batch_index) => {
|
|
2573
|
+
// if (!failed_indices_in_batch.has(batch_index)) {
|
|
2574
|
+
// const original_index = item.original_index;
|
|
2575
|
+
// operation_status.set(original_index, true);
|
|
2576
|
+
// succeeded_in_this_attempt.add(original_index);
|
|
2577
|
+
// // Clear any previous errors for this operation
|
|
2578
|
+
// latest_errors.delete(original_index);
|
|
2579
|
+
// }
|
|
2580
|
+
// });
|
|
2581
|
+
// // Filter pending operations to only include failed ones
|
|
2582
|
+
// if (retries >= 0 && attempt_count < max_attempts) {
|
|
2583
|
+
// pending_operations = pending_operations.filter((_, index) => failed_indices_in_batch.has(index));
|
|
2584
|
+
// // Add exponential backoff for retries
|
|
2585
|
+
// if (pending_operations.length > 0) {
|
|
2586
|
+
// const delay = Math.min(1000 * Math.pow(2, attempt_count - 1), 5000);
|
|
2587
|
+
// await new Promise(resolve => setTimeout(resolve, delay));
|
|
2588
|
+
// }
|
|
2589
|
+
// } else {
|
|
2590
|
+
// // No more retries, exit
|
|
2591
|
+
// break;
|
|
2592
|
+
// }
|
|
2593
|
+
// } else {
|
|
2594
|
+
// // All operations in this batch succeeded
|
|
2595
|
+
// pending_operations.forEach(item => {
|
|
2596
|
+
// operation_status.set(item.original_index, true);
|
|
2597
|
+
// succeeded_in_this_attempt.add(item.original_index);
|
|
2598
|
+
// // Clear any previous errors for these operations
|
|
2599
|
+
// latest_errors.delete(item.original_index);
|
|
2600
|
+
// });
|
|
2601
|
+
// pending_operations = [];
|
|
2602
|
+
// }
|
|
2603
|
+
// // Log successful recoveries for monitoring
|
|
2604
|
+
// if (attempt_count > 1 && succeeded_in_this_attempt.size > 0) {
|
|
2605
|
+
// console.log(`[BulkOps] Recovered ${succeeded_in_this_attempt.size} operations on attempt ${attempt_count}`);
|
|
2606
|
+
// }
|
|
2607
|
+
// } catch (error: any) {
|
|
2608
|
+
// aggregated_result.ok = false;
|
|
2609
|
+
// // Track error for all pending operations
|
|
2610
|
+
// const affected_indices = pending_operations.map(item => item.original_index);
|
|
2611
|
+
// for (const original_index of affected_indices) {
|
|
2612
|
+
// latest_errors.set(original_index, {
|
|
2613
|
+
// message: error.message || 'Unknown error',
|
|
2614
|
+
// code: error.code,
|
|
2615
|
+
// attempt: attempt_count,
|
|
2616
|
+
// index: original_index,
|
|
2617
|
+
// timestamp: new Date().toISOString(),
|
|
2618
|
+
// type: 'batch_error'
|
|
2619
|
+
// });
|
|
2620
|
+
// }
|
|
2621
|
+
// // If retries are disabled or we've exhausted retries, throw
|
|
2622
|
+
// if (retries < 0 || attempt_count >= max_attempts) {
|
|
2623
|
+
// break;
|
|
2624
|
+
// }
|
|
2625
|
+
// // Add exponential backoff before retry
|
|
2626
|
+
// const delay = Math.min(1000 * Math.pow(2, attempt_count - 1), 5000);
|
|
2627
|
+
// await new Promise(resolve => setTimeout(resolve, delay));
|
|
2628
|
+
// }
|
|
2629
|
+
// }
|
|
2630
|
+
// // Final reconciliation: determine which operations ultimately failed
|
|
2631
|
+
// aggregated_result.failed_operations = [];
|
|
2632
|
+
// aggregated_result.errors = [];
|
|
2633
|
+
// for (const [index, succeeded] of operation_status.entries()) {
|
|
2634
|
+
// if (!succeeded) {
|
|
2635
|
+
// aggregated_result.failed_operations.push(index);
|
|
2636
|
+
// const error = latest_errors.get(index);
|
|
2637
|
+
// if (error) {
|
|
2638
|
+
// aggregated_result.errors.push(error);
|
|
2639
|
+
// }
|
|
2640
|
+
// }
|
|
2641
|
+
// }
|
|
2642
|
+
// // Sort failed operations for consistency
|
|
2643
|
+
// aggregated_result.failed_operations.sort((a, b) => a - b);
|
|
2644
|
+
// // Clean up errors array if empty
|
|
2645
|
+
// if (aggregated_result.errors.length === 0) {
|
|
2646
|
+
// delete (aggregated_result as any).errors;
|
|
2647
|
+
// }
|
|
2648
|
+
// // If we still have failed operations after all retries, include detailed error
|
|
2649
|
+
// if (aggregated_result.failed_operations.length > 0) {
|
|
2650
|
+
// const error = new Error(
|
|
2651
|
+
// `Bulk operations partially failed: ${aggregated_result.failed_operations.length} of ${operations.length} operations could not be completed after ${attempt_count} attempts. ` +
|
|
2652
|
+
// `Successfully processed: ${operations.length - aggregated_result.failed_operations.length} operations.`
|
|
2653
|
+
// );
|
|
2654
|
+
// (error as any).aggregated_result = aggregated_result;
|
|
2655
|
+
// (error as any).retry_attempts = attempt_count;
|
|
2656
|
+
// (error as any).success_rate = ((operations.length - aggregated_result.failed_operations.length) / operations.length * 100).toFixed(2) + '%';
|
|
2657
|
+
// // Only throw if all operations failed
|
|
2658
|
+
// if (aggregated_result.failed_operations.length === operations.length) {
|
|
2659
|
+
// throw error;
|
|
2660
|
+
// }
|
|
2661
|
+
// // Log partial failure for monitoring
|
|
2662
|
+
// console.warn(`[BulkOps] Partial failure:`, (error as any).success_rate, 'success rate');
|
|
2663
|
+
// } else {
|
|
2664
|
+
// // Clean up failed operations array.
|
|
2665
|
+
// delete (aggregated_result as any).failed_operations;
|
|
2666
|
+
// }
|
|
2667
|
+
// return aggregated_result;
|
|
2668
|
+
// }
|
|
2669
|
+
/**
|
|
2670
|
+
* Execute bulk write operations.
|
|
2671
|
+
*
|
|
2672
|
+
* @param operations Array of bulk write operations.
|
|
2673
|
+
* @param opts Additional options, see {@link Collection.BulkOpts}
|
|
2674
|
+
*
|
|
2675
|
+
* @note The `opts.throw` option defaults to `true`.
|
|
2676
|
+
*
|
|
2677
|
+
* @returns
|
|
2678
|
+
* - A {@link Collection.BulkError} if occurred and `opts.throw === false`.
|
|
2679
|
+
* - A {@link mongodb.BulkWriteResult}.
|
|
2680
|
+
*
|
|
2681
|
+
* @throws {Collection.BulkError} When `opts.throw !== false` and if the bulk operation failed, this does not check against the bulk write result (this may change in the future).
|
|
2682
|
+
* @throws {InvalidUsageError} (always) When the provided argument(s) are invalid or if the collection was not used properly.
|
|
2683
|
+
*/
|
|
2684
|
+
async bulk_operations(operations, opts) {
|
|
2685
|
+
// Assert.
|
|
2686
|
+
if (!this.initialized) {
|
|
2687
|
+
await this.init();
|
|
2688
|
+
}
|
|
2689
|
+
this.assert_init();
|
|
2690
|
+
this.assert_not_finalized();
|
|
2691
|
+
if (!Array.isArray(operations)) {
|
|
2692
|
+
throw new TypeError('Operations must be an array');
|
|
2693
|
+
}
|
|
2694
|
+
if (operations.length > 100000) {
|
|
2695
|
+
throw new InvalidUsageError({
|
|
2696
|
+
message: 'Bulk operations exceed MongoDB limit of 100000',
|
|
2697
|
+
reason: "invalid_operations_length",
|
|
2698
|
+
});
|
|
2699
|
+
}
|
|
2700
|
+
// Unpack opts.
|
|
2701
|
+
const throw_errors = opts?.throw ?? true; // NEVER change this default.
|
|
2702
|
+
// Apply record version + TTL per operation.
|
|
2703
|
+
if (this.ttl_enabled || this.record_version != null) {
|
|
2704
|
+
const now = new Date();
|
|
2705
|
+
for (const op of operations) {
|
|
2706
|
+
// --- Record version injection (when applicable) ---
|
|
2707
|
+
if (this.record_version != null) {
|
|
2708
|
+
const rv = this.record_version;
|
|
2709
|
+
// insertOne → always an insert; stamp unless user set a different value
|
|
2710
|
+
if (op.insertOne?.document && typeof op.insertOne.document === "object") {
|
|
2711
|
+
const d = op.insertOne.document;
|
|
2712
|
+
if (d.__record_version == null) {
|
|
2713
|
+
d.__record_version = rv;
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
// replaceOne → only stamp when upsert === true (insert path)
|
|
2717
|
+
else if (op.replaceOne?.replacement && typeof op.replaceOne.replacement === "object") {
|
|
2718
|
+
if (op.replaceOne.upsert) {
|
|
2719
|
+
const d = op.replaceOne.replacement;
|
|
2720
|
+
if (d.__record_version == null) {
|
|
2721
|
+
d.__record_version = rv;
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
// updateOne/Many → only set $setOnInsert when upsert true and user did not set any value
|
|
2726
|
+
else if (op.updateOne?.update) {
|
|
2727
|
+
if (op.updateOne.upsert)
|
|
2728
|
+
this._apply_record_version_to_operation(op.updateOne.update, true);
|
|
2729
|
+
}
|
|
2730
|
+
else if (op.updateMany?.update) {
|
|
2731
|
+
if (op.updateMany.upsert)
|
|
2732
|
+
this._apply_record_version_to_operation(op.updateMany.update, true);
|
|
2733
|
+
}
|
|
2734
|
+
// deleteOne/deleteMany → no-op
|
|
2735
|
+
}
|
|
2736
|
+
// --- TTL injection (when enabled) ---
|
|
2737
|
+
if (!this.ttl_enabled)
|
|
2738
|
+
continue;
|
|
2739
|
+
// insertOne
|
|
2740
|
+
if (op.insertOne?.document && typeof op.insertOne.document === "object") {
|
|
2741
|
+
if (this.sliding_ttl || op.insertOne.document.__ttl_timestamp == null) {
|
|
2742
|
+
op.insertOne.document.__ttl_timestamp = now;
|
|
2743
|
+
}
|
|
2744
|
+
continue;
|
|
2745
|
+
}
|
|
2746
|
+
// replaceOne
|
|
2747
|
+
if (op.replaceOne?.replacement && typeof op.replaceOne.replacement === "object") {
|
|
2748
|
+
if (this.sliding_ttl) {
|
|
2749
|
+
op.replaceOne.replacement.__ttl_timestamp = now;
|
|
2750
|
+
}
|
|
2751
|
+
else if (op.replaceOne.upsert && op.replaceOne.replacement.__ttl_timestamp == null) {
|
|
2752
|
+
op.replaceOne.replacement.__ttl_timestamp = now;
|
|
2753
|
+
}
|
|
2754
|
+
continue;
|
|
2755
|
+
}
|
|
2756
|
+
// updateOne
|
|
2757
|
+
if (op.updateOne?.update) {
|
|
2758
|
+
this._apply_ttl_to_operation(op.updateOne.update, op.updateOne.upsert);
|
|
2759
|
+
continue;
|
|
2760
|
+
}
|
|
2761
|
+
// updateMany
|
|
2762
|
+
if (op.updateMany?.update) {
|
|
2763
|
+
this._apply_ttl_to_operation(op.updateMany.update, op.updateMany.upsert);
|
|
2764
|
+
continue;
|
|
2765
|
+
}
|
|
2766
|
+
// deleteOne / deleteMany: no TTL changes
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
// Perform.
|
|
2770
|
+
try {
|
|
2771
|
+
return await this._with_retry(() => this._col.bulkWrite(operations, this.get_operation_options({
|
|
2772
|
+
ordered: true,
|
|
2773
|
+
...(typeof opts?.timeout === "number" ? { maxTimeMS: opts.timeout } : {}),
|
|
2774
|
+
})), opts?.retry);
|
|
2775
|
+
}
|
|
2776
|
+
catch (e) {
|
|
2777
|
+
// Encountered a non retryable error or no retries (left).
|
|
2778
|
+
const err = new Collection.BulkError({
|
|
2779
|
+
message: 'Bulk operations failed due to an unexpected error.',
|
|
2780
|
+
query: {},
|
|
2781
|
+
reason: this._should_retry_error(e)
|
|
2782
|
+
? (Collection.Retry.get_attempts(opts?.retry) > 1 ? 'retries_exhausted' : 'retryable')
|
|
2783
|
+
: 'unknown',
|
|
2784
|
+
cause: e,
|
|
2785
|
+
});
|
|
2786
|
+
if (throw_errors)
|
|
2787
|
+
throw err;
|
|
2788
|
+
return err;
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
/**
|
|
2792
|
+
* Execute an aggregation pipeline.
|
|
2793
|
+
*
|
|
2794
|
+
* @param pipeline MongoDB aggregation pipeline stages.
|
|
2795
|
+
* @param opts Aggregation options, see {@link Collection.AggregateOpts}
|
|
2796
|
+
*
|
|
2797
|
+
* @note The `opts.throw` option defaults to `true`.
|
|
2798
|
+
* @note This method does not execute the {@link Collection.Opts.on_load}
|
|
2799
|
+
* and {@link Collection.Opts.on_transform_version} callbacks.
|
|
2800
|
+
*
|
|
2801
|
+
* @returns
|
|
2802
|
+
* - A {@link Collection.AggregateError} if occurred and `opts.throw === false`.
|
|
2803
|
+
* - An {@link mongodb.AggregationCursor} if `opts.cursor === true`.
|
|
2804
|
+
* - An array of document results.
|
|
2805
|
+
*
|
|
2806
|
+
* @throws {Collection.AggregateError} When `opts.throw !== false` and if the aggregate operation failed, this does not check against the aggregate result (this may change in the future).
|
|
2807
|
+
* @throws {InvalidUsageError} (always) When the provided argument(s) are invalid or if the collection was not used properly.
|
|
2808
|
+
*/
|
|
2809
|
+
async aggregate(pipeline, // @todo add strict pipeline type.
|
|
2810
|
+
opts) {
|
|
2811
|
+
// Asserts.
|
|
2812
|
+
if (!this.initialized) {
|
|
2813
|
+
await this.init();
|
|
2814
|
+
}
|
|
2815
|
+
this.assert_init();
|
|
2816
|
+
this.assert_not_finalized();
|
|
2817
|
+
// Unpack opts.
|
|
2818
|
+
const throw_errors = opts?.throw ?? true; // NEVER change this default.
|
|
2819
|
+
// Aggregate.
|
|
2820
|
+
try {
|
|
2821
|
+
const cursor = await this._with_retry(() => this._col.aggregate(pipeline, this.get_operation_options(typeof opts?.timeout === "number" ? { maxTimeMS: opts.timeout } : {})), opts?.retry);
|
|
2822
|
+
if (typeof opts?.timeout === "number" && typeof cursor.maxTimeMS === "function") {
|
|
2823
|
+
cursor.maxTimeMS(opts.timeout);
|
|
2824
|
+
}
|
|
2825
|
+
if (opts?.cursor)
|
|
2826
|
+
return cursor;
|
|
2827
|
+
const arr = await this._with_retry(() => cursor.toArray(), opts?.retry);
|
|
2828
|
+
return arr;
|
|
2829
|
+
// We do not apply the on-load callback here.
|
|
2830
|
+
// Since the aggregation pipeline might have projected
|
|
2831
|
+
// the document and we can not guarantee its shape,
|
|
2832
|
+
// we avoid applying the on-load callback.
|
|
2833
|
+
// Post-process loaded docs when possible.
|
|
2834
|
+
// const processed = Array.isArray(arr)
|
|
2835
|
+
// ? arr.map(d => (d && typeof d === "object")
|
|
2836
|
+
// ? this._apply_on_load<undefined>(d, true) FIX // not sure if we should apply on load here.
|
|
2837
|
+
// : d
|
|
2838
|
+
// )
|
|
2839
|
+
// : arr;
|
|
2840
|
+
// return processed as Res;
|
|
2841
|
+
}
|
|
2842
|
+
catch (e) {
|
|
2843
|
+
// Encountered a non retryable error or no retries (left).
|
|
2844
|
+
const err = new Collection.AggregateError({
|
|
2845
|
+
message: 'Aggregate operation failed due to an unexpected error.',
|
|
2846
|
+
query: {},
|
|
2847
|
+
reason: this._should_retry_error(e)
|
|
2848
|
+
? (Collection.Retry.get_attempts(opts?.retry) > 1 ? 'retries_exhausted' : 'retryable')
|
|
2849
|
+
: 'unknown',
|
|
2850
|
+
cause: e,
|
|
2851
|
+
});
|
|
2852
|
+
if (throw_errors)
|
|
2853
|
+
throw err;
|
|
2854
|
+
return err;
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
/**
|
|
2858
|
+
* Clean a document from all default system attributes.
|
|
2859
|
+
* @param doc The document to clean.
|
|
2860
|
+
* @returns The cleaned document without system attributes.
|
|
2861
|
+
*/
|
|
2862
|
+
clean(doc) {
|
|
2863
|
+
if (doc == null) {
|
|
2864
|
+
return doc;
|
|
2865
|
+
}
|
|
2866
|
+
if (typeof doc === "object") {
|
|
2867
|
+
const out = { ...doc };
|
|
2868
|
+
delete out._id;
|
|
2869
|
+
delete out._path;
|
|
2870
|
+
if (out.__ttl_timestamp != null) {
|
|
2871
|
+
delete out.__ttl_timestamp;
|
|
2872
|
+
}
|
|
2873
|
+
if (out.__record_version != null) {
|
|
2874
|
+
delete out.__record_version;
|
|
2875
|
+
}
|
|
2876
|
+
return out;
|
|
2877
|
+
}
|
|
2878
|
+
return doc;
|
|
2879
|
+
}
|
|
2880
|
+
// ---------------------------------------------------------
|
|
2881
|
+
// Sessions & transactions.
|
|
2882
|
+
// ---------------------------------------------------------
|
|
2883
|
+
/**
|
|
2884
|
+
* Start a new transaction by creating a TransactionCollection instance.
|
|
2885
|
+
* @returns A new TransactionCollection instance with transaction capabilities.
|
|
2886
|
+
*/
|
|
2887
|
+
async start_transaction() {
|
|
2888
|
+
if (!this.db.client) {
|
|
2889
|
+
throw new InvalidUsageError({
|
|
2890
|
+
message: "Database client is not initialized, ensure the parent 'volt.Server' is initialized.",
|
|
2891
|
+
reason: "client_not_connected",
|
|
2892
|
+
});
|
|
2893
|
+
}
|
|
2894
|
+
if (!this.initialized) {
|
|
2895
|
+
await this.init();
|
|
2896
|
+
}
|
|
2897
|
+
this.assert_init();
|
|
2898
|
+
return new TransactionCollection({
|
|
2899
|
+
derived_collection: this,
|
|
2900
|
+
transaction_based: true,
|
|
2901
|
+
});
|
|
2902
|
+
}
|
|
2903
|
+
// ------------------- DEPRECATED -------------------------
|
|
2904
|
+
/** Prepare a _path based regex operation. @deprecated */
|
|
2905
|
+
prepare_path_regex_filter(path) {
|
|
2906
|
+
// Validate path to prevent ReDoS
|
|
2907
|
+
while (path.length > 0 && path.charAt(path.length - 1) === "/") {
|
|
2908
|
+
path = path.substring(0, path.length - 1);
|
|
2909
|
+
}
|
|
2910
|
+
if (path.length == 0) {
|
|
2911
|
+
throw new InvalidUsageError({
|
|
2912
|
+
message: `Invalid path '${path}'`,
|
|
2913
|
+
reason: "invalid_path",
|
|
2914
|
+
});
|
|
2915
|
+
}
|
|
2916
|
+
if (path.length > 1000) {
|
|
2917
|
+
throw new InvalidUsageError({
|
|
2918
|
+
message: `Path too long (${path.length})`,
|
|
2919
|
+
reason: "invalid_path",
|
|
2920
|
+
});
|
|
2921
|
+
}
|
|
2922
|
+
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2923
|
+
const filter = {
|
|
2924
|
+
_path: {
|
|
2925
|
+
$regex: `^${escapeRegExp(path)}/`,
|
|
2926
|
+
// $options: 'i' // Case insensitive for consistency
|
|
2927
|
+
}
|
|
2928
|
+
};
|
|
2929
|
+
return filter;
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
/** Nested types for the {@link Collection} class. */
|
|
2933
|
+
(function (Collection) {
|
|
2934
|
+
// -------------------------------------------------------------------
|
|
2935
|
+
// Retry options.
|
|
2936
|
+
// -------------------------------------------------------------------
|
|
2937
|
+
/** Mini module for managing retry attempts. */
|
|
2938
|
+
let Retry;
|
|
2939
|
+
(function (Retry) {
|
|
2940
|
+
/**
|
|
2941
|
+
* Get the number of attempts from a a retry type
|
|
2942
|
+
* @returns 1 when undefined, or the specified number of attempts,
|
|
2943
|
+
* with a minimum of 1 and maximum of 100.
|
|
2944
|
+
*/
|
|
2945
|
+
function get_attempts(retry) {
|
|
2946
|
+
return Math.max(1, Math.min(100, typeof retry === "number" ? retry : !retry ? 1 : retry.attempts));
|
|
2947
|
+
}
|
|
2948
|
+
Retry.get_attempts = get_attempts;
|
|
2949
|
+
/**
|
|
2950
|
+
* Normalize retry options into a bounded, concrete shape.
|
|
2951
|
+
*
|
|
2952
|
+
* @param retry A retry attempts number or {@link Collection.Retry.Opts}.
|
|
2953
|
+
* @returns A normalized retry configuration.
|
|
2954
|
+
*/
|
|
2955
|
+
function normalize(retry) {
|
|
2956
|
+
const base = typeof retry === "number"
|
|
2957
|
+
? { attempts: retry }
|
|
2958
|
+
: typeof retry === "object"
|
|
2959
|
+
? retry ?? { attempts: 1 }
|
|
2960
|
+
: { attempts: 1 };
|
|
2961
|
+
let attempts = Number(base.attempts);
|
|
2962
|
+
if (!Number.isFinite(attempts))
|
|
2963
|
+
attempts = 1;
|
|
2964
|
+
// Clamp attempts to [1, 100] (1 = try once, no retries).
|
|
2965
|
+
attempts = Math.max(1, Math.min(100, attempts));
|
|
2966
|
+
const initial_delay = base.initial_delay ?? 100;
|
|
2967
|
+
const max_delay = base.max_delay ?? 1000;
|
|
2968
|
+
const backoff_factor = base.backoff_factor ?? 2;
|
|
2969
|
+
// Small bounded jitter to avoid thundering herd; internal only.
|
|
2970
|
+
const jitter_ratio = 0.2;
|
|
2971
|
+
return {
|
|
2972
|
+
attempts,
|
|
2973
|
+
initial_delay,
|
|
2974
|
+
max_delay,
|
|
2975
|
+
backoff_factor,
|
|
2976
|
+
jitter_ratio,
|
|
2977
|
+
};
|
|
2978
|
+
}
|
|
2979
|
+
Retry.normalize = normalize;
|
|
2980
|
+
/**
|
|
2981
|
+
* Compute a single backoff delay using exponential growth with bounded jitter.
|
|
2982
|
+
*
|
|
2983
|
+
* @param attempt_index Zero-based retry index (0 = first retry).
|
|
2984
|
+
* @param initial_delay Initial delay for the *first* retry.
|
|
2985
|
+
* @param backoff_factor Exponential factor.
|
|
2986
|
+
* @param max_delay Maximum delay cap.
|
|
2987
|
+
* @param jitter_ratio Additive jitter ratio in `[0, 1]`.
|
|
2988
|
+
* @returns Milliseconds to wait before the next retry.
|
|
2989
|
+
*/
|
|
2990
|
+
function compute_backoff_delay(attempt_index, params) {
|
|
2991
|
+
const base = Math.min(params.max_delay, (params.initial_delay <= 0 ? 0 : params.initial_delay) * Math.pow(Math.max(1, params.backoff_factor), attempt_index));
|
|
2992
|
+
if (base <= 0)
|
|
2993
|
+
return 0;
|
|
2994
|
+
// Jitter in [ -j*base, +j*base ]
|
|
2995
|
+
const jitter = (Math.random() * 2 - 1) * (params.jitter_ratio * base);
|
|
2996
|
+
const delay = Math.max(0, Math.min(params.max_delay, base + jitter));
|
|
2997
|
+
return Math.floor(delay);
|
|
2998
|
+
}
|
|
2999
|
+
Retry.compute_backoff_delay = compute_backoff_delay;
|
|
3000
|
+
})(Retry = Collection.Retry || (Collection.Retry = {}));
|
|
3001
|
+
// -------------------------------------------------------------------
|
|
3002
|
+
// Errors.
|
|
3003
|
+
// ---------------------------------------------------------
|
|
3004
|
+
/** The base error for {@link NotFoundError}, {@link DeleteError} etc. */
|
|
3005
|
+
class OperationError extends Error {
|
|
3006
|
+
/** The error message. */
|
|
3007
|
+
message;
|
|
3008
|
+
query;
|
|
3009
|
+
reason;
|
|
3010
|
+
/** An optional error that caused this error. */
|
|
3011
|
+
cause;
|
|
3012
|
+
/** Construct a not found error. */
|
|
3013
|
+
constructor(opts) {
|
|
3014
|
+
super(opts.message);
|
|
3015
|
+
this.message = opts.message;
|
|
3016
|
+
this.name = "OperationError";
|
|
3017
|
+
this.query = opts.query;
|
|
3018
|
+
this.reason = opts.reason;
|
|
3019
|
+
this.cause = opts.cause;
|
|
3020
|
+
Object.setPrototypeOf(this, new.target.prototype); // ensure instanceof works after transpile.
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
Collection.OperationError = OperationError;
|
|
3024
|
+
/**
|
|
3025
|
+
* Error thrown when a document is not found.
|
|
3026
|
+
* This error extends the {@link OperationError} which in turn extends the default {@link Error} class.
|
|
3027
|
+
*/
|
|
3028
|
+
class NotFoundError extends OperationError {
|
|
3029
|
+
/**
|
|
3030
|
+
* Constructor method.
|
|
3031
|
+
* @param opts The error options, see {@link OperationError.Opts}.
|
|
3032
|
+
*/
|
|
3033
|
+
constructor(opts) {
|
|
3034
|
+
super(opts);
|
|
3035
|
+
this.name = "NotFoundError";
|
|
3036
|
+
Object.setPrototypeOf(this, new.target.prototype); // ensure instanceof works after transpile.
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
Collection.NotFoundError = NotFoundError;
|
|
3040
|
+
/**
|
|
3041
|
+
* Error thrown when a {@link Collection.Opts.on_transform_version} callback fails.
|
|
3042
|
+
* This error extends the {@link OperationError} which in turn extends the default {@link Error} class.
|
|
3043
|
+
*/
|
|
3044
|
+
class OnTransformError extends OperationError {
|
|
3045
|
+
/**
|
|
3046
|
+
* Constructor method.
|
|
3047
|
+
* @param opts The error options, see {@link OperationError.Opts}.
|
|
3048
|
+
*/
|
|
3049
|
+
constructor(opts) {
|
|
3050
|
+
super(opts);
|
|
3051
|
+
this.name = "OnTransformError";
|
|
3052
|
+
Object.setPrototypeOf(this, new.target.prototype); // ensure instanceof works after transpile.
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
Collection.OnTransformError = OnTransformError;
|
|
3056
|
+
/**
|
|
3057
|
+
* Error thrown when a {@link Collection.Opts.on_load} callback fails.
|
|
3058
|
+
* This error extends the {@link OperationError} which in turn extends the default {@link Error} class.
|
|
3059
|
+
*/
|
|
3060
|
+
class OnLoadError extends OperationError {
|
|
3061
|
+
/**
|
|
3062
|
+
* Constructor method.
|
|
3063
|
+
* @param opts The error options, see {@link OperationError.Opts}.
|
|
3064
|
+
*/
|
|
3065
|
+
constructor(opts) {
|
|
3066
|
+
super(opts);
|
|
3067
|
+
this.name = "OnLoadError";
|
|
3068
|
+
Object.setPrototypeOf(this, new.target.prototype); // ensure instanceof works after transpile.
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
Collection.OnLoadError = OnLoadError;
|
|
3072
|
+
/**
|
|
3073
|
+
* Error thrown when a count operation fails.
|
|
3074
|
+
* This error extends {@link OperationError} which in turn extends the default {@link Error} class.
|
|
3075
|
+
*/
|
|
3076
|
+
class CountError extends OperationError {
|
|
3077
|
+
/**
|
|
3078
|
+
* Construct a {@link CountError}.
|
|
3079
|
+
* @param opts The error options, see {@link OperationError.Opts}.
|
|
3080
|
+
*/
|
|
3081
|
+
constructor(opts) {
|
|
3082
|
+
super(opts);
|
|
3083
|
+
this.name = "CountError";
|
|
3084
|
+
Object.setPrototypeOf(this, new.target.prototype); // ensure instanceof works after transpile.
|
|
3085
|
+
}
|
|
3086
|
+
}
|
|
3087
|
+
Collection.CountError = CountError;
|
|
3088
|
+
/**
|
|
3089
|
+
* Error thrown when a list operation fails.
|
|
3090
|
+
* This error extends the {@link OperationError} which in turn extends the default {@link Error} class.
|
|
3091
|
+
*/
|
|
3092
|
+
class ListError extends OperationError {
|
|
3093
|
+
/**
|
|
3094
|
+
* Constructor method.
|
|
3095
|
+
* @param opts The error options, see {@link OperationError.Opts}.
|
|
3096
|
+
*/
|
|
3097
|
+
constructor(opts) {
|
|
3098
|
+
super(opts);
|
|
3099
|
+
this.name = "ListError";
|
|
3100
|
+
Object.setPrototypeOf(this, new.target.prototype); // ensure instanceof works after transpile.
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
Collection.ListError = ListError;
|
|
3104
|
+
/**
|
|
3105
|
+
* Error thrown when a load operation fails.
|
|
3106
|
+
* This error extends the {@link OperationError} which in turn extends the default {@link Error} class.
|
|
3107
|
+
*/
|
|
3108
|
+
class ExistsError extends OperationError {
|
|
3109
|
+
/**
|
|
3110
|
+
* Constructor method.
|
|
3111
|
+
* @param opts The error options, see {@link OperationError.Opts}.
|
|
3112
|
+
*/
|
|
3113
|
+
constructor(opts) {
|
|
3114
|
+
super(opts);
|
|
3115
|
+
this.name = "ExistsError";
|
|
3116
|
+
Object.setPrototypeOf(this, new.target.prototype); // ensure instanceof works after transpile.
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
Collection.ExistsError = ExistsError;
|
|
3120
|
+
/**
|
|
3121
|
+
* Error thrown when a load operation fails.
|
|
3122
|
+
* This error extends the {@link OperationError} which in turn extends the default {@link Error} class.
|
|
3123
|
+
*/
|
|
3124
|
+
class LoadError extends OperationError {
|
|
3125
|
+
/**
|
|
3126
|
+
* Constructor method.
|
|
3127
|
+
* @param opts The error options, see {@link OperationError.Opts}.
|
|
3128
|
+
*/
|
|
3129
|
+
constructor(opts) {
|
|
3130
|
+
super(opts);
|
|
3131
|
+
this.name = "LoadError";
|
|
3132
|
+
Object.setPrototypeOf(this, new.target.prototype); // ensure instanceof works after transpile.
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
Collection.LoadError = LoadError;
|
|
3136
|
+
/**
|
|
3137
|
+
* Error thrown when a save operation fails.
|
|
3138
|
+
* This error extends the {@link OperationError} which in turn extends the default {@link Error} class.
|
|
3139
|
+
*/
|
|
3140
|
+
class SaveError extends OperationError {
|
|
3141
|
+
/**
|
|
3142
|
+
* Constructor method.
|
|
3143
|
+
* @param opts The error options, see {@link OperationError.Opts}.
|
|
3144
|
+
*/
|
|
3145
|
+
constructor(opts) {
|
|
3146
|
+
super(opts);
|
|
3147
|
+
this.name = "SaveError";
|
|
3148
|
+
Object.setPrototypeOf(this, new.target.prototype); // ensure instanceof works after transpile.
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
Collection.SaveError = SaveError;
|
|
3152
|
+
/**
|
|
3153
|
+
* Error thrown when a delete operation fails.
|
|
3154
|
+
* This error extends the {@link OperationError} which in turn extends the default {@link Error} class.
|
|
3155
|
+
*/
|
|
3156
|
+
class DeleteError extends OperationError {
|
|
3157
|
+
/**
|
|
3158
|
+
* Constructor method.
|
|
3159
|
+
* @param opts The error options, see {@link OperationError.Opts}.
|
|
3160
|
+
*/
|
|
3161
|
+
constructor(opts) {
|
|
3162
|
+
super(opts);
|
|
3163
|
+
this.name = "DeleteError";
|
|
3164
|
+
Object.setPrototypeOf(this, new.target.prototype); // ensure instanceof works after transpile.
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
Collection.DeleteError = DeleteError;
|
|
3168
|
+
/**
|
|
3169
|
+
* Error thrown when a bulk operation fails.
|
|
3170
|
+
* This error extends the {@link OperationError} which in turn extends the default {@link Error} class.
|
|
3171
|
+
*/
|
|
3172
|
+
class BulkError extends OperationError {
|
|
3173
|
+
/**
|
|
3174
|
+
* Constructor method.
|
|
3175
|
+
* @param opts The error options, see {@link OperationError.Opts}.
|
|
3176
|
+
*/
|
|
3177
|
+
constructor(opts) {
|
|
3178
|
+
super(opts);
|
|
3179
|
+
this.name = "BulkError";
|
|
3180
|
+
Object.setPrototypeOf(this, new.target.prototype); // ensure instanceof works after transpile.
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
Collection.BulkError = BulkError;
|
|
3184
|
+
/**
|
|
3185
|
+
* Error thrown when an aggregate operation fails.
|
|
3186
|
+
* This error extends the {@link OperationError} which in turn extends the default {@link Error} class.
|
|
3187
|
+
*/
|
|
3188
|
+
class AggregateError extends OperationError {
|
|
3189
|
+
/**
|
|
3190
|
+
* Constructor method.
|
|
3191
|
+
* @param opts The error options, see {@link OperationError.Opts}.
|
|
3192
|
+
*/
|
|
3193
|
+
constructor(opts) {
|
|
3194
|
+
super(opts);
|
|
3195
|
+
this.name = "AggregateError";
|
|
3196
|
+
Object.setPrototypeOf(this, new.target.prototype); // ensure instanceof works after transpile.
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
Collection.AggregateError = AggregateError;
|
|
3200
|
+
/** The nested types for the {@link Projection} type. */
|
|
3201
|
+
let Projection;
|
|
3202
|
+
(function (Projection) {
|
|
3203
|
+
/**
|
|
3204
|
+
* Convert a projection query into a MongoDB-compatible format.
|
|
3205
|
+
* @throws An error if both inclusion (1) and exclusion (0) patterns are found,
|
|
3206
|
+
* since this is not allowed by mongodb.
|
|
3207
|
+
*/
|
|
3208
|
+
function init(projection) {
|
|
3209
|
+
if (Array.isArray(projection)) {
|
|
3210
|
+
const p = {};
|
|
3211
|
+
for (let i = 0; i < projection.length; i++) {
|
|
3212
|
+
p[projection[i]] = 1;
|
|
3213
|
+
}
|
|
3214
|
+
return p;
|
|
3215
|
+
}
|
|
3216
|
+
else {
|
|
3217
|
+
const p = projection;
|
|
3218
|
+
// object form
|
|
3219
|
+
let has_include = false;
|
|
3220
|
+
let has_exclude = false;
|
|
3221
|
+
for (const [k, v] of Object.entries(p)) {
|
|
3222
|
+
if (v === 1 || v === true) {
|
|
3223
|
+
if (k !== "_id")
|
|
3224
|
+
has_include = true;
|
|
3225
|
+
}
|
|
3226
|
+
else if (v === 0 || v === false) {
|
|
3227
|
+
if (k !== "_id")
|
|
3228
|
+
has_exclude = true;
|
|
3229
|
+
}
|
|
3230
|
+
else {
|
|
3231
|
+
throw new InvalidUsageError({
|
|
3232
|
+
message: `Invalid projection value for "${k}": expected 0, 1, true or false.`,
|
|
3233
|
+
reason: "invalid_projection",
|
|
3234
|
+
});
|
|
3235
|
+
}
|
|
3236
|
+
if (has_include && has_exclude) {
|
|
3237
|
+
throw new InvalidUsageError({
|
|
3238
|
+
message: "Invalid projection: cannot mix inclusion and exclusion (except for _id).",
|
|
3239
|
+
reason: "invalid_projection",
|
|
3240
|
+
});
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
return p;
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
Projection.init = init;
|
|
3247
|
+
})(Projection = Collection.Projection || (Collection.Projection = {}));
|
|
3248
|
+
// Unit tests for `ProjectedDocument`.
|
|
3249
|
+
{
|
|
3250
|
+
}
|
|
3251
|
+
})(Collection || (Collection = {}));
|
|
3252
|
+
// ---------------------------------------------------------
|
|
3253
|
+
// The extended transaction based collection class.
|
|
3254
|
+
// ---------------------------------------------------------
|
|
3255
|
+
/**
|
|
3256
|
+
* TransactionCollection extends Collection with transaction-specific methods.
|
|
3257
|
+
* This class provides commit and abort functionality for MongoDB transactions.
|
|
3258
|
+
*/
|
|
3259
|
+
export class TransactionCollection extends Collection {
|
|
3260
|
+
async commit() {
|
|
3261
|
+
const session = this._session;
|
|
3262
|
+
if (!session) {
|
|
3263
|
+
throw new InvalidUsageError({
|
|
3264
|
+
message: "No active session for this transaction.",
|
|
3265
|
+
reason: "no_session",
|
|
3266
|
+
});
|
|
3267
|
+
}
|
|
3268
|
+
if (this.is_finalized_transaction) {
|
|
3269
|
+
throw new InvalidUsageError({
|
|
3270
|
+
message: "Transaction has already been finalized.",
|
|
3271
|
+
reason: "transaction_finalized",
|
|
3272
|
+
});
|
|
3273
|
+
}
|
|
3274
|
+
// if (typeof (session as any).inTransaction === "function" && !(session as any).inTransaction()) {
|
|
3275
|
+
// throw new Error("Cannot commit: session is not in a transaction.");
|
|
3276
|
+
// }
|
|
3277
|
+
const max_retries_unknown = 10; // for UnknownTransactionCommitResult / network-ish
|
|
3278
|
+
const base_delay_ms = 20;
|
|
3279
|
+
const max_delay_ms = 1000;
|
|
3280
|
+
for (let attempt = 0; attempt <= max_retries_unknown; attempt++) {
|
|
3281
|
+
try {
|
|
3282
|
+
await session.commitTransaction();
|
|
3283
|
+
this.is_finalized_transaction = true;
|
|
3284
|
+
try {
|
|
3285
|
+
await session.endSession();
|
|
3286
|
+
}
|
|
3287
|
+
finally {
|
|
3288
|
+
this._session = undefined;
|
|
3289
|
+
}
|
|
3290
|
+
return;
|
|
3291
|
+
}
|
|
3292
|
+
catch (err) {
|
|
3293
|
+
const has_label = (label) => {
|
|
3294
|
+
if (!err || typeof err !== "object") {
|
|
3295
|
+
return false;
|
|
3296
|
+
}
|
|
3297
|
+
if (typeof err?.hasErrorLabel === "function") {
|
|
3298
|
+
try {
|
|
3299
|
+
return !!err.hasErrorLabel(label);
|
|
3300
|
+
}
|
|
3301
|
+
catch { }
|
|
3302
|
+
}
|
|
3303
|
+
return Array.isArray(err?.errorLabels) && err.errorLabels.includes(label);
|
|
3304
|
+
};
|
|
3305
|
+
const unknown_commit = has_label("UnknownTransactionCommitResult");
|
|
3306
|
+
const transient = has_label("TransientTransactionError");
|
|
3307
|
+
const is_networkish = err?.name === "MongoNetworkError" || err?.name === "MongoNetworkTimeoutError";
|
|
3308
|
+
// const no_such_txn = err?.codeName === "NoSuchTransaction";
|
|
3309
|
+
// Unknown outcome or network glitch: retry commit with backoff
|
|
3310
|
+
if ((unknown_commit || is_networkish) && attempt < max_retries_unknown) {
|
|
3311
|
+
const delay = Math.min(max_delay_ms, base_delay_ms * Math.pow(2, attempt));
|
|
3312
|
+
await new Promise(res => setTimeout(res, delay));
|
|
3313
|
+
continue;
|
|
3314
|
+
}
|
|
3315
|
+
// Transient: abort and tell caller to retry the whole transaction
|
|
3316
|
+
if (transient) {
|
|
3317
|
+
try {
|
|
3318
|
+
await session.abortTransaction();
|
|
3319
|
+
}
|
|
3320
|
+
catch { }
|
|
3321
|
+
this.is_finalized_transaction = true;
|
|
3322
|
+
try {
|
|
3323
|
+
await session.endSession();
|
|
3324
|
+
}
|
|
3325
|
+
finally {
|
|
3326
|
+
this._session = undefined;
|
|
3327
|
+
}
|
|
3328
|
+
const e = new Error(`TransientTransactionError during commit; transaction aborted. Retry the entire transaction. ${err?.message ?? ""}`);
|
|
3329
|
+
e.codeName = err?.codeName;
|
|
3330
|
+
e.errorLabels = err?.errorLabels;
|
|
3331
|
+
throw e;
|
|
3332
|
+
}
|
|
3333
|
+
// Already ended on server: consider finalized
|
|
3334
|
+
// DONT SILENTLY ALLOW THIS.
|
|
3335
|
+
// if (no_such_txn) {
|
|
3336
|
+
// this.is_finalized_transaction = true;
|
|
3337
|
+
// try { await session.endSession(); } finally { this._session = undefined; }
|
|
3338
|
+
// return;
|
|
3339
|
+
// }
|
|
3340
|
+
// Exceeded retries for unknown outcome / network-ish
|
|
3341
|
+
if ((unknown_commit || is_networkish) && attempt >= max_retries_unknown) {
|
|
3342
|
+
this.is_finalized_transaction = true;
|
|
3343
|
+
try {
|
|
3344
|
+
await session.endSession();
|
|
3345
|
+
}
|
|
3346
|
+
finally {
|
|
3347
|
+
this._session = undefined;
|
|
3348
|
+
}
|
|
3349
|
+
const e = new Error(`Commit failed after ${attempt + 1} attempt(s) with unknown outcome; last error: ${err?.message ?? err}`);
|
|
3350
|
+
e.codeName = err?.codeName;
|
|
3351
|
+
e.errorLabels = err?.errorLabels;
|
|
3352
|
+
throw e;
|
|
3353
|
+
}
|
|
3354
|
+
// Non-retryable: finalize and rethrow
|
|
3355
|
+
this.is_finalized_transaction = true;
|
|
3356
|
+
try {
|
|
3357
|
+
await session.endSession();
|
|
3358
|
+
}
|
|
3359
|
+
finally {
|
|
3360
|
+
this._session = undefined;
|
|
3361
|
+
}
|
|
3362
|
+
throw err;
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3366
|
+
async abort() {
|
|
3367
|
+
const session = this._session;
|
|
3368
|
+
if (!session) {
|
|
3369
|
+
throw new InvalidUsageError({
|
|
3370
|
+
message: "No active session for this transaction.",
|
|
3371
|
+
reason: "no_session",
|
|
3372
|
+
});
|
|
3373
|
+
}
|
|
3374
|
+
if (this.is_finalized_transaction) {
|
|
3375
|
+
throw new InvalidUsageError({
|
|
3376
|
+
message: "Transaction has already been finalized.",
|
|
3377
|
+
reason: "transaction_finalized",
|
|
3378
|
+
});
|
|
3379
|
+
}
|
|
3380
|
+
const max_retries = 5;
|
|
3381
|
+
const base_delay_ms = 20;
|
|
3382
|
+
const max_delay_ms = 500;
|
|
3383
|
+
for (let attempt = 0; attempt <= max_retries; attempt++) {
|
|
3384
|
+
try {
|
|
3385
|
+
await session.abortTransaction();
|
|
3386
|
+
this.is_finalized_transaction = true;
|
|
3387
|
+
try {
|
|
3388
|
+
await session.endSession();
|
|
3389
|
+
}
|
|
3390
|
+
finally {
|
|
3391
|
+
this._session = undefined;
|
|
3392
|
+
}
|
|
3393
|
+
return;
|
|
3394
|
+
}
|
|
3395
|
+
catch (err) {
|
|
3396
|
+
// If server says it doesn't exist, treat as already aborted/ended
|
|
3397
|
+
if (err?.codeName === "NoSuchTransaction") {
|
|
3398
|
+
this.is_finalized_transaction = true;
|
|
3399
|
+
try {
|
|
3400
|
+
await session.endSession();
|
|
3401
|
+
}
|
|
3402
|
+
finally {
|
|
3403
|
+
this._session = undefined;
|
|
3404
|
+
}
|
|
3405
|
+
return;
|
|
3406
|
+
}
|
|
3407
|
+
const has_label = (label) => {
|
|
3408
|
+
if (!err || typeof err !== "object") {
|
|
3409
|
+
return false;
|
|
3410
|
+
}
|
|
3411
|
+
if (typeof err?.hasErrorLabel === "function") {
|
|
3412
|
+
try {
|
|
3413
|
+
return !!err.hasErrorLabel(label);
|
|
3414
|
+
}
|
|
3415
|
+
catch { }
|
|
3416
|
+
}
|
|
3417
|
+
return Array.isArray(err?.errorLabels) && err.errorLabels.includes(label);
|
|
3418
|
+
};
|
|
3419
|
+
const transient = has_label("TransientTransactionError");
|
|
3420
|
+
const is_networkish = err?.name === "MongoNetworkError" || err?.name === "MongoNetworkTimeoutError";
|
|
3421
|
+
// Transient outcome or network glitch: retry commit with backoff
|
|
3422
|
+
if ((transient || is_networkish) && attempt < max_retries) {
|
|
3423
|
+
const delay = Math.min(max_delay_ms, base_delay_ms * Math.pow(2, attempt));
|
|
3424
|
+
await new Promise(res => setTimeout(res, delay));
|
|
3425
|
+
continue;
|
|
3426
|
+
}
|
|
3427
|
+
// Give up: finalize and rethrow
|
|
3428
|
+
this.is_finalized_transaction = true;
|
|
3429
|
+
try {
|
|
3430
|
+
await session.endSession();
|
|
3431
|
+
}
|
|
3432
|
+
finally {
|
|
3433
|
+
this._session = undefined;
|
|
3434
|
+
}
|
|
3435
|
+
throw err;
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
3439
|
+
/**
|
|
3440
|
+
* Cleanup method for proper resource management
|
|
3441
|
+
* Can be called manually or via async disposal
|
|
3442
|
+
*
|
|
3443
|
+
* @warning This method aborts the transaction if it is still active.
|
|
3444
|
+
*/
|
|
3445
|
+
async cleanup() {
|
|
3446
|
+
if (this._session && !this.is_finalized_transaction) {
|
|
3447
|
+
try {
|
|
3448
|
+
await this.abort();
|
|
3449
|
+
}
|
|
3450
|
+
catch (error) {
|
|
3451
|
+
console.error('Failed to abort transaction during cleanup:', error);
|
|
3452
|
+
// Still try to end the session
|
|
3453
|
+
if (this._session) {
|
|
3454
|
+
try {
|
|
3455
|
+
await this._session.endSession();
|
|
3456
|
+
}
|
|
3457
|
+
catch (endError) {
|
|
3458
|
+
console.error('Failed to end session during cleanup:', endError);
|
|
3459
|
+
}
|
|
3460
|
+
}
|
|
3461
|
+
}
|
|
3462
|
+
finally {
|
|
3463
|
+
this.is_finalized_transaction = true;
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
// Support for async disposal (TC39 proposal)
|
|
3468
|
+
async [Symbol.asyncDispose]() {
|
|
3469
|
+
await this.cleanup();
|
|
3470
|
+
}
|
|
3471
|
+
/**
|
|
3472
|
+
* Check if the transaction is still active (not finalized).
|
|
3473
|
+
* @returns True if the transaction is active, false otherwise.
|
|
3474
|
+
*/
|
|
3475
|
+
is_active() {
|
|
3476
|
+
return this.is_transaction && !this.is_finalized_transaction && this._session != null;
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3479
|
+
// -------------------------------------------------------
|
|
3480
|
+
// Some unit tests for save.
|
|
3481
|
+
// -------------------------------------------------------
|
|
3482
|
+
async function test_save() {
|
|
3483
|
+
const res = void await test_col.save({ uid: "" },
|
|
3484
|
+
// @ts-ignore
|
|
3485
|
+
{ uid: "" }, { return: true });
|
|
3486
|
+
const res_no_throw = await test_col.save({ uid: "" },
|
|
3487
|
+
// @ts-ignore
|
|
3488
|
+
{ uid: "" }, { return: true, throw: false, bulk: false });
|
|
3489
|
+
function init_save_opts(opts) {
|
|
3490
|
+
return opts;
|
|
3491
|
+
}
|
|
3492
|
+
// ok: bulk path
|
|
3493
|
+
const a = init_save_opts({ bulk: true, upsert: true });
|
|
3494
|
+
// ok: no return, throw not allowed
|
|
3495
|
+
const b = init_save_opts({ return: false, upsert: true });
|
|
3496
|
+
// ok; throw `true` allowed when return is `false`
|
|
3497
|
+
const b2 = init_save_opts({ return: false, throw: true });
|
|
3498
|
+
// ok: return + no upsert, throw allowed
|
|
3499
|
+
const c = init_save_opts({ return: true, upsert: false, throw: false });
|
|
3500
|
+
// @ts-expect-error ❌ bulk not allowed when return is true
|
|
3501
|
+
const e = init_save_opts({ return: true, upsert: true, bulk: true });
|
|
3502
|
+
const res_bulk_op = await test_col.save({ uid: "" }, { uid: "" }, { bulk: true });
|
|
3503
|
+
const res_undef = await test_col.save({ uid: "" }, { uid: "" });
|
|
3504
|
+
const res_doc = await test_col.save({ uid: "" }, { uid: "" }, { return: true });
|
|
3505
|
+
const res_doc_or_undef = await test_col.save({ uid: "" }, { uid: "" }, { return: true, throw: false, upsert: false });
|
|
3506
|
+
async function save_wrapper(doc, bulk) {
|
|
3507
|
+
return await test_col.save({ id: "test" }, { $set: doc }, { bulk });
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
// -------------------------------------------------------
|