@vandenberghinc/volt 1.2.5 → 1.2.6

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