@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.
Files changed (502) hide show
  1. package/backend/dist/cjs/{blacklist.d.ts → backend/src/blacklist.d.ts} +5 -3
  2. package/backend/dist/cjs/{blacklist.js → backend/src/blacklist.js} +8 -5
  3. package/backend/dist/cjs/{cli.js → backend/src/cli.js} +29 -47
  4. package/backend/dist/cjs/backend/src/database/collection.d.ts +1543 -0
  5. package/backend/dist/cjs/backend/src/database/collection.js +3042 -0
  6. package/backend/dist/cjs/backend/src/database/database.d.ts +66 -0
  7. package/backend/dist/cjs/{database → backend/src/database}/database.js +48 -43
  8. package/backend/dist/cjs/backend/src/database/filters/filters.d.ts +6 -0
  9. package/backend/dist/cjs/backend/src/database/filters/filters.js +15 -0
  10. package/backend/dist/cjs/backend/src/database/filters/strict_filter.d.ts +223 -0
  11. package/backend/dist/cjs/backend/src/database/filters/strict_filter.js +15 -0
  12. package/backend/dist/cjs/backend/src/database/filters/strict_filter_test.js +443 -0
  13. package/backend/dist/cjs/backend/src/database/filters/strict_filter_test_v0.js +15 -0
  14. package/backend/dist/cjs/backend/src/database/filters/strict_filter_v0.d.ts +50 -0
  15. package/backend/dist/cjs/backend/src/database/filters/strict_filter_v0.js +15 -0
  16. package/backend/dist/cjs/backend/src/database/filters/strict_filter_v1.d.ts +76 -0
  17. package/backend/dist/cjs/backend/src/database/filters/strict_filter_v1.js +15 -0
  18. package/backend/dist/cjs/backend/src/database/filters/strict_filter_v2.d.ts +75 -0
  19. package/backend/dist/cjs/backend/src/database/filters/strict_filter_v2.js +15 -0
  20. package/backend/dist/cjs/backend/src/database/filters/strict_filter_v3.d.ts +219 -0
  21. package/backend/dist/cjs/backend/src/database/filters/strict_filter_v3.js +15 -0
  22. package/backend/dist/cjs/backend/src/database/filters/strict_update_filter.d.ts +165 -0
  23. package/backend/dist/cjs/backend/src/database/filters/strict_update_filter.js +15 -0
  24. package/backend/dist/cjs/backend/src/database/filters/strict_update_filter_test.d.ts +5 -0
  25. package/backend/dist/cjs/backend/src/database/filters/strict_update_filter_test.js +355 -0
  26. package/backend/dist/cjs/backend/src/database/flatten.d.ts +75 -0
  27. package/backend/dist/cjs/{logger.js → backend/src/database/flatten.js} +18 -7
  28. package/backend/dist/cjs/backend/src/database/flatten_test.js +175 -0
  29. package/backend/dist/cjs/backend/src/database/quota/quota.d.ts +461 -0
  30. package/backend/dist/cjs/backend/src/database/quota/quota.js +1014 -0
  31. package/backend/dist/cjs/backend/src/database/quota/quota_v1.d.ts +534 -0
  32. package/backend/dist/cjs/backend/src/database/quota/quota_v1.js +1087 -0
  33. package/backend/dist/cjs/backend/src/database/quota/safe_int.d.ts +293 -0
  34. package/backend/dist/cjs/backend/src/database/quota/safe_int.js +573 -0
  35. package/backend/dist/{esm → cjs/backend/src}/endpoint.d.ts +69 -46
  36. package/backend/dist/cjs/{endpoint.js → backend/src/endpoint.js} +87 -101
  37. package/backend/dist/cjs/backend/src/errors/index.d.ts +7 -0
  38. package/backend/dist/cjs/backend/src/errors/index.js +25 -0
  39. package/backend/dist/{esm/utils.d.ts → cjs/backend/src/errors/internal_external.d.ts} +14 -22
  40. package/backend/dist/cjs/backend/src/errors/internal_external.js +85 -0
  41. package/backend/dist/cjs/backend/src/errors/invalid_usage_error.d.ts +38 -0
  42. package/backend/dist/cjs/{mutex.js → backend/src/errors/invalid_usage_error.js} +20 -37
  43. package/backend/dist/cjs/backend/src/errors/system_error.d.ts +230 -0
  44. package/backend/dist/cjs/backend/src/errors/system_error.js +393 -0
  45. package/backend/dist/cjs/backend/src/events.d.ts +54 -0
  46. package/backend/dist/cjs/backend/src/events.js +15 -0
  47. package/backend/dist/cjs/{frontend.js → backend/src/frontend.js} +1 -1
  48. package/backend/dist/cjs/{image_endpoint.d.ts → backend/src/image_endpoint.d.ts} +16 -1
  49. package/backend/dist/cjs/{image_endpoint.js → backend/src/image_endpoint.js} +3 -5
  50. package/backend/dist/cjs/backend/src/logger.d.ts +5 -0
  51. package/backend/dist/cjs/backend/src/logger.js +15 -0
  52. package/backend/dist/cjs/backend/src/meta.d.ts +64 -0
  53. package/backend/dist/cjs/{meta.js → backend/src/meta.js} +9 -12
  54. package/backend/dist/cjs/backend/src/payments/paddle.d.ts +326 -0
  55. package/backend/dist/cjs/{payments → backend/src/payments}/paddle.js +377 -327
  56. package/backend/dist/cjs/backend/src/plugins/browser.d.ts +1 -0
  57. package/backend/dist/cjs/backend/src/plugins/browser.js +15 -0
  58. package/backend/dist/cjs/backend/src/plugins/mail/mail.d.ts +248 -0
  59. package/backend/dist/cjs/backend/src/plugins/mail/mail.js +379 -0
  60. package/backend/dist/{esm → cjs/backend/src}/plugins/mail/ui.d.ts +23 -0
  61. package/backend/dist/cjs/backend/src/plugins/pdf.d.ts +1 -0
  62. package/backend/dist/cjs/backend/src/rate_limit.d.ts +145 -0
  63. package/backend/dist/cjs/backend/src/rate_limit.js +549 -0
  64. package/backend/dist/cjs/{route.d.ts → backend/src/route.d.ts} +3 -10
  65. package/backend/dist/cjs/{route.js → backend/src/route.js} +23 -21
  66. package/backend/dist/cjs/backend/src/server.d.ts +485 -0
  67. package/backend/dist/cjs/{server.js → backend/src/server.js} +688 -873
  68. package/backend/dist/cjs/backend/src/splash_screen.d.ts +80 -0
  69. package/backend/dist/cjs/{splash_screen.js → backend/src/splash_screen.js} +24 -3
  70. package/backend/dist/cjs/backend/src/status.d.ts +74 -0
  71. package/backend/dist/cjs/{status.js → backend/src/status.js} +64 -64
  72. package/backend/dist/cjs/backend/src/stream.d.ts +376 -0
  73. package/backend/dist/cjs/{stream.js → backend/src/stream.js} +299 -276
  74. package/backend/dist/cjs/backend/src/users.d.ts +807 -0
  75. package/backend/dist/cjs/backend/src/users.js +1971 -0
  76. package/backend/dist/cjs/backend/src/utils.d.ts +16 -0
  77. package/backend/dist/cjs/{utils.js → backend/src/utils.js} +14 -77
  78. package/backend/dist/{esm → cjs/backend/src}/view.d.ts +33 -11
  79. package/backend/dist/cjs/backend/src/view.js +508 -0
  80. package/backend/dist/{esm → cjs/backend/src}/volt.d.ts +10 -1
  81. package/backend/dist/cjs/{volt.js → backend/src/volt.js} +8 -5
  82. package/backend/dist/cjs/frontend/src/modules/request.d.ts +70 -0
  83. package/backend/dist/cjs/frontend/src/modules/request.js +99 -0
  84. package/backend/dist/esm/{blacklist.d.ts → backend/src/blacklist.d.ts} +5 -3
  85. package/backend/dist/esm/{blacklist.js → backend/src/blacklist.js} +9 -6
  86. package/backend/dist/esm/{cli.js → backend/src/cli.js} +43 -60
  87. package/backend/dist/esm/backend/src/database/collection.d.ts +1543 -0
  88. package/backend/dist/esm/backend/src/database/collection.js +3510 -0
  89. package/backend/dist/esm/backend/src/database/database.d.ts +66 -0
  90. package/backend/dist/esm/{database → backend/src/database}/database.js +62 -103
  91. package/backend/dist/esm/backend/src/database/document.d.ts +1 -0
  92. package/backend/dist/esm/backend/src/database/document.js +558 -0
  93. package/backend/dist/esm/backend/src/database/filters/filters.d.ts +6 -0
  94. package/backend/dist/esm/backend/src/database/filters/filters.js +1 -0
  95. package/backend/dist/esm/backend/src/database/filters/strict_filter.d.ts +223 -0
  96. package/backend/dist/esm/backend/src/database/filters/strict_filter.js +3 -0
  97. package/backend/dist/esm/backend/src/database/filters/strict_filter_test.d.ts +1 -0
  98. package/backend/dist/esm/backend/src/database/filters/strict_filter_test.js +505 -0
  99. package/backend/dist/esm/backend/src/database/filters/strict_filter_test_v0.d.ts +1 -0
  100. package/backend/dist/esm/backend/src/database/filters/strict_filter_test_v0.js +712 -0
  101. package/backend/dist/esm/backend/src/database/filters/strict_filter_v0.d.ts +50 -0
  102. package/backend/dist/esm/backend/src/database/filters/strict_filter_v0.js +5 -0
  103. package/backend/dist/esm/backend/src/database/filters/strict_filter_v1.d.ts +76 -0
  104. package/backend/dist/esm/backend/src/database/filters/strict_filter_v1.js +44 -0
  105. package/backend/dist/esm/backend/src/database/filters/strict_filter_v2.d.ts +75 -0
  106. package/backend/dist/esm/backend/src/database/filters/strict_filter_v2.js +5 -0
  107. package/backend/dist/esm/backend/src/database/filters/strict_filter_v3.d.ts +219 -0
  108. package/backend/dist/esm/backend/src/database/filters/strict_filter_v3.js +1 -0
  109. package/backend/dist/esm/backend/src/database/filters/strict_update_filter.d.ts +165 -0
  110. package/backend/dist/esm/backend/src/database/filters/strict_update_filter.js +5 -0
  111. package/backend/dist/esm/backend/src/database/filters/strict_update_filter_test.d.ts +5 -0
  112. package/backend/dist/esm/backend/src/database/filters/strict_update_filter_test.js +405 -0
  113. package/backend/dist/esm/backend/src/database/flatten.d.ts +75 -0
  114. package/backend/dist/esm/backend/src/database/flatten.js +22 -0
  115. package/backend/dist/esm/backend/src/database/flatten_test.d.ts +1 -0
  116. package/backend/dist/esm/backend/src/database/flatten_test.js +174 -0
  117. package/backend/dist/esm/backend/src/database/quota/quota.d.ts +461 -0
  118. package/backend/dist/esm/backend/src/database/quota/quota.js +1118 -0
  119. package/backend/dist/esm/backend/src/database/quota/quota_v1.d.ts +534 -0
  120. package/backend/dist/esm/backend/src/database/quota/quota_v1.js +1242 -0
  121. package/backend/dist/esm/backend/src/database/quota/safe_int.d.ts +293 -0
  122. package/backend/dist/esm/backend/src/database/quota/safe_int.js +602 -0
  123. package/backend/dist/{cjs → esm/backend/src}/endpoint.d.ts +69 -46
  124. package/backend/dist/esm/{endpoint.js → backend/src/endpoint.js} +136 -127
  125. package/backend/dist/esm/backend/src/errors/index.d.ts +7 -0
  126. package/backend/dist/esm/backend/src/errors/index.js +7 -0
  127. package/backend/dist/{cjs/utils.d.ts → esm/backend/src/errors/internal_external.d.ts} +14 -22
  128. package/backend/dist/esm/backend/src/errors/internal_external.js +70 -0
  129. package/backend/dist/esm/backend/src/errors/invalid_usage_error.d.ts +38 -0
  130. package/backend/dist/esm/backend/src/errors/invalid_usage_error.js +30 -0
  131. package/backend/dist/esm/backend/src/errors/system_error.d.ts +230 -0
  132. package/backend/dist/esm/backend/src/errors/system_error.js +402 -0
  133. package/backend/dist/esm/backend/src/events.d.ts +54 -0
  134. package/backend/dist/esm/backend/src/events.js +5 -0
  135. package/backend/dist/esm/{frontend.js → backend/src/frontend.js} +1 -1
  136. package/backend/dist/esm/{image_endpoint.d.ts → backend/src/image_endpoint.d.ts} +16 -1
  137. package/backend/dist/esm/{image_endpoint.js → backend/src/image_endpoint.js} +16 -20
  138. package/backend/dist/esm/backend/src/logger.d.ts +5 -0
  139. package/backend/dist/esm/backend/src/logger.js +8 -0
  140. package/backend/dist/esm/backend/src/meta.d.ts +64 -0
  141. package/backend/dist/esm/{meta.js → backend/src/meta.js} +15 -54
  142. package/backend/dist/esm/backend/src/payments/paddle.d.ts +326 -0
  143. package/backend/dist/esm/{payments → backend/src/payments}/paddle.js +417 -452
  144. package/backend/dist/esm/backend/src/plugins/browser.d.ts +1 -0
  145. package/backend/dist/esm/backend/src/plugins/browser.js +170 -0
  146. package/backend/dist/esm/backend/src/plugins/mail/mail.d.ts +248 -0
  147. package/backend/dist/esm/backend/src/plugins/mail/mail.js +389 -0
  148. package/backend/dist/{cjs → esm/backend/src}/plugins/mail/ui.d.ts +23 -0
  149. package/backend/dist/esm/{plugins → backend/src/plugins}/mail/ui.js +3 -6
  150. package/backend/dist/esm/backend/src/plugins/pdf.d.ts +1 -0
  151. package/backend/dist/esm/{plugins → backend/src/plugins}/pdf.js +3 -3
  152. package/backend/dist/esm/backend/src/rate_limit.d.ts +145 -0
  153. package/backend/dist/esm/backend/src/rate_limit.js +667 -0
  154. package/backend/dist/esm/{route.d.ts → backend/src/route.d.ts} +3 -10
  155. package/backend/dist/esm/{route.js → backend/src/route.js} +26 -21
  156. package/backend/dist/esm/backend/src/server.d.ts +485 -0
  157. package/backend/dist/esm/{server.js → backend/src/server.js} +891 -1441
  158. package/backend/dist/esm/backend/src/splash_screen.d.ts +80 -0
  159. package/backend/dist/esm/{splash_screen.js → backend/src/splash_screen.js} +42 -55
  160. package/backend/dist/esm/backend/src/status.d.ts +74 -0
  161. package/backend/dist/esm/backend/src/status.js +199 -0
  162. package/backend/dist/esm/backend/src/stream.d.ts +376 -0
  163. package/backend/dist/esm/{stream.js → backend/src/stream.js} +327 -292
  164. package/backend/dist/esm/backend/src/users.d.ts +809 -0
  165. package/backend/dist/esm/backend/src/users.js +2140 -0
  166. package/backend/dist/esm/backend/src/utils.d.ts +16 -0
  167. package/backend/dist/esm/{utils.js → backend/src/utils.js} +20 -81
  168. package/backend/dist/{cjs → esm/backend/src}/view.d.ts +33 -11
  169. package/backend/dist/esm/{view.js → backend/src/view.js} +266 -86
  170. package/backend/dist/{cjs → esm/backend/src}/volt.d.ts +10 -1
  171. package/backend/dist/esm/{volt.js → backend/src/volt.js} +7 -4
  172. package/backend/dist/esm/frontend/src/modules/request.d.ts +70 -0
  173. package/backend/dist/esm/frontend/src/modules/request.js +117 -0
  174. package/frontend/dist/backend/src/database/collection.d.ts +1543 -0
  175. package/frontend/dist/backend/src/database/collection.js +3510 -0
  176. package/frontend/dist/backend/src/database/database.d.ts +66 -0
  177. package/frontend/dist/backend/src/database/database.js +196 -0
  178. package/frontend/dist/backend/src/database/filters/filters.d.ts +6 -0
  179. package/frontend/dist/backend/src/database/filters/filters.js +1 -0
  180. package/frontend/dist/backend/src/database/filters/strict_filter.d.ts +223 -0
  181. package/frontend/dist/backend/src/database/filters/strict_filter.js +3 -0
  182. package/frontend/dist/backend/src/database/filters/strict_update_filter.d.ts +165 -0
  183. package/frontend/dist/backend/src/database/filters/strict_update_filter.js +5 -0
  184. package/frontend/dist/backend/src/database/flatten.d.ts +75 -0
  185. package/frontend/dist/backend/src/database/flatten.js +22 -0
  186. package/frontend/dist/backend/src/endpoint.d.ts +204 -0
  187. package/frontend/dist/backend/src/endpoint.js +570 -0
  188. package/frontend/dist/backend/src/errors/index.d.ts +7 -0
  189. package/frontend/dist/backend/src/errors/index.js +7 -0
  190. package/frontend/dist/backend/src/errors/internal_external.d.ts +38 -0
  191. package/frontend/dist/backend/src/errors/internal_external.js +70 -0
  192. package/frontend/dist/backend/src/errors/invalid_usage_error.d.ts +38 -0
  193. package/frontend/dist/backend/src/errors/invalid_usage_error.js +30 -0
  194. package/frontend/dist/backend/src/errors/system_error.d.ts +230 -0
  195. package/frontend/dist/backend/src/errors/system_error.js +402 -0
  196. package/frontend/dist/backend/src/events.d.ts +54 -0
  197. package/frontend/dist/backend/src/events.js +5 -0
  198. package/frontend/dist/backend/src/frontend.d.ts +11 -0
  199. package/frontend/dist/backend/src/frontend.js +12 -0
  200. package/frontend/dist/backend/src/image_endpoint.d.ts +39 -0
  201. package/frontend/dist/backend/src/image_endpoint.js +202 -0
  202. package/frontend/dist/backend/src/meta.d.ts +64 -0
  203. package/frontend/dist/backend/src/meta.js +110 -0
  204. package/frontend/dist/backend/src/payments/paddle.d.ts +326 -0
  205. package/frontend/dist/backend/src/payments/paddle.js +2256 -0
  206. package/frontend/dist/backend/src/plugins/mail/mail.d.ts +248 -0
  207. package/frontend/dist/backend/src/plugins/mail/mail.js +389 -0
  208. package/{backend/dist/esm/plugins/mail.d.ts → frontend/dist/backend/src/plugins/mail/ui.d.ts} +23 -0
  209. package/{backend/dist/esm/plugins/mail.js → frontend/dist/backend/src/plugins/mail/ui.js} +3 -6
  210. package/frontend/dist/backend/src/rate_limit.d.ts +145 -0
  211. package/frontend/dist/backend/src/rate_limit.js +673 -0
  212. package/frontend/dist/backend/src/route.d.ts +35 -0
  213. package/frontend/dist/backend/src/route.js +212 -0
  214. package/frontend/dist/backend/src/server.d.ts +485 -0
  215. package/frontend/dist/backend/src/server.js +2670 -0
  216. package/frontend/dist/backend/src/splash_screen.d.ts +80 -0
  217. package/frontend/dist/backend/src/splash_screen.js +135 -0
  218. package/frontend/dist/backend/src/status.d.ts +74 -0
  219. package/frontend/dist/backend/src/status.js +199 -0
  220. package/frontend/dist/backend/src/stream.d.ts +376 -0
  221. package/frontend/dist/backend/src/stream.js +1007 -0
  222. package/frontend/dist/backend/src/users.d.ts +807 -0
  223. package/frontend/dist/backend/src/users.js +2118 -0
  224. package/frontend/dist/backend/src/utils.d.ts +16 -0
  225. package/frontend/dist/backend/src/utils.js +241 -0
  226. package/frontend/dist/backend/src/view.d.ts +162 -0
  227. package/frontend/dist/backend/src/view.js +720 -0
  228. package/frontend/dist/frontend/src/elements/base.d.ts +4414 -0
  229. package/frontend/dist/{elements → frontend/src/elements}/base.js +3624 -260
  230. package/frontend/dist/frontend/src/elements/module.d.ts +95 -0
  231. package/frontend/dist/{elements → frontend/src/elements}/module.js +53 -52
  232. package/frontend/dist/frontend/src/elements/types.d.ts +52 -0
  233. package/frontend/dist/frontend/src/elements/types.js +5 -0
  234. package/frontend/dist/frontend/src/modules/attachment.d.ts +126 -0
  235. package/frontend/dist/frontend/src/modules/attachment.js +306 -0
  236. package/frontend/dist/frontend/src/modules/auth.d.ts +44 -0
  237. package/frontend/dist/frontend/src/modules/auth.js +80 -0
  238. package/frontend/dist/{modules → frontend/src/modules}/color.js +2 -2
  239. package/frontend/dist/frontend/src/modules/compression.d.ts +39 -0
  240. package/frontend/dist/frontend/src/modules/compression.js +102 -0
  241. package/frontend/dist/frontend/src/modules/cookies.d.ts +44 -0
  242. package/frontend/dist/frontend/src/modules/cookies.js +143 -0
  243. package/frontend/dist/frontend/src/modules/events.d.ts +31 -0
  244. package/frontend/dist/frontend/src/modules/events.js +74 -0
  245. package/frontend/dist/frontend/src/modules/google.d.ts +23 -0
  246. package/frontend/dist/frontend/src/modules/google.js +52 -0
  247. package/frontend/dist/frontend/src/modules/meta.d.ts +14 -0
  248. package/frontend/dist/{modules → frontend/src/modules}/meta.js +9 -7
  249. package/frontend/dist/{modules → frontend/src/modules}/paddle.d.ts +37 -134
  250. package/frontend/dist/{modules → frontend/src/modules}/paddle.js +620 -568
  251. package/frontend/dist/frontend/src/modules/request.d.ts +70 -0
  252. package/frontend/dist/frontend/src/modules/request.js +117 -0
  253. package/frontend/dist/frontend/src/modules/settings.d.ts +3 -0
  254. package/frontend/dist/frontend/src/modules/settings.js +5 -0
  255. package/frontend/dist/frontend/src/modules/statics.d.ts +21 -0
  256. package/frontend/dist/{modules → frontend/src/modules}/statics.js +15 -18
  257. package/frontend/dist/frontend/src/modules/support.d.ts +30 -0
  258. package/frontend/dist/frontend/src/modules/support.js +53 -0
  259. package/frontend/dist/{modules → frontend/src/modules}/theme.d.ts +67 -0
  260. package/frontend/dist/{modules → frontend/src/modules}/theme.js +68 -38
  261. package/frontend/dist/frontend/src/modules/themes.d.ts +12 -0
  262. package/frontend/dist/frontend/src/modules/themes.js +22 -0
  263. package/frontend/dist/frontend/src/modules/user.d.ts +164 -0
  264. package/frontend/dist/frontend/src/modules/user.js +268 -0
  265. package/frontend/dist/frontend/src/modules/utils.d.ts +176 -0
  266. package/frontend/dist/frontend/src/modules/utils.js +569 -0
  267. package/frontend/dist/frontend/src/types/gradient.d.ts +29 -0
  268. package/frontend/dist/{types → frontend/src/types}/gradient.js +14 -18
  269. package/frontend/dist/frontend/src/ui/border_button.d.ts +94 -0
  270. package/frontend/dist/{ui → frontend/src/ui}/border_button.js +7 -13
  271. package/frontend/dist/frontend/src/ui/button.d.ts +28 -0
  272. package/frontend/dist/{ui → frontend/src/ui}/button.js +21 -12
  273. package/frontend/dist/frontend/src/ui/canvas.d.ts +138 -0
  274. package/frontend/dist/{ui → frontend/src/ui}/canvas.js +88 -55
  275. package/frontend/dist/frontend/src/ui/checkbox.d.ts +74 -0
  276. package/frontend/dist/{ui → frontend/src/ui}/checkbox.js +80 -41
  277. package/frontend/dist/{ui → frontend/src/ui}/code.d.ts +73 -6
  278. package/frontend/dist/{ui → frontend/src/ui}/code.js +55 -52
  279. package/frontend/dist/{ui → frontend/src/ui}/context_menu.d.ts +4 -0
  280. package/frontend/dist/{ui → frontend/src/ui}/context_menu.js +12 -17
  281. package/frontend/dist/{ui → frontend/src/ui}/css.d.ts +4 -0
  282. package/frontend/dist/{ui → frontend/src/ui}/css.js +3 -3
  283. package/frontend/dist/{ui → frontend/src/ui}/divider.d.ts +4 -0
  284. package/frontend/dist/{ui → frontend/src/ui}/divider.js +3 -3
  285. package/frontend/dist/{ui → frontend/src/ui}/dropdown.d.ts +57 -2
  286. package/frontend/dist/{ui → frontend/src/ui}/dropdown.js +87 -94
  287. package/frontend/dist/{ui → frontend/src/ui}/for_each.d.ts +4 -0
  288. package/frontend/dist/{ui → frontend/src/ui}/for_each.js +3 -3
  289. package/frontend/dist/{ui → frontend/src/ui}/form.d.ts +6 -2
  290. package/frontend/dist/{ui → frontend/src/ui}/form.js +10 -7
  291. package/frontend/dist/frontend/src/ui/frame_modes.d.ts +37 -0
  292. package/frontend/dist/{ui → frontend/src/ui}/frame_modes.js +16 -22
  293. package/frontend/dist/{ui → frontend/src/ui}/google_map.d.ts +4 -0
  294. package/frontend/dist/{ui → frontend/src/ui}/google_map.js +4 -4
  295. package/frontend/dist/{ui → frontend/src/ui}/gradient.d.ts +4 -0
  296. package/frontend/dist/{ui → frontend/src/ui}/gradient.js +3 -3
  297. package/frontend/dist/{ui → frontend/src/ui}/image.d.ts +4 -0
  298. package/frontend/dist/{ui → frontend/src/ui}/image.js +5 -5
  299. package/frontend/dist/frontend/src/ui/input.d.ts +392 -0
  300. package/frontend/dist/{ui → frontend/src/ui}/input.js +346 -360
  301. package/frontend/dist/{ui → frontend/src/ui}/link.d.ts +4 -0
  302. package/frontend/dist/{ui → frontend/src/ui}/link.js +3 -3
  303. package/frontend/dist/{ui → frontend/src/ui}/list.d.ts +4 -0
  304. package/frontend/dist/{ui → frontend/src/ui}/list.js +12 -6
  305. package/frontend/dist/frontend/src/ui/loader_button.d.ts +80 -0
  306. package/frontend/dist/{ui → frontend/src/ui}/loader_button.js +35 -47
  307. package/frontend/dist/frontend/src/ui/loaders.d.ts +57 -0
  308. package/frontend/dist/{ui → frontend/src/ui}/loaders.js +11 -11
  309. package/frontend/dist/{ui → frontend/src/ui}/popup.d.ts +11 -6
  310. package/frontend/dist/{ui → frontend/src/ui}/popup.js +32 -18
  311. package/frontend/dist/frontend/src/ui/pseudo.d.ts +44 -0
  312. package/frontend/dist/{ui → frontend/src/ui}/pseudo.js +84 -8
  313. package/frontend/dist/{ui → frontend/src/ui}/scroller.d.ts +14 -2
  314. package/frontend/dist/{ui → frontend/src/ui}/scroller.js +37 -43
  315. package/frontend/dist/{ui → frontend/src/ui}/slider.d.ts +5 -1
  316. package/frontend/dist/{ui → frontend/src/ui}/slider.js +4 -4
  317. package/frontend/dist/{ui → frontend/src/ui}/spacer.d.ts +4 -0
  318. package/frontend/dist/{ui → frontend/src/ui}/spacer.js +3 -3
  319. package/frontend/dist/{ui → frontend/src/ui}/span.d.ts +4 -0
  320. package/frontend/dist/{ui → frontend/src/ui}/span.js +3 -3
  321. package/frontend/dist/{ui → frontend/src/ui}/stack.d.ts +4 -0
  322. package/frontend/dist/{ui → frontend/src/ui}/stack.js +3 -9
  323. package/frontend/dist/frontend/src/ui/steps.d.ts +131 -0
  324. package/frontend/dist/{ui → frontend/src/ui}/steps.js +30 -45
  325. package/frontend/dist/{ui → frontend/src/ui}/style.d.ts +4 -0
  326. package/frontend/dist/{ui → frontend/src/ui}/style.js +3 -3
  327. package/frontend/dist/{ui → frontend/src/ui}/switch.d.ts +5 -1
  328. package/frontend/dist/{ui → frontend/src/ui}/switch.js +4 -4
  329. package/frontend/dist/{ui → frontend/src/ui}/table.d.ts +4 -0
  330. package/frontend/dist/{ui → frontend/src/ui}/table.js +6 -6
  331. package/frontend/dist/{ui → frontend/src/ui}/tabs.d.ts +45 -3
  332. package/frontend/dist/{ui → frontend/src/ui}/tabs.js +65 -40
  333. package/frontend/dist/{ui → frontend/src/ui}/text.d.ts +4 -0
  334. package/frontend/dist/{ui → frontend/src/ui}/text.js +3 -3
  335. package/frontend/dist/frontend/src/ui/title.d.ts +91 -0
  336. package/frontend/dist/frontend/src/ui/title.js +272 -0
  337. package/frontend/dist/{ui → frontend/src/ui}/view.d.ts +4 -0
  338. package/frontend/dist/{ui → frontend/src/ui}/view.js +3 -3
  339. package/frontend/dist/{volt.d.ts → frontend/src/volt.d.ts} +3 -0
  340. package/frontend/dist/{volt.js → frontend/src/volt.js} +4 -0
  341. package/frontend/tools/bundle_d_ts.js +71 -0
  342. package/frontend/tools/convert_to_jsdoc_input.txt +9452 -0
  343. package/frontend/tools/convert_to_jsdoc_output.txt +7626 -0
  344. package/frontend/tools/convert_to_jsdoc_tmp.js +345 -0
  345. package/package.json +11 -12
  346. package/backend/dist/cjs/database/collection.d.ts +0 -160
  347. package/backend/dist/cjs/database/collection.js +0 -842
  348. package/backend/dist/cjs/database/database.d.ts +0 -121
  349. package/backend/dist/cjs/database/document.d.ts +0 -131
  350. package/backend/dist/cjs/database/document.js +0 -224
  351. package/backend/dist/cjs/database.d.ts +0 -502
  352. package/backend/dist/cjs/database.js +0 -2248
  353. package/backend/dist/cjs/logger.d.ts +0 -3
  354. package/backend/dist/cjs/meta.d.ts +0 -50
  355. package/backend/dist/cjs/mutex.d.ts +0 -24
  356. package/backend/dist/cjs/payments/paddle.d.ts +0 -160
  357. package/backend/dist/cjs/plugins/browser.d.ts +0 -36
  358. package/backend/dist/cjs/plugins/browser.js +0 -198
  359. package/backend/dist/cjs/plugins/css.d.ts +0 -11
  360. package/backend/dist/cjs/plugins/css.js +0 -80
  361. package/backend/dist/cjs/plugins/mail.d.ts +0 -277
  362. package/backend/dist/cjs/plugins/mail.js +0 -1370
  363. package/backend/dist/cjs/plugins/ts/compiler.d.ts +0 -139
  364. package/backend/dist/cjs/plugins/ts/compiler.js +0 -750
  365. package/backend/dist/cjs/plugins/ts/preprocessing.d.ts +0 -14
  366. package/backend/dist/cjs/plugins/ts/preprocessing.js +0 -440
  367. package/backend/dist/cjs/rate_limit.d.ts +0 -63
  368. package/backend/dist/cjs/rate_limit.js +0 -348
  369. package/backend/dist/cjs/request.deprc.d.ts +0 -48
  370. package/backend/dist/cjs/request.deprc.js +0 -572
  371. package/backend/dist/cjs/response.deprc.d.ts +0 -55
  372. package/backend/dist/cjs/response.deprc.js +0 -275
  373. package/backend/dist/cjs/server.d.ts +0 -342
  374. package/backend/dist/cjs/splash_screen.d.ts +0 -35
  375. package/backend/dist/cjs/status.d.ts +0 -61
  376. package/backend/dist/cjs/stream.d.ts +0 -79
  377. package/backend/dist/cjs/users.d.ts +0 -111
  378. package/backend/dist/cjs/users.js +0 -1817
  379. package/backend/dist/cjs/view.js +0 -352
  380. package/backend/dist/cjs/vinc.dev.d.ts +0 -3
  381. package/backend/dist/cjs/vinc.dev.js +0 -7
  382. package/backend/dist/css/adyen.css +0 -92
  383. package/backend/dist/css/volt.css +0 -70
  384. package/backend/dist/esm/database/collection.d.ts +0 -160
  385. package/backend/dist/esm/database/collection.js +0 -1328
  386. package/backend/dist/esm/database/database.d.ts +0 -121
  387. package/backend/dist/esm/database/document.d.ts +0 -131
  388. package/backend/dist/esm/database/document.js +0 -247
  389. package/backend/dist/esm/database.d.ts +0 -502
  390. package/backend/dist/esm/database.js +0 -2423
  391. package/backend/dist/esm/file_watcher.js +0 -329
  392. package/backend/dist/esm/logger.d.ts +0 -3
  393. package/backend/dist/esm/logger.js +0 -11
  394. package/backend/dist/esm/meta.d.ts +0 -50
  395. package/backend/dist/esm/mutex.d.ts +0 -24
  396. package/backend/dist/esm/mutex.js +0 -48
  397. package/backend/dist/esm/payments/paddle.d.ts +0 -160
  398. package/backend/dist/esm/plugins/browser.d.ts +0 -36
  399. package/backend/dist/esm/plugins/browser.js +0 -176
  400. package/backend/dist/esm/plugins/css.d.ts +0 -11
  401. package/backend/dist/esm/plugins/css.js +0 -90
  402. package/backend/dist/esm/plugins/ts/compiler.d.ts +0 -139
  403. package/backend/dist/esm/plugins/ts/compiler.js +0 -1194
  404. package/backend/dist/esm/plugins/ts/preprocessing.d.ts +0 -14
  405. package/backend/dist/esm/plugins/ts/preprocessing.js +0 -726
  406. package/backend/dist/esm/rate_limit.d.ts +0 -63
  407. package/backend/dist/esm/rate_limit.js +0 -417
  408. package/backend/dist/esm/request.deprc.d.ts +0 -48
  409. package/backend/dist/esm/request.deprc.js +0 -572
  410. package/backend/dist/esm/response.deprc.d.ts +0 -55
  411. package/backend/dist/esm/response.deprc.js +0 -275
  412. package/backend/dist/esm/server.d.ts +0 -342
  413. package/backend/dist/esm/splash_screen.d.ts +0 -35
  414. package/backend/dist/esm/status.d.ts +0 -61
  415. package/backend/dist/esm/status.js +0 -197
  416. package/backend/dist/esm/stream.d.ts +0 -79
  417. package/backend/dist/esm/users.d.ts +0 -111
  418. package/backend/dist/esm/users.js +0 -1935
  419. package/backend/dist/esm/vinc.dev.d.ts +0 -3
  420. package/backend/dist/esm/vinc.dev.js +0 -7
  421. package/frontend/dist/elements/base.d.ts +0 -9889
  422. package/frontend/dist/elements/module.d.ts +0 -30
  423. package/frontend/dist/modules/array.d.ts +0 -94
  424. package/frontend/dist/modules/array.js +0 -634
  425. package/frontend/dist/modules/auth.d.ts +0 -46
  426. package/frontend/dist/modules/auth.js +0 -139
  427. package/frontend/dist/modules/colors.d.ts +0 -1
  428. package/frontend/dist/modules/colors.js +0 -417
  429. package/frontend/dist/modules/compression.d.ts +0 -6
  430. package/frontend/dist/modules/compression.js +0 -999
  431. package/frontend/dist/modules/cookies.d.ts +0 -18
  432. package/frontend/dist/modules/cookies.js +0 -167
  433. package/frontend/dist/modules/date.d.ts +0 -142
  434. package/frontend/dist/modules/date.js +0 -493
  435. package/frontend/dist/modules/events.d.ts +0 -8
  436. package/frontend/dist/modules/events.js +0 -91
  437. package/frontend/dist/modules/google.d.ts +0 -11
  438. package/frontend/dist/modules/google.js +0 -54
  439. package/frontend/dist/modules/meta.d.ts +0 -10
  440. package/frontend/dist/modules/mutex.d.ts +0 -7
  441. package/frontend/dist/modules/mutex.js +0 -51
  442. package/frontend/dist/modules/number.d.ts +0 -16
  443. package/frontend/dist/modules/number.js +0 -23
  444. package/frontend/dist/modules/object.d.ts +0 -52
  445. package/frontend/dist/modules/object.js +0 -383
  446. package/frontend/dist/modules/scheme.d.ts +0 -227
  447. package/frontend/dist/modules/scheme.js +0 -531
  448. package/frontend/dist/modules/settings.d.ts +0 -3
  449. package/frontend/dist/modules/settings.js +0 -4
  450. package/frontend/dist/modules/statics.d.ts +0 -5
  451. package/frontend/dist/modules/string.d.ts +0 -124
  452. package/frontend/dist/modules/string.js +0 -745
  453. package/frontend/dist/modules/support.d.ts +0 -19
  454. package/frontend/dist/modules/support.js +0 -103
  455. package/frontend/dist/modules/themes.d.ts +0 -8
  456. package/frontend/dist/modules/themes.js +0 -18
  457. package/frontend/dist/modules/user.d.ts +0 -59
  458. package/frontend/dist/modules/user.js +0 -280
  459. package/frontend/dist/modules/utils.d.ts +0 -87
  460. package/frontend/dist/modules/utils.js +0 -923
  461. package/frontend/dist/types/gradient.d.ts +0 -12
  462. package/frontend/dist/ui/border_button.d.ts +0 -152
  463. package/frontend/dist/ui/button.d.ts +0 -21
  464. package/frontend/dist/ui/canvas.d.ts +0 -56
  465. package/frontend/dist/ui/checkbox.d.ts +0 -52
  466. package/frontend/dist/ui/frame_modes.d.ts +0 -25
  467. package/frontend/dist/ui/input.d.ts +0 -241
  468. package/frontend/dist/ui/loader_button.d.ts +0 -93
  469. package/frontend/dist/ui/loaders.d.ts +0 -57
  470. package/frontend/dist/ui/pseudo.d.ts +0 -16
  471. package/frontend/dist/ui/steps.d.ts +0 -59
  472. package/frontend/dist/ui/title.d.ts +0 -21
  473. package/frontend/dist/ui/title.js +0 -121
  474. package/frontend/examples/dashboard/dashboard.ts +0 -776
  475. /package/backend/dist/cjs/{cli.d.ts → backend/src/cli.d.ts} +0 -0
  476. /package/backend/dist/cjs/{file_watcher.d.ts → backend/src/database/document.d.ts} +0 -0
  477. /package/backend/dist/cjs/{file_watcher.js → backend/src/database/document.js} +0 -0
  478. /package/backend/dist/cjs/{plugins/pdf.d.ts → backend/src/database/filters/strict_filter_test.d.ts} +0 -0
  479. /package/backend/dist/{esm/file_watcher.d.ts → cjs/backend/src/database/filters/strict_filter_test_v0.d.ts} +0 -0
  480. /package/backend/dist/{esm/plugins/pdf.d.ts → cjs/backend/src/database/flatten_test.d.ts} +0 -0
  481. /package/backend/dist/cjs/{frontend.d.ts → backend/src/frontend.d.ts} +0 -0
  482. /package/backend/dist/cjs/{plugins → backend/src/plugins}/communication.d.ts +0 -0
  483. /package/backend/dist/cjs/{plugins → backend/src/plugins}/communication.js +0 -0
  484. /package/backend/dist/cjs/{plugins → backend/src/plugins}/mail/ui.js +0 -0
  485. /package/backend/dist/cjs/{plugins → backend/src/plugins}/pdf.js +0 -0
  486. /package/backend/dist/cjs/{plugins → backend/src/plugins}/thread_monitor.d.ts +0 -0
  487. /package/backend/dist/cjs/{plugins → backend/src/plugins}/thread_monitor.js +0 -0
  488. /package/backend/dist/cjs/{vinc.d.ts → backend/src/vinc.d.ts} +0 -0
  489. /package/backend/dist/cjs/{vinc.js → backend/src/vinc.js} +0 -0
  490. /package/backend/dist/esm/{cli.d.ts → backend/src/cli.d.ts} +0 -0
  491. /package/backend/dist/esm/{frontend.d.ts → backend/src/frontend.d.ts} +0 -0
  492. /package/backend/dist/esm/{plugins → backend/src/plugins}/communication.d.ts +0 -0
  493. /package/backend/dist/esm/{plugins → backend/src/plugins}/communication.js +0 -0
  494. /package/backend/dist/esm/{plugins → backend/src/plugins}/thread_monitor.d.ts +0 -0
  495. /package/backend/dist/esm/{plugins → backend/src/plugins}/thread_monitor.js +0 -0
  496. /package/backend/dist/esm/{vinc.d.ts → backend/src/vinc.d.ts} +0 -0
  497. /package/backend/dist/esm/{vinc.js → backend/src/vinc.js} +0 -0
  498. /package/frontend/dist/{elements → frontend/src/elements}/register_element.d.ts +0 -0
  499. /package/frontend/dist/{elements → frontend/src/elements}/register_element.js +0 -0
  500. /package/frontend/dist/{modules → frontend/src/modules}/color.d.ts +0 -0
  501. /package/frontend/dist/{ui → frontend/src/ui}/ui.d.ts +0 -0
  502. /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
+ // -------------------------------------------------------