@ambiten/core 1.0.0

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 (304) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +247 -0
  3. package/bin/bootstrap.js +26 -0
  4. package/dist/ambiten-cache/ambitenCache.d.ts +64 -0
  5. package/dist/ambiten-cache/ambitenCache.d.ts.map +1 -0
  6. package/dist/ambiten-cache/ambitenCache.js +127 -0
  7. package/dist/ambiten-cache/index.d.ts +2 -0
  8. package/dist/ambiten-cache/index.d.ts.map +1 -0
  9. package/dist/ambiten-cache/index.js +4 -0
  10. package/dist/browser.d.ts +32 -0
  11. package/dist/browser.d.ts.map +1 -0
  12. package/dist/browser.js +37 -0
  13. package/dist/cli/ambiten-core-cli.d.ts +4 -0
  14. package/dist/cli/ambiten-core-cli.d.ts.map +1 -0
  15. package/dist/cli/ambiten-core-cli.js +41 -0
  16. package/dist/cli/buildInteractiveConfig.d.ts +4 -0
  17. package/dist/cli/buildInteractiveConfig.d.ts.map +1 -0
  18. package/dist/cli/buildInteractiveConfig.js +198 -0
  19. package/dist/cli/index.d.ts +5 -0
  20. package/dist/cli/index.d.ts.map +1 -0
  21. package/dist/cli/index.js +7 -0
  22. package/dist/cli/prompt.d.ts +10 -0
  23. package/dist/cli/prompt.d.ts.map +1 -0
  24. package/dist/cli/prompt.js +48 -0
  25. package/dist/cli/types.d.ts +11 -0
  26. package/dist/cli/types.d.ts.map +1 -0
  27. package/dist/cli/types.js +2 -0
  28. package/dist/config/ambiten.config.schema.json +278 -0
  29. package/dist/config/index.d.ts +2 -0
  30. package/dist/config/index.d.ts.map +1 -0
  31. package/dist/config/index.js +4 -0
  32. package/dist/config/loadAmbitenConfig.d.ts +3 -0
  33. package/dist/config/loadAmbitenConfig.d.ts.map +1 -0
  34. package/dist/config/loadAmbitenConfig.js +131 -0
  35. package/dist/context/ambitenContext.d.ts +35 -0
  36. package/dist/context/ambitenContext.d.ts.map +1 -0
  37. package/dist/context/ambitenContext.js +147 -0
  38. package/dist/context/helpers/runManualTransaction.d.ts +3 -0
  39. package/dist/context/helpers/runManualTransaction.d.ts.map +1 -0
  40. package/dist/context/helpers/runManualTransaction.js +22 -0
  41. package/dist/context/helpers/transactionSession.d.ts +9 -0
  42. package/dist/context/helpers/transactionSession.d.ts.map +1 -0
  43. package/dist/context/helpers/transactionSession.js +8 -0
  44. package/dist/context/index.d.ts +4 -0
  45. package/dist/context/index.d.ts.map +1 -0
  46. package/dist/context/index.js +6 -0
  47. package/dist/debug/debugLog.d.ts +2 -0
  48. package/dist/debug/debugLog.d.ts.map +1 -0
  49. package/dist/debug/debugLog.js +23 -0
  50. package/dist/debug/index.d.ts +2 -0
  51. package/dist/debug/index.d.ts.map +1 -0
  52. package/dist/debug/index.js +4 -0
  53. package/dist/gc/ambitenGC.d.ts +49 -0
  54. package/dist/gc/ambitenGC.d.ts.map +1 -0
  55. package/dist/gc/ambitenGC.js +138 -0
  56. package/dist/gc/gcCron.browser.d.ts +2 -0
  57. package/dist/gc/gcCron.browser.d.ts.map +1 -0
  58. package/dist/gc/gcCron.browser.js +9 -0
  59. package/dist/gc/gcCron.node.d.ts +19 -0
  60. package/dist/gc/gcCron.node.d.ts.map +1 -0
  61. package/dist/gc/gcCron.node.js +47 -0
  62. package/dist/gc/gcManager.d.ts +37 -0
  63. package/dist/gc/gcManager.d.ts.map +1 -0
  64. package/dist/gc/gcManager.js +62 -0
  65. package/dist/gc/index.d.ts +5 -0
  66. package/dist/gc/index.d.ts.map +1 -0
  67. package/dist/gc/index.js +7 -0
  68. package/dist/graphql/ambitenGraphQL.d.ts +24 -0
  69. package/dist/graphql/ambitenGraphQL.d.ts.map +1 -0
  70. package/dist/graphql/ambitenGraphQL.js +215 -0
  71. package/dist/graphql/index.d.ts +2 -0
  72. package/dist/graphql/index.d.ts.map +1 -0
  73. package/dist/graphql/index.js +4 -0
  74. package/dist/index-browser.js +25 -0
  75. package/dist/index-browser.js.map +1 -0
  76. package/dist/index-cli.js +26 -0
  77. package/dist/index-cli.js.map +1 -0
  78. package/dist/index.d.ts +30 -0
  79. package/dist/index.d.ts.map +1 -0
  80. package/dist/index.js +25 -0
  81. package/dist/index.js.map +1 -0
  82. package/dist/init-cli/generate.project.d.ts +9 -0
  83. package/dist/init-cli/generate.project.d.ts.map +1 -0
  84. package/dist/init-cli/generate.project.js +38 -0
  85. package/dist/instrumentation/index.d.ts +3 -0
  86. package/dist/instrumentation/index.d.ts.map +1 -0
  87. package/dist/instrumentation/index.js +5 -0
  88. package/dist/instrumentation/measureQuery.browser.d.ts +3 -0
  89. package/dist/instrumentation/measureQuery.browser.d.ts.map +1 -0
  90. package/dist/instrumentation/measureQuery.browser.js +6 -0
  91. package/dist/instrumentation/measureQueryWithErrors.d.ts +11 -0
  92. package/dist/instrumentation/measureQueryWithErrors.d.ts.map +1 -0
  93. package/dist/instrumentation/measureQueryWithErrors.js +90 -0
  94. package/dist/lib-core/ambitenClient.d.ts +115 -0
  95. package/dist/lib-core/ambitenClient.d.ts.map +1 -0
  96. package/dist/lib-core/ambitenClient.js +537 -0
  97. package/dist/lib-core/ambitenModelFactory.d.ts +979 -0
  98. package/dist/lib-core/ambitenModelFactory.d.ts.map +1 -0
  99. package/dist/lib-core/ambitenModelFactory.js +3984 -0
  100. package/dist/lib-core/ambitenSchema.d.ts +126 -0
  101. package/dist/lib-core/ambitenSchema.d.ts.map +1 -0
  102. package/dist/lib-core/ambitenSchema.js +247 -0
  103. package/dist/lib-core/bootstrap/ambitenBootstrap.d.ts +38 -0
  104. package/dist/lib-core/bootstrap/ambitenBootstrap.d.ts.map +1 -0
  105. package/dist/lib-core/bootstrap/ambitenBootstrap.js +381 -0
  106. package/dist/lib-core/bootstrap/index.d.ts +2 -0
  107. package/dist/lib-core/bootstrap/index.d.ts.map +1 -0
  108. package/dist/lib-core/bootstrap/index.js +4 -0
  109. package/dist/lib-core/index.d.ts +5 -0
  110. package/dist/lib-core/index.d.ts.map +1 -0
  111. package/dist/lib-core/index.js +7 -0
  112. package/dist/middleware/authorization/auth.d.ts +22 -0
  113. package/dist/middleware/authorization/auth.d.ts.map +1 -0
  114. package/dist/middleware/authorization/auth.js +37 -0
  115. package/dist/middleware/index.d.ts +4 -0
  116. package/dist/middleware/index.d.ts.map +1 -0
  117. package/dist/middleware/index.js +6 -0
  118. package/dist/middleware/rbac/rbacMiddleware.d.ts +103 -0
  119. package/dist/middleware/rbac/rbacMiddleware.d.ts.map +1 -0
  120. package/dist/middleware/rbac/rbacMiddleware.js +246 -0
  121. package/dist/middleware/rbac/rbacTypes.d.ts +4 -0
  122. package/dist/middleware/rbac/rbacTypes.d.ts.map +1 -0
  123. package/dist/middleware/rbac/rbacTypes.js +8 -0
  124. package/dist/plugins/index.d.ts +3 -0
  125. package/dist/plugins/index.d.ts.map +1 -0
  126. package/dist/plugins/index.js +5 -0
  127. package/dist/plugins/softDelete/applySoftDelete.d.ts +5 -0
  128. package/dist/plugins/softDelete/applySoftDelete.d.ts.map +1 -0
  129. package/dist/plugins/softDelete/applySoftDelete.js +139 -0
  130. package/dist/plugins/softDelete/types.d.ts +14 -0
  131. package/dist/plugins/softDelete/types.d.ts.map +1 -0
  132. package/dist/plugins/softDelete/types.js +2 -0
  133. package/dist/redis-manager/index.d.ts +2 -0
  134. package/dist/redis-manager/index.d.ts.map +1 -0
  135. package/dist/redis-manager/index.js +4 -0
  136. package/dist/redis-manager/redisClient.d.ts +32 -0
  137. package/dist/redis-manager/redisClient.d.ts.map +1 -0
  138. package/dist/redis-manager/redisClient.js +146 -0
  139. package/dist/tanancy/MultiTenantManager.d.ts +98 -0
  140. package/dist/tanancy/MultiTenantManager.d.ts.map +1 -0
  141. package/dist/tanancy/MultiTenantManager.js +190 -0
  142. package/dist/tanancy/TenantContext.d.ts +31 -0
  143. package/dist/tanancy/TenantContext.d.ts.map +1 -0
  144. package/dist/tanancy/TenantContext.js +59 -0
  145. package/dist/tanancy/TenantModelResolver.d.ts +10 -0
  146. package/dist/tanancy/TenantModelResolver.d.ts.map +1 -0
  147. package/dist/tanancy/TenantModelResolver.js +114 -0
  148. package/dist/tanancy/index.d.ts +5 -0
  149. package/dist/tanancy/index.d.ts.map +1 -0
  150. package/dist/tanancy/index.js +7 -0
  151. package/dist/tanancy/init/initMultiTenancy.d.ts +29 -0
  152. package/dist/tanancy/init/initMultiTenancy.d.ts.map +1 -0
  153. package/dist/tanancy/init/initMultiTenancy.js +62 -0
  154. package/dist/templates/generateDefaultConfigContent.d.ts +10 -0
  155. package/dist/templates/generateDefaultConfigContent.d.ts.map +1 -0
  156. package/dist/templates/generateDefaultConfigContent.js +108 -0
  157. package/dist/templates/generateGCManagerFile.d.ts +2 -0
  158. package/dist/templates/generateGCManagerFile.d.ts.map +1 -0
  159. package/dist/templates/generateGCManagerFile.js +39 -0
  160. package/dist/templates/generateGCRunnerFile.d.ts +2 -0
  161. package/dist/templates/generateGCRunnerFile.d.ts.map +1 -0
  162. package/dist/templates/generateGCRunnerFile.js +25 -0
  163. package/dist/templates/generateMainEntry.d.ts +3 -0
  164. package/dist/templates/generateMainEntry.d.ts.map +1 -0
  165. package/dist/templates/generateMainEntry.js +24 -0
  166. package/dist/templates/index.d.ts +6 -0
  167. package/dist/templates/index.d.ts.map +1 -0
  168. package/dist/templates/index.js +8 -0
  169. package/dist/templates/scaffold/structureGenerator.d.ts +4 -0
  170. package/dist/templates/scaffold/structureGenerator.d.ts.map +1 -0
  171. package/dist/templates/scaffold/structureGenerator.js +193 -0
  172. package/dist/types/GCConfig.d.ts +7 -0
  173. package/dist/types/GCConfig.d.ts.map +1 -0
  174. package/dist/types/GCConfig.js +2 -0
  175. package/dist/types/ambiten-runtime-type.d.ts +24 -0
  176. package/dist/types/ambiten-runtime-type.d.ts.map +1 -0
  177. package/dist/types/ambiten-runtime-type.js +2 -0
  178. package/dist/types/ambiten.client.type.d.ts +136 -0
  179. package/dist/types/ambiten.client.type.d.ts.map +1 -0
  180. package/dist/types/ambiten.client.type.js +3 -0
  181. package/dist/types/ambiten.model.type.d.ts +135 -0
  182. package/dist/types/ambiten.model.type.d.ts.map +1 -0
  183. package/dist/types/ambiten.model.type.js +4 -0
  184. package/dist/types/ambitenConfig.d.ts +103 -0
  185. package/dist/types/ambitenConfig.d.ts.map +1 -0
  186. package/dist/types/ambitenConfig.js +3 -0
  187. package/dist/types/bootstrapClient.type.d.ts +11 -0
  188. package/dist/types/bootstrapClient.type.d.ts.map +1 -0
  189. package/dist/types/bootstrapClient.type.js +2 -0
  190. package/dist/types/db.provider.d.ts +66 -0
  191. package/dist/types/db.provider.d.ts.map +1 -0
  192. package/dist/types/db.provider.js +2 -0
  193. package/dist/types/document.d.ts +7 -0
  194. package/dist/types/document.d.ts.map +1 -0
  195. package/dist/types/document.js +2 -0
  196. package/dist/types/event.type.d.ts +6 -0
  197. package/dist/types/event.type.d.ts.map +1 -0
  198. package/dist/types/event.type.js +27 -0
  199. package/dist/types/graphql.type.d.ts +22 -0
  200. package/dist/types/graphql.type.d.ts.map +1 -0
  201. package/dist/types/graphql.type.js +2 -0
  202. package/dist/types/index.d.ts +15 -0
  203. package/dist/types/index.d.ts.map +1 -0
  204. package/dist/types/index.js +17 -0
  205. package/dist/types/logger.types.d.ts +15 -0
  206. package/dist/types/logger.types.d.ts.map +1 -0
  207. package/dist/types/logger.types.js +3 -0
  208. package/dist/types/middleware/types.d.ts +28 -0
  209. package/dist/types/middleware/types.d.ts.map +1 -0
  210. package/dist/types/middleware/types.js +2 -0
  211. package/dist/types/result.utils.types.d.ts +7 -0
  212. package/dist/types/result.utils.types.d.ts.map +1 -0
  213. package/dist/types/result.utils.types.js +2 -0
  214. package/dist/types/schema.type.d.ts +23 -0
  215. package/dist/types/schema.type.d.ts.map +1 -0
  216. package/dist/types/schema.type.js +10 -0
  217. package/dist/types/user.type.d.ts +11 -0
  218. package/dist/types/user.type.d.ts.map +1 -0
  219. package/dist/types/user.type.js +2 -0
  220. package/dist/utils/AuthUtils.d.ts +15 -0
  221. package/dist/utils/AuthUtils.d.ts.map +1 -0
  222. package/dist/utils/AuthUtils.js +26 -0
  223. package/dist/utils/ModelRegistry.d.ts +15 -0
  224. package/dist/utils/ModelRegistry.d.ts.map +1 -0
  225. package/dist/utils/ModelRegistry.js +35 -0
  226. package/dist/utils/banner.d.ts +2 -0
  227. package/dist/utils/banner.d.ts.map +1 -0
  228. package/dist/utils/banner.js +20 -0
  229. package/dist/utils/builders/createModel.d.ts +37 -0
  230. package/dist/utils/builders/createModel.d.ts.map +1 -0
  231. package/dist/utils/builders/createModel.js +38 -0
  232. package/dist/utils/builders/getTenantDb.d.ts +12 -0
  233. package/dist/utils/builders/getTenantDb.d.ts.map +1 -0
  234. package/dist/utils/builders/getTenantDb.js +25 -0
  235. package/dist/utils/builders/index.d.ts +4 -0
  236. package/dist/utils/builders/index.d.ts.map +1 -0
  237. package/dist/utils/builders/index.js +6 -0
  238. package/dist/utils/builders/schema.d.ts +22 -0
  239. package/dist/utils/builders/schema.d.ts.map +1 -0
  240. package/dist/utils/builders/schema.js +26 -0
  241. package/dist/utils/cacheWithRedis.d.ts +27 -0
  242. package/dist/utils/cacheWithRedis.d.ts.map +1 -0
  243. package/dist/utils/cacheWithRedis.js +22 -0
  244. package/dist/utils/castId.d.ts +5 -0
  245. package/dist/utils/castId.d.ts.map +1 -0
  246. package/dist/utils/castId.js +52 -0
  247. package/dist/utils/color-palatte.d.ts +4 -0
  248. package/dist/utils/color-palatte.d.ts.map +1 -0
  249. package/dist/utils/color-palatte.js +24 -0
  250. package/dist/utils/configureAmbitenContext.d.ts +13 -0
  251. package/dist/utils/configureAmbitenContext.d.ts.map +1 -0
  252. package/dist/utils/configureAmbitenContext.js +35 -0
  253. package/dist/utils/detect-package-manager.d.ts +5 -0
  254. package/dist/utils/detect-package-manager.d.ts.map +1 -0
  255. package/dist/utils/detect-package-manager.js +59 -0
  256. package/dist/utils/ensureModelNameSafe.d.ts +12 -0
  257. package/dist/utils/ensureModelNameSafe.d.ts.map +1 -0
  258. package/dist/utils/ensureModelNameSafe.js +28 -0
  259. package/dist/utils/ensureRedis.d.ts +7 -0
  260. package/dist/utils/ensureRedis.d.ts.map +1 -0
  261. package/dist/utils/ensureRedis.js +40 -0
  262. package/dist/utils/error/ambitenError-handler.d.ts +24 -0
  263. package/dist/utils/error/ambitenError-handler.d.ts.map +1 -0
  264. package/dist/utils/error/ambitenError-handler.js +39 -0
  265. package/dist/utils/error/errorTypes.d.ts +27 -0
  266. package/dist/utils/error/errorTypes.d.ts.map +1 -0
  267. package/dist/utils/error/errorTypes.js +30 -0
  268. package/dist/utils/error/index.d.ts +3 -0
  269. package/dist/utils/error/index.d.ts.map +1 -0
  270. package/dist/utils/error/index.js +5 -0
  271. package/dist/utils/eventOptions.d.ts +38 -0
  272. package/dist/utils/eventOptions.d.ts.map +1 -0
  273. package/dist/utils/eventOptions.js +130 -0
  274. package/dist/utils/events.d.ts +5 -0
  275. package/dist/utils/events.d.ts.map +1 -0
  276. package/dist/utils/events.js +7 -0
  277. package/dist/utils/index.d.ts +23 -0
  278. package/dist/utils/index.d.ts.map +1 -0
  279. package/dist/utils/index.js +25 -0
  280. package/dist/utils/invalidateTenantCache.d.ts +18 -0
  281. package/dist/utils/invalidateTenantCache.d.ts.map +1 -0
  282. package/dist/utils/invalidateTenantCache.js +27 -0
  283. package/dist/utils/logger.d.ts +3 -0
  284. package/dist/utils/logger.d.ts.map +1 -0
  285. package/dist/utils/logger.js +27 -0
  286. package/dist/utils/modelLoader.d.ts +3 -0
  287. package/dist/utils/modelLoader.d.ts.map +1 -0
  288. package/dist/utils/modelLoader.js +41 -0
  289. package/dist/utils/resolveAmbitenOption.d.ts +2 -0
  290. package/dist/utils/resolveAmbitenOption.d.ts.map +1 -0
  291. package/dist/utils/resolveAmbitenOption.js +10 -0
  292. package/dist/utils/resolveRuntimeConfig.d.ts +22 -0
  293. package/dist/utils/resolveRuntimeConfig.d.ts.map +1 -0
  294. package/dist/utils/resolveRuntimeConfig.js +33 -0
  295. package/dist/utils/scaffoldLoger.d.ts +11 -0
  296. package/dist/utils/scaffoldLoger.d.ts.map +1 -0
  297. package/dist/utils/scaffoldLoger.js +68 -0
  298. package/dist/utils/symbols.d.ts +4 -0
  299. package/dist/utils/symbols.d.ts.map +1 -0
  300. package/dist/utils/symbols.js +6 -0
  301. package/dist/utils/time.d.ts +5 -0
  302. package/dist/utils/time.d.ts.map +1 -0
  303. package/dist/utils/time.js +39 -0
  304. package/package.json +180 -0
@@ -0,0 +1,3984 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AmbitenModel = void 0;
4
+ const tslib_1 = require("tslib");
5
+ require("dotenv/config");
6
+ const mongodb_1 = require("mongodb");
7
+ const ambitenClient_1 = require("./ambitenClient");
8
+ const ambitenSchema_1 = require("./ambitenSchema");
9
+ const events_1 = tslib_1.__importDefault(require("events"));
10
+ const graphql_subscriptions_1 = require("graphql-subscriptions");
11
+ const tanancy_1 = require("../tanancy");
12
+ const redis_manager_1 = require("../redis-manager");
13
+ const utils_1 = require("../utils");
14
+ const context_1 = require("../context");
15
+ const instrumentation_1 = require("../instrumentation");
16
+ const debug_1 = require("../debug");
17
+ const pubsub = new graphql_subscriptions_1.PubSub();
18
+ /**
19
+ * A MongoDB-backed model with schema validation, middleware, multi-tenancy,
20
+ * transactions, caching, soft-delete support, and query instrumentation.
21
+ *
22
+ * @typeParam T - The MongoDB document shape handled by this model.
23
+ */
24
+ class AmbitenModel {
25
+ _provider;
26
+ _collectionName;
27
+ _collectionOverride;
28
+ _schema;
29
+ _initialized = false;
30
+ _defaultCtx;
31
+ _modelGCConfig;
32
+ _gcConfig;
33
+ _gcTimer;
34
+ eventEmitter = new events_1.default();
35
+ beforeMiddlewares = new Map();
36
+ afterMiddlewares = new Map();
37
+ _softDeleteConfig;
38
+ /**
39
+ * Creates a new model instance.
40
+ *
41
+ * @param options - Model configuration including collection, schema, provider,
42
+ * default context, and optional GC configuration.
43
+ */
44
+ constructor(options) {
45
+ if (!options) {
46
+ const message = 'AmbitenModel options are required.';
47
+ throw (0, utils_1.createAmbitenError)(utils_1.ErrorType.NULL_OR_UNDEFINED, message, { details: { providedOptions: options, typeError: utils_1.ErrorType.AmbitenModelError } });
48
+ }
49
+ if (!options.collectionName) {
50
+ const message = 'collectionName is required.';
51
+ throw (0, utils_1.createAmbitenError)(utils_1.ErrorType.NULL_OR_UNDEFINED, message, { details: { providedOptions: options, typeError: utils_1.ErrorType.AmbitenModelError } });
52
+ }
53
+ this._collectionName = options.collectionName;
54
+ this._schema =
55
+ options.schema ?? new ambitenSchema_1.AmbitenSchema({});
56
+ this._provider = options.provider ?? ambitenClient_1.AmbitenClient.init();
57
+ this._collectionOverride = options.collection;
58
+ this._modelGCConfig = options.gcConfig;
59
+ if (options.ctx?.db) {
60
+ const fixedDb = options.ctx.db;
61
+ this._provider = { db: async () => fixedDb };
62
+ }
63
+ if (options.ctx?.tenantId || options.ctx?.dbName || options.ctx?.collectionName) {
64
+ this._defaultCtx = {
65
+ tenantId: options.ctx?.tenantId,
66
+ dbName: options.ctx?.dbName,
67
+ collectionName: options.ctx?.collectionName,
68
+ db: options.ctx?.db,
69
+ session: options.ctx?.session,
70
+ config: options.ctx?.config,
71
+ };
72
+ }
73
+ const schemaGCConfig = this.resolveSchemaGCConfig();
74
+ if (this.isGCEnabled() || schemaGCConfig) {
75
+ utils_1.AmbitenModelRegistry.registerModel(this);
76
+ }
77
+ this.initMiddleware();
78
+ }
79
+ /**
80
+ * Merges an explicit operation context with the model default context and the
81
+ * current runtime context.
82
+ *
83
+ * @param ctx - Optional operation context.
84
+ * @returns A merged context or `undefined` when no context values are present.
85
+ */
86
+ mergeCtx(ctx) {
87
+ const runtimeCtx = context_1.AmbitenContext.get();
88
+ const merged = {
89
+ tenantId: ctx?.tenantId ?? this._defaultCtx?.tenantId ?? runtimeCtx.tenantId,
90
+ dbName: ctx?.dbName ?? this._defaultCtx?.dbName ?? runtimeCtx.dbName,
91
+ db: ctx?.db ?? this._defaultCtx?.db,
92
+ collectionName: ctx?.collectionName ??
93
+ this._defaultCtx?.collectionName ??
94
+ runtimeCtx.collectionName,
95
+ config: ctx?.config ?? this._defaultCtx?.config,
96
+ session: ctx?.session ?? this._defaultCtx?.session ?? runtimeCtx.session,
97
+ withDeleted: ctx?.withDeleted,
98
+ onlyDeleted: ctx?.onlyDeleted,
99
+ hardDelete: ctx?.hardDelete,
100
+ };
101
+ return Object.values(merged).some((value) => value !== undefined)
102
+ ? merged
103
+ : undefined;
104
+ }
105
+ /**
106
+ * Ensures a runtime Ambiten context exists before executing an instrumented action.
107
+ *
108
+ * This allows model methods to remain safe when invoked outside adapter-managed
109
+ * request lifecycles while still preserving tenant-aware instrumentation.
110
+ *
111
+ * @param ctx - Optional model context.
112
+ * @param action - The action to execute inside a runtime context.
113
+ * @returns The action result.
114
+ */
115
+ async runWithModelContext(ctx, action) {
116
+ if (context_1.AmbitenContext.hasActiveContext()) {
117
+ return action();
118
+ }
119
+ const merged = this.mergeCtx(ctx);
120
+ return context_1.AmbitenContext.run({
121
+ tenantId: merged?.tenantId,
122
+ dbName: merged?.dbName,
123
+ collectionName: merged?.collectionName ?? this._collectionName,
124
+ session: merged?.session,
125
+ }, action);
126
+ }
127
+ isGCEnabled() {
128
+ return this._modelGCConfig?.enableGC === true;
129
+ }
130
+ resolveSchemaGCConfig() {
131
+ return this._schema.getGCConfig?.();
132
+ }
133
+ /**
134
+ * Resolves the MongoDB collection for the current model and context.
135
+ *
136
+ * @param ctx - Optional model context.
137
+ * @returns The resolved MongoDB collection.
138
+ */
139
+ async getCollection(ctx) {
140
+ if (this._collectionOverride) {
141
+ return this._collectionOverride;
142
+ }
143
+ this.ensureConfigured();
144
+ const db = await this.resolveDb(ctx);
145
+ const collectionName = this.resolveCollectionName(ctx);
146
+ (0, debug_1.debugLog)('Resolved collection', {
147
+ collectionName,
148
+ dbName: db.databaseName,
149
+ tenantId: this.mergeCtx(ctx)?.tenantId,
150
+ });
151
+ return db.collection(collectionName);
152
+ }
153
+ /**
154
+ * Resolves a registered tenant configuration.
155
+ *
156
+ * @param tenantId - The tenant identifier.
157
+ * @returns The tenant configuration.
158
+ */
159
+ getResolvedTenant(tenantId) {
160
+ const tenant = tanancy_1.MultiTenantManager.getTenant(tenantId);
161
+ if (!tenant) {
162
+ throw new Error(`Tenant "${tenantId}" is not registered.`);
163
+ }
164
+ return { ...tenant };
165
+ }
166
+ /**
167
+ * Resolves the MongoDB database for the current operation.
168
+ *
169
+ * @param ctx - Optional model context.
170
+ * @returns The resolved MongoDB database instance.
171
+ */
172
+ async resolveDb(ctx) {
173
+ const resolvedCtx = this.mergeCtx(ctx);
174
+ if (resolvedCtx?.db) {
175
+ return resolvedCtx.db;
176
+ }
177
+ if (resolvedCtx?.tenantId) {
178
+ const tenant = this.getResolvedTenant(resolvedCtx.tenantId);
179
+ const client = await tanancy_1.MultiTenantManager.getClient(resolvedCtx.tenantId);
180
+ if (!client) {
181
+ throw new Error(`MongoClient for tenant "${resolvedCtx.tenantId}" is not available.`);
182
+ }
183
+ const dbName = resolvedCtx.dbName ?? tenant.dbName;
184
+ if (!dbName) {
185
+ throw new Error(`No database name configured for tenant "${resolvedCtx.tenantId}".`);
186
+ }
187
+ return client.db(dbName);
188
+ }
189
+ const db = await this._provider.db(resolvedCtx);
190
+ if (!db || typeof db.collection !== 'function') {
191
+ throw new Error(`AmbitenModel: provider.db() did not return a valid Db instance for collection "${this._collectionName}".`);
192
+ }
193
+ return db;
194
+ }
195
+ /**
196
+ * Resolves the collection name for the current operation.
197
+ *
198
+ * @param ctx - Optional model context.
199
+ * @returns The collection name.
200
+ */
201
+ resolveCollectionName(ctx) {
202
+ const resolvedCtx = this.mergeCtx(ctx);
203
+ const collectionName = resolvedCtx?.collectionName ?? this._collectionName;
204
+ if (!collectionName || typeof collectionName !== 'string' || !collectionName.trim()) {
205
+ throw new Error('AmbitenModel: collectionName is not configured.');
206
+ }
207
+ return collectionName.trim();
208
+ }
209
+ /**
210
+ * Resolves the client session for the current operation.
211
+ *
212
+ * @param ctx - Optional model context.
213
+ * @returns The MongoDB client session if available.
214
+ */
215
+ resolveSession(ctx) {
216
+ return this.mergeCtx(ctx)?.session;
217
+ }
218
+ /**
219
+ * Resolves the MongoDB client session to use for an operation.
220
+ *
221
+ * Precedence order:
222
+ * 1. Explicit externally supplied session
223
+ * 2. Per-call model context session
224
+ * 3. Active runtime session from AmbitenContext
225
+ *
226
+ * This ensures explicit caller intent always overrides implicit runtime state.
227
+ *
228
+ * @param ctx - Optional model execution context.
229
+ * @param externalSession - Optional explicit session supplied by the caller.
230
+ * @returns The resolved MongoDB client session, or `undefined` when no session is available.
231
+ */
232
+ resolveSessionStrict(ctx, externalSession) {
233
+ return externalSession ?? ctx?.session ?? context_1.AmbitenContext.getSession();
234
+ }
235
+ /**
236
+ * Subscribes to a model event.
237
+ *
238
+ * @param event - Event type.
239
+ * @param listener - Event listener callback.
240
+ */
241
+ on(event, listener) {
242
+ this.eventEmitter.on(event, listener);
243
+ }
244
+ /**
245
+ * Subscribes to a model event once.
246
+ *
247
+ * @param event - Event type.
248
+ * @param listener - Event listener callback.
249
+ */
250
+ once(event, listener) {
251
+ this.eventEmitter.once(event, listener);
252
+ }
253
+ /**
254
+ * Removes a subscribed event listener.
255
+ *
256
+ * @param event - Event type.
257
+ * @param listener - Event listener callback.
258
+ */
259
+ off(event, listener) {
260
+ this.eventEmitter.off(event, listener);
261
+ }
262
+ /**
263
+ * Removes a specific event listener.
264
+ *
265
+ * @param event - Event type.
266
+ * @param listener - Event listener callback.
267
+ */
268
+ removeListener(event, listener) {
269
+ this.eventEmitter.removeListener(event, listener);
270
+ }
271
+ /**
272
+ * Ensures the model has the minimum configuration required to operate.
273
+ */
274
+ ensureConfigured() {
275
+ if (!this._collectionName || typeof this._collectionName !== 'string') {
276
+ throw new Error('AmbitenModel: collectionName is not configured.');
277
+ }
278
+ if (!this._provider || typeof this._provider.db !== 'function') {
279
+ throw new Error('AmbitenModel: provider is not configured. Provide a valid DbProvider with a db(ctx) method.');
280
+ }
281
+ }
282
+ /**
283
+ * Initializes the model by validating configuration, resolving the collection,
284
+ * and creating the GC TTL index when enabled.
285
+ */
286
+ async init() {
287
+ if (this._initialized)
288
+ return;
289
+ try {
290
+ this.ensureConfigured();
291
+ if (!this._schema) {
292
+ this._schema = new ambitenSchema_1.AmbitenSchema({});
293
+ }
294
+ await this.getCollection();
295
+ if (this.isGCEnabled() && this._modelGCConfig) {
296
+ await this.ensureGCIndex(this._modelGCConfig);
297
+ }
298
+ this._initialized = true;
299
+ }
300
+ catch (error) {
301
+ throw (0, utils_1.createAmbitenError)(utils_1.ErrorType.INITIALIZATION_ERROR, `Failed to initialize AmbitenModel: ${error?.message ?? String(error)}`, {
302
+ details: {
303
+ operation: 'init',
304
+ TypeError: utils_1.ErrorType.INITIALIZATION_ERROR,
305
+ }
306
+ });
307
+ }
308
+ }
309
+ /**
310
+ * Returns a model instance bound to a default context.
311
+ *
312
+ * @param ctx - Context to bind to the cloned model.
313
+ * @returns A cloned model instance with merged default context.
314
+ */
315
+ bind(ctx) {
316
+ const clone = Object.create(this);
317
+ clone._defaultCtx = { ...this._defaultCtx, ...ctx };
318
+ return clone;
319
+ }
320
+ /**
321
+ * Registers or reconfigures the model.
322
+ *
323
+ * @param options - Model registration options.
324
+ */
325
+ async registerModel(options) {
326
+ const { ctx, collectionName, schema, collection } = options;
327
+ if (!collectionName) {
328
+ throw new Error('Collection name is required.');
329
+ }
330
+ this._collectionName = collectionName;
331
+ this._collectionOverride = collection;
332
+ this._schema = schema ?? new ambitenSchema_1.AmbitenSchema({});
333
+ if (ctx) {
334
+ this._defaultCtx = {
335
+ ...this._defaultCtx,
336
+ ...ctx,
337
+ };
338
+ }
339
+ this._initialized = false;
340
+ this.ensureConfigured();
341
+ await this.init();
342
+ }
343
+ /**
344
+ * Ensures the TTL index used by the garbage collector exists.
345
+ *
346
+ * @param gc - GC configuration.
347
+ */
348
+ async ensureGCIndex(gc) {
349
+ if (!gc.enableGC || !gc.ttl) {
350
+ return;
351
+ }
352
+ const col = await this.getCollection();
353
+ const field = gc.field ??
354
+ gc.updatedAtField ??
355
+ gc.createdAtField ??
356
+ 'updatedAt';
357
+ await col.createIndex({ [field]: 1 }, {
358
+ expireAfterSeconds: gc.ttl,
359
+ name: gc.indexName ?? `${this._collectionName}_${field}_ttl`,
360
+ background: true
361
+ });
362
+ }
363
+ /**
364
+ * Returns the schema attached to this model.
365
+ */
366
+ get schema() {
367
+ return this._schema;
368
+ }
369
+ /**
370
+ * Returns the schema attached to this model.
371
+ */
372
+ getSchema() {
373
+ return this.schema;
374
+ }
375
+ /**
376
+ * Validates a document against the model schema.
377
+ * Throws an error if validation fails.
378
+ * @param doc - Document to validate.
379
+ */
380
+ validate(doc) {
381
+ if (this._schema) {
382
+ this._schema.validate(doc);
383
+ }
384
+ }
385
+ /**
386
+ * Validates a document asynchronously against the model schema.
387
+ * Useful for schemas that perform async validation, such as checking uniqueness
388
+ * @param doc - Document to validate.
389
+ * @returns The validated document.
390
+ */
391
+ async validateAsync(doc) {
392
+ if (this._schema) {
393
+ await this._schema.validateAsync(doc);
394
+ }
395
+ return doc;
396
+ }
397
+ /**
398
+ * Initializes internal schema middleware for relationships, aggregation, and
399
+ * change notifications. This is called during model initialization and ensures that all registered
400
+ * middleware is set up before any operations are performed.
401
+ */
402
+ initMiddleware() {
403
+ if (!this._schema)
404
+ return;
405
+ this._schema.pre('create', async (ctx) => {
406
+ const doc = ctx.doc;
407
+ if (!doc)
408
+ return;
409
+ const relationships = this._schema.getRelationships() ?? [];
410
+ for (const { ref, localField } of relationships) {
411
+ const relatedCollection = (await this.getCollection()).db?.collection(ref);
412
+ const filter = { [localField]: doc._id };
413
+ await relatedCollection?.updateMany(filter, {
414
+ $set: { [localField]: doc._id },
415
+ });
416
+ }
417
+ });
418
+ this._schema.post('create', async (ctx) => {
419
+ const doc = (ctx.result ?? ctx.doc);
420
+ if (!doc)
421
+ return;
422
+ const relationships = this._schema.getRelationships() ?? [];
423
+ for (const { ref, localField } of relationships) {
424
+ const relatedCollection = (await this.getCollection()).db?.collection(ref);
425
+ const filter = { [localField]: doc._id };
426
+ await relatedCollection?.updateOne(filter, {
427
+ $set: { [localField]: doc._id },
428
+ });
429
+ }
430
+ await pubsub.publish('DB_CHANGE', {
431
+ dbChange: {
432
+ action: 'create',
433
+ doc,
434
+ },
435
+ });
436
+ });
437
+ this._schema.post('updateOne', async (ctx) => {
438
+ const filter = ctx.filter;
439
+ const update = ctx.update;
440
+ if (!filter || !update)
441
+ return;
442
+ const relationships = this._schema.getRelationships() ?? [];
443
+ for (const { ref, localField } of relationships) {
444
+ const relatedCollection = (await this.getCollection()).db?.collection(ref);
445
+ const updateFilter = { [localField]: update.$set?._id };
446
+ await relatedCollection?.updateOne(updateFilter, {
447
+ $set: { [localField]: update.$set?._id },
448
+ });
449
+ }
450
+ await pubsub.publish('DB_CHANGE', {
451
+ dbChange: {
452
+ action: 'updateOne',
453
+ filter,
454
+ update,
455
+ },
456
+ });
457
+ });
458
+ this._schema.pre('deleteOne', async (ctx) => {
459
+ const doc = (ctx.doc ?? ctx.result);
460
+ if (!doc)
461
+ return;
462
+ const relationships = this._schema.getRelationships() ?? [];
463
+ for (const { ref, localField } of relationships) {
464
+ const relatedCollection = (await this.getCollection()).db?.collection(ref);
465
+ const filter = { [localField]: doc._id };
466
+ await relatedCollection?.deleteOne(filter);
467
+ }
468
+ });
469
+ this._schema.pre('aggregate', async (ctx) => {
470
+ const pipeline = ctx.pipeline;
471
+ if (!pipeline)
472
+ return;
473
+ const relationships = this._schema.getRelationships() ?? [];
474
+ for (const { ref, localField } of relationships) {
475
+ pipeline.unshift({
476
+ $lookup: {
477
+ from: ref,
478
+ localField: localField,
479
+ foreignField: '_id',
480
+ as: ref,
481
+ },
482
+ });
483
+ }
484
+ });
485
+ this._schema.post('aggregate', async (ctx) => {
486
+ const result = ctx.result;
487
+ if (!result)
488
+ return;
489
+ const relationships = this._schema.getRelationships() ?? [];
490
+ for (const doc of result) {
491
+ for (const { ref } of relationships) {
492
+ delete doc[ref];
493
+ }
494
+ }
495
+ await pubsub.publish('DB_CHANGE', {
496
+ dbChange: {
497
+ action: 'aggregate',
498
+ result,
499
+ },
500
+ });
501
+ });
502
+ }
503
+ /**
504
+ * Creates a new document in the model collection.
505
+ *
506
+ * This operation runs inside the active Ambiten runtime context, is instrumented
507
+ * through {@link measureQuery}, executes registered create middlewares,
508
+ * validates the incoming document against the model schema, triggers schema
509
+ * create hooks, invalidates relevant cache patterns, and publishes a database
510
+ * change event after successful insertion.
511
+ *
512
+ * @param doc - The document to insert.
513
+ * @param ctx - Optional model execution context for tenant, database,
514
+ * collection, session, and related runtime overrides.
515
+ * @returns The created document as a normalized model result.
516
+ * @throws {Error} When the provided document is missing or invalid.
517
+ */
518
+ async create(doc, ctx) {
519
+ await this.init();
520
+ if (!doc || typeof doc !== 'object') {
521
+ throw new Error('Document must be a valid object.');
522
+ }
523
+ return this.runWithModelContext(ctx, async () => (0, instrumentation_1.measureQuery)({
524
+ operation: 'create',
525
+ collectionName: this.resolveCollectionName(ctx),
526
+ documentCount: 1
527
+ }, async () => {
528
+ const collectionName = this.resolveCollectionName(ctx);
529
+ const session = this.resolveSession(ctx);
530
+ const col = await this.getCollection(ctx);
531
+ const middlewareCtx = this.buildMiddlewareContext('create', ctx, {
532
+ doc: doc
533
+ });
534
+ middlewareCtx.collectionName =
535
+ middlewareCtx.collectionName ?? collectionName;
536
+ await this.runBeforeMiddlewares('create', middlewareCtx);
537
+ const nextDoc = (middlewareCtx.doc ?? doc);
538
+ middlewareCtx.doc = nextDoc;
539
+ this.validate(nextDoc);
540
+ await this.schema.executePre('create', this.buildHookPayload('create', ctx, {
541
+ collectionName: middlewareCtx.collectionName,
542
+ tenantId: middlewareCtx.tenantId,
543
+ dbName: middlewareCtx.dbName,
544
+ session,
545
+ doc: nextDoc,
546
+ meta: middlewareCtx.meta
547
+ }));
548
+ const insertResult = await col.insertOne(nextDoc, session ? { session } : undefined);
549
+ const createdDoc = {
550
+ ...nextDoc,
551
+ _id: insertResult.insertedId
552
+ };
553
+ await this.schema.executePost('create', this.buildHookPayload('create', ctx, {
554
+ collectionName: middlewareCtx.collectionName,
555
+ tenantId: middlewareCtx.tenantId,
556
+ dbName: middlewareCtx.dbName,
557
+ session,
558
+ doc: createdDoc,
559
+ result: createdDoc,
560
+ meta: middlewareCtx.meta
561
+ }));
562
+ const payload = this.toModelResult(createdDoc);
563
+ middlewareCtx.result = payload;
564
+ await this.runAfterMiddlewares('create', middlewareCtx);
565
+ const finalResult = (middlewareCtx.result ?? payload);
566
+ await this.invalidateCachePatterns('create', middlewareCtx);
567
+ await pubsub.publish(`${utils_1.DB_CHANGE_EVENT}`, {
568
+ documentInserted: {
569
+ action: 'create',
570
+ collectionName: middlewareCtx.collectionName,
571
+ doc: finalResult,
572
+ meta: middlewareCtx.meta
573
+ }
574
+ });
575
+ return finalResult;
576
+ }));
577
+ }
578
+ /**
579
+ * Finds documents matching the provided filter.
580
+ *
581
+ * This operation runs inside the active Ambiten runtime context, is instrumented
582
+ * through {@link measureQuery}, executes registered find middlewares, supports
583
+ * optional result caching, triggers schema find hooks, and returns normalized
584
+ * model results.
585
+ *
586
+ * @param filter - MongoDB filter used to match documents.
587
+ * @param ctx - Optional model execution context for tenant, database,
588
+ * collection, session, and related runtime overrides.
589
+ * @param options - Optional query-level features such as caching.
590
+ * @returns An array of normalized model results.
591
+ * @throws {Error} When the filter is invalid.
592
+ */
593
+ async find(filter = {}, ctx, options) {
594
+ await this.init();
595
+ if (!filter || typeof filter !== 'object') {
596
+ throw new Error('Filter must be a valid object.');
597
+ }
598
+ return this.runWithModelContext(ctx, async () => (0, instrumentation_1.measureQuery)({
599
+ operation: 'find',
600
+ collectionName: this.resolveCollectionName(ctx),
601
+ filter
602
+ }, async () => {
603
+ const collectionName = this.resolveCollectionName(ctx);
604
+ const session = this.resolveSession(ctx);
605
+ const col = await this.getCollection(ctx);
606
+ const middlewareCtx = this.buildMiddlewareContext('find', ctx, { filter });
607
+ middlewareCtx.collectionName =
608
+ middlewareCtx.collectionName ?? collectionName;
609
+ await this.runBeforeMiddlewares('find', middlewareCtx);
610
+ const effectiveFilter = (middlewareCtx.filter ?? filter);
611
+ middlewareCtx.filter = effectiveFilter;
612
+ const cache = options?.cache ? this.resolveCache(options) : null;
613
+ await this.schema.executePre('find', this.buildHookPayload('create', ctx, {
614
+ collectionName: middlewareCtx.collectionName,
615
+ tenantId: middlewareCtx.tenantId,
616
+ dbName: middlewareCtx.dbName,
617
+ session,
618
+ filter: effectiveFilter,
619
+ meta: middlewareCtx.meta
620
+ }));
621
+ let cacheKey = null;
622
+ if (cache) {
623
+ cacheKey = this.buildCacheKey('find', {
624
+ collectionName: middlewareCtx.collectionName,
625
+ filter: effectiveFilter,
626
+ tenantId: middlewareCtx.tenantId,
627
+ dbName: middlewareCtx.dbName
628
+ });
629
+ const cached = await cache.get(cacheKey, {
630
+ ttlSeconds: typeof options?.cache === 'object' ? options.cache.ttl : undefined,
631
+ tenantId: middlewareCtx.tenantId,
632
+ namespace: middlewareCtx.collectionName
633
+ });
634
+ if (cached) {
635
+ middlewareCtx.result = cached;
636
+ await this.schema.executePost('find', this.buildHookPayload('find', ctx, {
637
+ collectionName: middlewareCtx.collectionName,
638
+ tenantId: middlewareCtx.tenantId,
639
+ dbName: middlewareCtx.dbName,
640
+ session,
641
+ filter: effectiveFilter,
642
+ result: cached,
643
+ meta: {
644
+ ...(middlewareCtx.meta ?? {}),
645
+ cacheHit: true
646
+ }
647
+ }));
648
+ await this.runAfterMiddlewares('find', middlewareCtx);
649
+ return (middlewareCtx.result ?? cached);
650
+ }
651
+ }
652
+ const cursor = col.find(effectiveFilter, session ? { session } : undefined);
653
+ const results = await cursor.toArray();
654
+ const payload = this.toModelResults(results);
655
+ await this.schema.executePost('find', this.buildHookPayload('find', ctx, {
656
+ collectionName: middlewareCtx.collectionName,
657
+ tenantId: middlewareCtx.tenantId,
658
+ dbName: middlewareCtx.dbName,
659
+ session,
660
+ filter: effectiveFilter,
661
+ result: payload,
662
+ meta: middlewareCtx.meta
663
+ }));
664
+ middlewareCtx.result = payload;
665
+ await this.runAfterMiddlewares('find', middlewareCtx);
666
+ const finalResult = (middlewareCtx.result ?? payload);
667
+ if (cache && cacheKey) {
668
+ await cache.set(cacheKey, finalResult, {
669
+ ttlSeconds: typeof options?.cache === 'object' ? options.cache.ttl : undefined,
670
+ tenantId: middlewareCtx.tenantId,
671
+ namespace: middlewareCtx.collectionName
672
+ });
673
+ }
674
+ return finalResult;
675
+ }));
676
+ }
677
+ /**
678
+ * Finds a single document matching the provided filter.
679
+ *
680
+ * This operation runs inside the active Ambiten runtime context, is instrumented
681
+ * through {@link measureQuery}, executes registered findOne middlewares,
682
+ * supports optional result caching, triggers schema find hooks, and returns a
683
+ * normalized model result when a document is found.
684
+ *
685
+ * @param filter - MongoDB filter used to match the document.
686
+ * @param ctx - Optional model execution context for tenant, database,
687
+ * collection, session, and related runtime overrides.
688
+ * @param options - Optional query-level features such as caching.
689
+ * @returns The matched document as a normalized model result, or `null` if no document matched.
690
+ * @throws {Error} When the filter is missing or invalid.
691
+ */
692
+ async findOne(filter, ctx, options) {
693
+ await this.init();
694
+ if (!filter || typeof filter !== 'object') {
695
+ throw new Error('Filter must be a valid object.');
696
+ }
697
+ return this.runWithModelContext(ctx, async () => (0, instrumentation_1.measureQuery)({
698
+ operation: 'findOne',
699
+ collectionName: this.resolveCollectionName(ctx),
700
+ filter
701
+ }, async () => {
702
+ const collectionName = this.resolveCollectionName(ctx);
703
+ const session = this.resolveSession(ctx);
704
+ const col = await this.getCollection(ctx);
705
+ const middlewareCtx = this.buildMiddlewareContext('findOne', ctx, {
706
+ filter
707
+ });
708
+ middlewareCtx.collectionName =
709
+ middlewareCtx.collectionName ?? collectionName;
710
+ await this.runBeforeMiddlewares('findOne', middlewareCtx);
711
+ const effectiveFilter = (middlewareCtx.filter ?? filter);
712
+ middlewareCtx.filter = effectiveFilter;
713
+ const cache = options?.cache ? this.resolveCache(options) : null;
714
+ let cacheKey = null;
715
+ await this.schema.executePre('findOne', this.buildHookPayload('findOne', ctx, {
716
+ collectionName: middlewareCtx.collectionName,
717
+ tenantId: middlewareCtx.tenantId,
718
+ dbName: middlewareCtx.dbName,
719
+ session,
720
+ filter: effectiveFilter,
721
+ meta: middlewareCtx.meta
722
+ }));
723
+ if (cache) {
724
+ cacheKey = this.buildCacheKey('findOne', {
725
+ collectionName: middlewareCtx.collectionName,
726
+ filter: effectiveFilter,
727
+ tenantId: middlewareCtx.tenantId,
728
+ dbName: middlewareCtx.dbName
729
+ });
730
+ const cached = await cache.get(cacheKey, {
731
+ ttlSeconds: typeof options?.cache === 'object' ? options.cache.ttl : undefined,
732
+ tenantId: middlewareCtx.tenantId,
733
+ namespace: middlewareCtx.collectionName
734
+ });
735
+ if (cached !== null && cached !== undefined) {
736
+ middlewareCtx.result = cached;
737
+ await this.schema.executePost('findOne', this.buildHookPayload('findOne', ctx, {
738
+ collectionName: middlewareCtx.collectionName,
739
+ tenantId: middlewareCtx.tenantId,
740
+ dbName: middlewareCtx.dbName,
741
+ session,
742
+ filter: effectiveFilter,
743
+ result: cached,
744
+ meta: {
745
+ ...(middlewareCtx.meta ?? {}),
746
+ cacheHit: true
747
+ }
748
+ }));
749
+ await this.runAfterMiddlewares('findOne', middlewareCtx);
750
+ return (middlewareCtx.result ?? cached);
751
+ }
752
+ }
753
+ const result = await col.findOne(effectiveFilter, session ? { session } : undefined);
754
+ const payload = result
755
+ ? this.toModelResult(result)
756
+ : null;
757
+ await this.schema.executePost('findOne', {
758
+ operation: 'findOne',
759
+ collectionName: middlewareCtx.collectionName,
760
+ tenantId: middlewareCtx.tenantId,
761
+ dbName: middlewareCtx.dbName,
762
+ session,
763
+ filter: effectiveFilter,
764
+ result: payload,
765
+ meta: middlewareCtx.meta
766
+ });
767
+ middlewareCtx.result = payload;
768
+ await this.runAfterMiddlewares('findOne', middlewareCtx);
769
+ const finalResult = (middlewareCtx.result ?? payload);
770
+ if (cache && cacheKey && finalResult !== null) {
771
+ await cache.set(cacheKey, finalResult, {
772
+ ttlSeconds: typeof options?.cache === 'object' ? options.cache.ttl : undefined,
773
+ tenantId: middlewareCtx.tenantId,
774
+ namespace: middlewareCtx.collectionName
775
+ });
776
+ }
777
+ return finalResult;
778
+ }));
779
+ }
780
+ /**
781
+ * Updates a single document matching the provided filter.
782
+ *
783
+ * This operation runs inside the active Ambiten runtime context, is instrumented
784
+ * through {@link measureQuery}, executes registered update middlewares,
785
+ * triggers schema update hooks, invalidates relevant cache patterns, and
786
+ * publishes a database change event after completion.
787
+ *
788
+ * @param filter - MongoDB filter used to identify the document to update.
789
+ * @param update - MongoDB update document to apply.
790
+ * @param ctx - Optional model execution context for tenant, database,
791
+ * collection, session, and related runtime overrides.
792
+ * @returns A promise that resolves when the operation completes.
793
+ * @throws {Error} When the filter is missing or invalid.
794
+ * @throws {Error} When the update document is missing or invalid.
795
+ */
796
+ async updateOne(filter, update, ctx) {
797
+ await this.init();
798
+ if (!filter || typeof filter !== 'object') {
799
+ throw new Error('Filter must be a valid object.');
800
+ }
801
+ if (!update || typeof update !== 'object') {
802
+ throw new Error('Update must be a valid object.');
803
+ }
804
+ return this.runWithModelContext(ctx, async () => (0, instrumentation_1.measureQuery)({
805
+ operation: 'updateOne',
806
+ collectionName: this.resolveCollectionName(ctx),
807
+ filter,
808
+ update
809
+ }, async () => {
810
+ const collectionName = this.resolveCollectionName(ctx);
811
+ const session = this.resolveSession(ctx);
812
+ const col = await this.getCollection(ctx);
813
+ const middlewareCtx = this.buildMiddlewareContext('updateOne', ctx, {
814
+ filter,
815
+ update
816
+ });
817
+ middlewareCtx.collectionName =
818
+ middlewareCtx.collectionName ?? collectionName;
819
+ await this.runBeforeMiddlewares('updateOne', middlewareCtx);
820
+ const effectiveFilter = (middlewareCtx.filter ?? filter);
821
+ const effectiveUpdate = (middlewareCtx.update ?? update);
822
+ middlewareCtx.filter = effectiveFilter;
823
+ middlewareCtx.update = effectiveUpdate;
824
+ await this.schema.executePre('updateOne', {
825
+ operation: 'updateOne',
826
+ collectionName: middlewareCtx.collectionName,
827
+ tenantId: middlewareCtx.tenantId,
828
+ dbName: middlewareCtx.dbName,
829
+ session,
830
+ filter: effectiveFilter,
831
+ update: effectiveUpdate,
832
+ meta: middlewareCtx.meta
833
+ });
834
+ const result = await col.updateOne(effectiveFilter, effectiveUpdate, session ? { session } : undefined);
835
+ const updateResult = {
836
+ acknowledged: result?.acknowledged,
837
+ matchedCount: result?.matchedCount,
838
+ modifiedCount: result?.modifiedCount,
839
+ upsertedCount: result?.upsertedCount,
840
+ upsertedId: result?.upsertedId
841
+ };
842
+ await this.schema.executePost('updateOne', {
843
+ operation: 'updateOne',
844
+ collectionName: middlewareCtx.collectionName,
845
+ tenantId: middlewareCtx.tenantId,
846
+ dbName: middlewareCtx.dbName,
847
+ session,
848
+ filter: effectiveFilter,
849
+ update: effectiveUpdate,
850
+ result: updateResult,
851
+ meta: middlewareCtx.meta
852
+ });
853
+ middlewareCtx.result = updateResult;
854
+ await this.runAfterMiddlewares('updateOne', middlewareCtx);
855
+ await this.invalidateCachePatterns('updateOne', middlewareCtx);
856
+ await pubsub.publish(`${utils_1.DB_CHANGE_EVENT}`, {
857
+ documentUpdated: {
858
+ action: 'updateOne',
859
+ collectionName: middlewareCtx.collectionName,
860
+ filter: effectiveFilter,
861
+ update: effectiveUpdate,
862
+ result: updateResult,
863
+ meta: middlewareCtx.meta
864
+ }
865
+ });
866
+ }));
867
+ }
868
+ /**
869
+ * Inserts multiple documents into the model collection.
870
+ *
871
+ * This operation runs inside the active Ambiten runtime context, is instrumented
872
+ * through {@link measureQuery}, executes registered bulkInsert middlewares,
873
+ * validates each incoming document against the model schema, triggers schema
874
+ * bulk insert hooks, invalidates relevant cache patterns, and publishes a
875
+ * database change event after successful insertion.
876
+ *
877
+ * @param docs - The documents to insert.
878
+ * @param ctx - Optional model execution context for tenant, database,
879
+ * collection, session, and related runtime overrides.
880
+ * @returns A promise that resolves when the operation completes.
881
+ */
882
+ async bulkInsert(docs, ctx) {
883
+ await this.init();
884
+ if (!Array.isArray(docs) || docs.length === 0) {
885
+ return;
886
+ }
887
+ return this.runWithModelContext(ctx, async () => (0, instrumentation_1.measureQuery)({
888
+ operation: 'bulkInsert',
889
+ collectionName: this.resolveCollectionName(ctx),
890
+ documentCount: docs.length
891
+ }, async () => {
892
+ const collectionName = this.resolveCollectionName(ctx);
893
+ const session = this.resolveSession(ctx);
894
+ const col = await this.getCollection(ctx);
895
+ const middlewareCtx = this.buildMiddlewareContext('bulkInsert', ctx, {
896
+ docs: docs
897
+ });
898
+ middlewareCtx.collectionName =
899
+ middlewareCtx.collectionName ?? collectionName;
900
+ await this.runBeforeMiddlewares('bulkInsert', middlewareCtx);
901
+ const nextDocs = (middlewareCtx.docs ?? docs);
902
+ middlewareCtx.docs = nextDocs;
903
+ if (!Array.isArray(nextDocs) || nextDocs.length === 0) {
904
+ middlewareCtx.result = [];
905
+ await this.runAfterMiddlewares('bulkInsert', middlewareCtx);
906
+ return;
907
+ }
908
+ for (const nextDoc of nextDocs) {
909
+ if (!nextDoc || typeof nextDoc !== 'object') {
910
+ throw new Error('Each document must be a valid object.');
911
+ }
912
+ this.validate(nextDoc);
913
+ }
914
+ const meta = this.buildOperationMeta(middlewareCtx.meta, {
915
+ bulkWrite: true
916
+ });
917
+ await this.schema.executePre('bulkInsert', this.buildHookPayload('bulkInsert', ctx, {
918
+ collectionName: middlewareCtx.collectionName,
919
+ tenantId: middlewareCtx.tenantId,
920
+ dbName: middlewareCtx.dbName,
921
+ session,
922
+ docs: nextDocs,
923
+ meta
924
+ }));
925
+ const result = await col.insertMany(nextDocs, {
926
+ ordered: false,
927
+ ...(session ? { session } : {})
928
+ });
929
+ const insertedDocs = nextDocs.map((doc, index) => ({
930
+ ...doc,
931
+ _id: result.insertedIds[index]
932
+ }));
933
+ await this.schema.executePost('bulkInsert', this.buildHookPayload('bulkInsert', ctx, {
934
+ collectionName: middlewareCtx.collectionName,
935
+ tenantId: middlewareCtx.tenantId,
936
+ dbName: middlewareCtx.dbName,
937
+ session,
938
+ docs: insertedDocs,
939
+ result: insertedDocs,
940
+ meta
941
+ }));
942
+ middlewareCtx.result = this.toModelResults(insertedDocs);
943
+ await this.runAfterMiddlewares('bulkInsert', middlewareCtx);
944
+ await this.invalidateCachePatterns('bulkInsert', middlewareCtx);
945
+ await pubsub.publish(`${utils_1.DB_CHANGE_EVENT}`, {
946
+ documentInserted: {
947
+ action: 'bulkInsert',
948
+ collectionName: middlewareCtx.collectionName,
949
+ docs: middlewareCtx.result,
950
+ meta
951
+ }
952
+ });
953
+ }));
954
+ }
955
+ /**
956
+ * Updates multiple documents using MongoDB bulk write semantics.
957
+ *
958
+ * This operation runs inside the active Ambiten runtime context, is instrumented
959
+ * through {@link measureQuery}, executes registered bulkUpdate middlewares,
960
+ * triggers schema bulk update hooks, invalidates relevant cache patterns, and
961
+ * publishes a database change event after completion.
962
+ *
963
+ * Each update entry is translated into an `updateOne` bulk operation using
964
+ * `$set` with the provided partial update document.
965
+ *
966
+ * @param updates - Array of filter/update pairs to execute as bulk updates.
967
+ * @param ctx - Optional model execution context for tenant, database,
968
+ * collection, session, and related runtime overrides.
969
+ * @returns A promise that resolves when the operation completes.
970
+ * @throws {Error} When the updates payload is invalid.
971
+ */
972
+ async bulkUpdate(updates, ctx) {
973
+ await this.init();
974
+ if (!Array.isArray(updates)) {
975
+ throw new Error('Updates must be a valid array.');
976
+ }
977
+ if (updates.length === 0) {
978
+ return;
979
+ }
980
+ return this.runWithModelContext(ctx, async () => (0, instrumentation_1.measureQuery)({
981
+ operation: 'bulkUpdate',
982
+ collectionName: this.resolveCollectionName(ctx),
983
+ documentCount: updates.length
984
+ }, async () => {
985
+ const collectionName = this.resolveCollectionName(ctx);
986
+ const session = this.resolveSession(ctx);
987
+ const col = await this.getCollection(ctx);
988
+ const middlewareCtx = this.buildMiddlewareContext('bulkUpdate', ctx, {
989
+ bulkUpdates: updates
990
+ });
991
+ middlewareCtx.collectionName =
992
+ middlewareCtx.collectionName ?? collectionName;
993
+ await this.runBeforeMiddlewares('bulkUpdate', middlewareCtx);
994
+ const nextUpdates = middlewareCtx.bulkUpdates ?? updates;
995
+ middlewareCtx.bulkUpdates = nextUpdates;
996
+ const meta = this.buildOperationMeta(middlewareCtx.meta, {
997
+ bulkWrite: true
998
+ });
999
+ middlewareCtx.meta = meta;
1000
+ if (!Array.isArray(nextUpdates) || nextUpdates.length === 0) {
1001
+ middlewareCtx.result = {
1002
+ acknowledged: true,
1003
+ matchedCount: 0,
1004
+ modifiedCount: 0,
1005
+ upsertedCount: 0,
1006
+ insertedCount: 0,
1007
+ deletedCount: 0
1008
+ };
1009
+ await this.runAfterMiddlewares('bulkUpdate', middlewareCtx);
1010
+ return;
1011
+ }
1012
+ for (const entry of nextUpdates) {
1013
+ if (!entry || typeof entry !== 'object') {
1014
+ throw new Error('Each bulk update entry must be a valid object.');
1015
+ }
1016
+ if (!entry.filter || typeof entry.filter !== 'object') {
1017
+ throw new Error('Each bulk update filter must be a valid object.');
1018
+ }
1019
+ if (!entry.update || typeof entry.update !== 'object') {
1020
+ throw new Error('Each bulk update document must be a valid object.');
1021
+ }
1022
+ }
1023
+ const bulkOps = nextUpdates.map(({ filter, update }) => ({
1024
+ updateOne: {
1025
+ filter: filter,
1026
+ update: { $set: update }
1027
+ }
1028
+ }));
1029
+ await this.schema.executePre('bulkUpdate', this.buildHookPayload('bulkUpdate', ctx, {
1030
+ collectionName: middlewareCtx.collectionName,
1031
+ tenantId: middlewareCtx.tenantId,
1032
+ dbName: middlewareCtx.dbName,
1033
+ session,
1034
+ bulkUpdates: nextUpdates,
1035
+ meta
1036
+ }));
1037
+ const result = await col.bulkWrite(bulkOps, {
1038
+ ordered: false,
1039
+ ...(session ? { session } : {})
1040
+ });
1041
+ const bulkResult = {
1042
+ acknowledged: result?.isOk?.() ?? true,
1043
+ matchedCount: result?.matchedCount,
1044
+ modifiedCount: result?.modifiedCount,
1045
+ upsertedCount: result?.upsertedCount,
1046
+ insertedCount: result?.insertedCount,
1047
+ deletedCount: result?.deletedCount
1048
+ };
1049
+ await this.schema.executePost('bulkUpdate', this.buildHookPayload('bulkUpdate', ctx, {
1050
+ collectionName: middlewareCtx.collectionName,
1051
+ tenantId: middlewareCtx.tenantId,
1052
+ dbName: middlewareCtx.dbName,
1053
+ session,
1054
+ bulkUpdates: nextUpdates,
1055
+ result: bulkResult,
1056
+ meta
1057
+ }));
1058
+ middlewareCtx.result = bulkResult;
1059
+ await this.runAfterMiddlewares('bulkUpdate', middlewareCtx);
1060
+ await this.invalidateCachePatterns('bulkUpdate', middlewareCtx);
1061
+ await pubsub.publish(`${utils_1.DB_CHANGE_EVENT}`, {
1062
+ documentUpdated: {
1063
+ action: 'bulkUpdate',
1064
+ collectionName: middlewareCtx.collectionName,
1065
+ bulkUpdates: nextUpdates,
1066
+ result: bulkResult,
1067
+ meta
1068
+ }
1069
+ });
1070
+ }));
1071
+ }
1072
+ /**
1073
+ * Deletes a single document matching the provided filter.
1074
+ *
1075
+ * This operation runs inside the active Ambiten runtime context, is instrumented
1076
+ * through {@link measureQuery}, executes registered delete middlewares,
1077
+ * supports middleware-driven soft delete flows, triggers schema delete or
1078
+ * update hooks as appropriate, invalidates relevant cache patterns, and
1079
+ * publishes a database change event after completion.
1080
+ *
1081
+ * When middleware metadata enables soft delete, the matched document is updated
1082
+ * instead of being physically removed from the collection.
1083
+ *
1084
+ * @param filter - MongoDB filter used to identify the document to delete.
1085
+ * @param ctx - Optional model execution context for tenant, database,
1086
+ * collection, session, and related runtime overrides.
1087
+ * @returns A promise that resolves when the operation completes.
1088
+ * @throws {Error} When the filter is missing or invalid.
1089
+ * @throws {Error} When soft delete is enabled but no softDeleteUpdate payload is provided.
1090
+ */
1091
+ async deleteOne(filter, ctx) {
1092
+ await this.init();
1093
+ if (!filter || typeof filter !== 'object') {
1094
+ throw new Error('Filter must be a valid object.');
1095
+ }
1096
+ return this.runWithModelContext(ctx, async () => (0, instrumentation_1.measureQuery)({
1097
+ operation: 'deleteOne',
1098
+ collectionName: this.resolveCollectionName(ctx),
1099
+ filter
1100
+ }, async () => {
1101
+ const collectionName = this.resolveCollectionName(ctx);
1102
+ const session = this.resolveSession(ctx);
1103
+ const col = await this.getCollection(ctx);
1104
+ const middlewareCtx = this.buildMiddlewareContext('deleteOne', ctx, {
1105
+ filter
1106
+ });
1107
+ middlewareCtx.collectionName =
1108
+ middlewareCtx.collectionName ?? collectionName;
1109
+ await this.runBeforeMiddlewares('deleteOne', middlewareCtx);
1110
+ const effectiveFilter = (middlewareCtx.filter ?? filter);
1111
+ middlewareCtx.filter = effectiveFilter;
1112
+ const doc = await col.findOne(effectiveFilter, session ? { session } : undefined);
1113
+ if (!doc) {
1114
+ middlewareCtx.result = null;
1115
+ await this.runAfterMiddlewares('deleteOne', middlewareCtx);
1116
+ return;
1117
+ }
1118
+ const payload = this.toModelResult(doc);
1119
+ middlewareCtx.result = payload;
1120
+ const meta = this.buildOperationMeta(middlewareCtx.meta);
1121
+ middlewareCtx.meta = meta;
1122
+ if (meta.softDelete === true) {
1123
+ const softDeleteUpdate = middlewareCtx.update;
1124
+ if (!softDeleteUpdate) {
1125
+ throw new Error('Soft delete is enabled but no soft delete update payload was provided.');
1126
+ }
1127
+ await this.schema.executePre('updateOne', this.buildHookPayload('deleteOne', ctx, {
1128
+ collectionName: middlewareCtx.collectionName,
1129
+ tenantId: middlewareCtx.tenantId,
1130
+ dbName: middlewareCtx.dbName,
1131
+ session,
1132
+ filter: effectiveFilter,
1133
+ update: softDeleteUpdate,
1134
+ result: payload,
1135
+ meta
1136
+ }));
1137
+ await col.updateOne(effectiveFilter, softDeleteUpdate, session ? { session } : undefined);
1138
+ await this.schema.executePost('updateOne', this.buildHookPayload('deleteOne', ctx, {
1139
+ collectionName: middlewareCtx.collectionName,
1140
+ tenantId: middlewareCtx.tenantId,
1141
+ dbName: middlewareCtx.dbName,
1142
+ session,
1143
+ filter: effectiveFilter,
1144
+ update: softDeleteUpdate,
1145
+ result: payload,
1146
+ meta
1147
+ }));
1148
+ }
1149
+ else {
1150
+ await this.schema.triggerMiddleware('pre', 'deleteOne', {
1151
+ operation: 'deleteOne',
1152
+ collectionName: middlewareCtx.collectionName,
1153
+ ...payload
1154
+ });
1155
+ await this.schema.executePre('deleteOne', this.buildHookPayload('deleteOne', ctx, {
1156
+ collectionName: middlewareCtx.collectionName,
1157
+ tenantId: middlewareCtx.tenantId,
1158
+ dbName: middlewareCtx.dbName,
1159
+ session,
1160
+ doc: payload,
1161
+ result: payload,
1162
+ meta
1163
+ }));
1164
+ await col.deleteOne(effectiveFilter, session ? { session } : undefined);
1165
+ await this.schema.triggerMiddleware('post', 'deleteOne', {
1166
+ operation: 'deleteOne',
1167
+ collectionName: middlewareCtx.collectionName,
1168
+ ...payload
1169
+ });
1170
+ await this.schema.executePost('deleteOne', this.buildHookPayload('deleteOne', ctx, {
1171
+ collectionName: middlewareCtx.collectionName,
1172
+ tenantId: middlewareCtx.tenantId,
1173
+ dbName: middlewareCtx.dbName,
1174
+ session,
1175
+ doc: payload,
1176
+ result: payload,
1177
+ meta
1178
+ }));
1179
+ }
1180
+ await this.runAfterMiddlewares('deleteOne', middlewareCtx);
1181
+ await this.invalidateCachePatterns('deleteOne', middlewareCtx);
1182
+ await pubsub.publish(`${utils_1.DB_CHANGE_EVENT}`, {
1183
+ documentDeleted: {
1184
+ action: meta.softDelete ? 'softDelete' : 'deleteOne',
1185
+ collectionName: middlewareCtx.collectionName,
1186
+ filter: effectiveFilter,
1187
+ meta: middlewareCtx.meta
1188
+ }
1189
+ });
1190
+ }));
1191
+ }
1192
+ /**
1193
+ * Deletes multiple documents matching the provided filter.
1194
+ *
1195
+ * This operation runs inside the active Ambiten runtime context, is instrumented
1196
+ * through {@link measureQuery}, executes registered deleteMany middlewares,
1197
+ * supports middleware-driven soft delete flows, triggers schema delete or
1198
+ * update hooks as appropriate, invalidates relevant cache patterns, and
1199
+ * publishes a database change event after completion.
1200
+ *
1201
+ * When middleware metadata enables soft delete, matched documents are updated
1202
+ * instead of being physically removed from the collection.
1203
+ *
1204
+ * @param filter - MongoDB filter used to identify the documents to delete.
1205
+ * @param ctx - Optional model execution context for tenant, database,
1206
+ * collection, session, and related runtime overrides.
1207
+ * @returns A promise that resolves when the operation completes.
1208
+ * @throws {Error} When the filter is missing or invalid.
1209
+ * @throws {Error} When soft delete is enabled but no softDeleteUpdate payload is provided.
1210
+ */
1211
+ async deleteMany(filter, ctx) {
1212
+ await this.init();
1213
+ if (!filter || typeof filter !== 'object') {
1214
+ throw new Error('Filter must be a valid object.');
1215
+ }
1216
+ return this.runWithModelContext(ctx, async () => (0, instrumentation_1.measureQuery)({
1217
+ operation: 'deleteMany',
1218
+ collectionName: this.resolveCollectionName(ctx),
1219
+ filter
1220
+ }, async () => {
1221
+ const collectionName = this.resolveCollectionName(ctx);
1222
+ const session = this.resolveSession(ctx);
1223
+ const col = await this.getCollection(ctx);
1224
+ const middlewareCtx = this.buildMiddlewareContext('deleteMany', ctx, {
1225
+ filter
1226
+ });
1227
+ middlewareCtx.collectionName =
1228
+ middlewareCtx.collectionName ?? collectionName;
1229
+ await this.runBeforeMiddlewares('deleteMany', middlewareCtx);
1230
+ const effectiveFilter = (middlewareCtx.filter ?? filter);
1231
+ middlewareCtx.filter = effectiveFilter;
1232
+ const meta = this.buildOperationMeta(middlewareCtx.meta);
1233
+ middlewareCtx.meta = meta;
1234
+ const docs = await col.find(effectiveFilter, session ? { session } : undefined).toArray();
1235
+ if (docs.length === 0) {
1236
+ middlewareCtx.result = {
1237
+ deletedCount: 0,
1238
+ docs: []
1239
+ };
1240
+ await this.runAfterMiddlewares('deleteMany', middlewareCtx);
1241
+ return;
1242
+ }
1243
+ const existingDocs = this.toModelResults(docs);
1244
+ if (meta.softDelete === true) {
1245
+ const softDeleteUpdate = middlewareCtx.update;
1246
+ if (!softDeleteUpdate) {
1247
+ throw new Error('Soft delete is enabled but no soft delete update payload was provided.');
1248
+ }
1249
+ const softDeleteMeta = this.buildOperationMeta(meta, {
1250
+ softDelete: true
1251
+ });
1252
+ await this.schema.executePre('updateMany', this.buildHookPayload('deleteMany', ctx, {
1253
+ collectionName: middlewareCtx.collectionName,
1254
+ tenantId: middlewareCtx.tenantId,
1255
+ dbName: middlewareCtx.dbName,
1256
+ session,
1257
+ filter: effectiveFilter,
1258
+ update: softDeleteUpdate,
1259
+ docs: docs,
1260
+ result: existingDocs,
1261
+ meta: softDeleteMeta
1262
+ }));
1263
+ const result = await col.updateMany(effectiveFilter, softDeleteUpdate, session ? { session } : undefined);
1264
+ const softDeleteResult = {
1265
+ deletedCount: result.modifiedCount,
1266
+ docs: existingDocs,
1267
+ softDeleted: true
1268
+ };
1269
+ await this.schema.executePost('updateMany', this.buildHookPayload('deleteMany', ctx, {
1270
+ collectionName: middlewareCtx.collectionName,
1271
+ tenantId: middlewareCtx.tenantId,
1272
+ dbName: middlewareCtx.dbName,
1273
+ session,
1274
+ filter: effectiveFilter,
1275
+ update: softDeleteUpdate,
1276
+ docs: docs,
1277
+ result: softDeleteResult,
1278
+ meta: softDeleteMeta
1279
+ }));
1280
+ middlewareCtx.result = softDeleteResult;
1281
+ }
1282
+ else {
1283
+ await this.schema.triggerMiddleware('pre', 'deleteMany', {
1284
+ operation: 'deleteMany',
1285
+ collectionName: middlewareCtx.collectionName,
1286
+ docs: docs
1287
+ });
1288
+ await this.schema.executePre('deleteMany', this.buildHookPayload('deleteMany', ctx, {
1289
+ collectionName: middlewareCtx.collectionName,
1290
+ tenantId: middlewareCtx.tenantId,
1291
+ dbName: middlewareCtx.dbName,
1292
+ session,
1293
+ docs: docs,
1294
+ result: existingDocs,
1295
+ meta
1296
+ }));
1297
+ const result = await col.deleteMany(effectiveFilter, session ? { session } : undefined);
1298
+ const deleteResult = {
1299
+ deletedCount: result?.deletedCount ?? 0,
1300
+ docs: existingDocs
1301
+ };
1302
+ await this.schema.triggerMiddleware('post', 'deleteMany', {
1303
+ operation: 'deleteMany',
1304
+ collectionName: middlewareCtx.collectionName,
1305
+ docs: docs
1306
+ });
1307
+ await this.schema.executePost('deleteMany', this.buildHookPayload('deleteMany', ctx, {
1308
+ collectionName: middlewareCtx.collectionName,
1309
+ tenantId: middlewareCtx.tenantId,
1310
+ dbName: middlewareCtx.dbName,
1311
+ session,
1312
+ docs: docs,
1313
+ result: deleteResult,
1314
+ meta
1315
+ }));
1316
+ middlewareCtx.result = deleteResult;
1317
+ }
1318
+ await this.runAfterMiddlewares('deleteMany', middlewareCtx);
1319
+ await this.invalidateCachePatterns('deleteMany', middlewareCtx);
1320
+ await pubsub.publish(`${utils_1.DB_CHANGE_EVENT}`, {
1321
+ documentDeleted: {
1322
+ action: meta.softDelete ? 'softDeleteMany' : 'deleteMany',
1323
+ collectionName: middlewareCtx.collectionName,
1324
+ filter: effectiveFilter,
1325
+ result: middlewareCtx.result,
1326
+ meta: middlewareCtx.meta
1327
+ }
1328
+ });
1329
+ }));
1330
+ }
1331
+ /**
1332
+ * Populates a single reference field on a document by resolving the related
1333
+ * document from another model.
1334
+ *
1335
+ * This helper performs a follow-up lookup using the referenced field value as
1336
+ * the `_id` of the related document. If the target field is empty or
1337
+ * undefined, the original document is returned unchanged.
1338
+ *
1339
+ * @typeParam K - The related model document type.
1340
+ * @param doc - The source document containing the reference field.
1341
+ * @param field - The field on the source document that stores the related document identifier.
1342
+ * @param relatedModel - The model used to resolve the related document.
1343
+ * @returns The source document with the populated field replaced by the related
1344
+ * document result, or `null` when the input document is invalid.
1345
+ */
1346
+ async populateOne(doc, field, relatedModel) {
1347
+ await this.init();
1348
+ if (!doc || typeof doc !== 'object') {
1349
+ return null;
1350
+ }
1351
+ const referenceValue = doc[field];
1352
+ if (referenceValue === undefined || referenceValue === null) {
1353
+ return doc;
1354
+ }
1355
+ const relatedDoc = await relatedModel.findOne({
1356
+ _id: referenceValue
1357
+ });
1358
+ return {
1359
+ ...doc,
1360
+ [field]: relatedDoc
1361
+ };
1362
+ }
1363
+ /**
1364
+ * Populates an array reference field on a document by resolving the related
1365
+ * documents from another model.
1366
+ *
1367
+ * This helper performs a follow-up lookup using the referenced field values as
1368
+ * `_id` values of the related documents. If the target field is empty,
1369
+ * undefined, or not an array, the original document is returned unchanged.
1370
+ *
1371
+ * @typeParam K - The related model document type.
1372
+ * @param doc - The source document containing the reference array field.
1373
+ * @param field - The field on the source document that stores related document identifiers.
1374
+ * @param relatedModel - The model used to resolve the related documents.
1375
+ * @returns The source document with the populated field replaced by the related
1376
+ * document results, or `null` when the input document is invalid.
1377
+ */
1378
+ async populateMany(doc, field, relatedModel) {
1379
+ await this.init();
1380
+ if (!doc || typeof doc !== 'object') {
1381
+ return null;
1382
+ }
1383
+ const referenceValues = doc[field];
1384
+ if (!Array.isArray(referenceValues) || referenceValues.length === 0) {
1385
+ return doc;
1386
+ }
1387
+ const relatedDocs = await relatedModel.find({
1388
+ _id: { $in: referenceValues }
1389
+ });
1390
+ return {
1391
+ ...doc,
1392
+ [field]: relatedDocs
1393
+ };
1394
+ }
1395
+ /**
1396
+ * Creates a new document inside a transaction-aware runtime context.
1397
+ *
1398
+ * This transactional variant runs within the active Ambiten runtime context,
1399
+ * executes registered create middlewares, validates the incoming document,
1400
+ * triggers schema create hooks with transactional metadata, invalidates
1401
+ * relevant cache patterns, and publishes a database change event after
1402
+ * successful insertion.
1403
+ *
1404
+ * @param doc - The document to insert.
1405
+ * @param ctx - Optional model execution context for tenant, database,
1406
+ * collection, session, and transaction-aware runtime overrides.
1407
+ * @returns The created document as a normalized model result.
1408
+ * @throws {Error} When the provided document is missing or invalid.
1409
+ */
1410
+ async createWithTransaction(doc, ctx) {
1411
+ await this.init();
1412
+ if (!doc || typeof doc !== 'object') {
1413
+ throw new Error('Document must be a valid object.');
1414
+ }
1415
+ return this.runWithModelContext(ctx, async () => this.withTransaction(async () => (0, instrumentation_1.measureQuery)({
1416
+ operation: 'create',
1417
+ collectionName: this.resolveCollectionName(ctx),
1418
+ documentCount: 1
1419
+ }, async () => {
1420
+ const collectionName = this.resolveCollectionName(ctx);
1421
+ const session = this.resolveSession(ctx);
1422
+ const col = await this.getCollection(ctx);
1423
+ const middlewareCtx = this.buildMiddlewareContext('create', ctx, {
1424
+ doc: doc,
1425
+ meta: { transactional: true }
1426
+ });
1427
+ middlewareCtx.collectionName =
1428
+ middlewareCtx.collectionName ?? collectionName;
1429
+ await this.runBeforeMiddlewares('create', middlewareCtx);
1430
+ const nextDoc = (middlewareCtx.doc ?? doc);
1431
+ middlewareCtx.doc = nextDoc;
1432
+ const meta = this.buildOperationMeta(middlewareCtx.meta, {
1433
+ transactional: true
1434
+ });
1435
+ this.validate(nextDoc);
1436
+ await this.schema.executePre('create', this.buildHookPayload('create', ctx, {
1437
+ collectionName: middlewareCtx.collectionName,
1438
+ tenantId: middlewareCtx.tenantId,
1439
+ dbName: middlewareCtx.dbName,
1440
+ session,
1441
+ doc: nextDoc,
1442
+ meta
1443
+ }));
1444
+ const result = await col.insertOne(nextDoc, session ? { session } : undefined);
1445
+ const createdDoc = {
1446
+ ...nextDoc,
1447
+ _id: result.insertedId
1448
+ };
1449
+ await this.schema.executePost('create', this.buildHookPayload('create', ctx, {
1450
+ operation: 'create',
1451
+ collectionName: middlewareCtx.collectionName,
1452
+ tenantId: middlewareCtx.tenantId,
1453
+ dbName: middlewareCtx.dbName,
1454
+ session,
1455
+ doc: createdDoc,
1456
+ result: createdDoc,
1457
+ meta
1458
+ }));
1459
+ const payload = this.toModelResult(createdDoc);
1460
+ middlewareCtx.result = payload;
1461
+ await this.runAfterMiddlewares('create', middlewareCtx);
1462
+ await this.invalidateCachePatterns('create', middlewareCtx);
1463
+ const finalResult = (middlewareCtx.result ?? payload);
1464
+ await pubsub.publish(`${utils_1.DB_CHANGE_EVENT}`, {
1465
+ documentInserted: {
1466
+ action: 'create',
1467
+ collectionName: middlewareCtx.collectionName,
1468
+ doc: finalResult,
1469
+ meta
1470
+ }
1471
+ });
1472
+ return finalResult;
1473
+ }), ctx));
1474
+ }
1475
+ /**
1476
+ * Executes multiple MongoDB bulk write operations inside a transaction-aware
1477
+ * runtime context.
1478
+ *
1479
+ * This transactional variant runs within the active Ambiten runtime context,
1480
+ * executes registered bulk update middlewares, triggers schema bulk update
1481
+ * hooks with transactional metadata, invalidates relevant cache patterns, and
1482
+ * publishes a database change event after completion.
1483
+ *
1484
+ * @param operations - MongoDB bulk write operations to execute.
1485
+ * @param options - Optional MongoDB bulk write options.
1486
+ * @param ctx - Optional model execution context for tenant, database,
1487
+ * collection, session, and transaction-aware runtime overrides.
1488
+ * @returns The MongoDB bulk write result.
1489
+ * @throws {Error} When the operations payload is missing or invalid.
1490
+ */
1491
+ async bulkWriteWithTransaction(operations, options = {}, ctx) {
1492
+ await this.init();
1493
+ if (!Array.isArray(operations)) {
1494
+ throw new Error('Operations must be a valid array.');
1495
+ }
1496
+ if (operations.length === 0) {
1497
+ throw new Error('Operations array must not be empty.');
1498
+ }
1499
+ return this.runWithModelContext(ctx, async () => this.withTransaction(async () => (0, instrumentation_1.measureQuery)({
1500
+ operation: 'bulkWrite',
1501
+ collectionName: this.resolveCollectionName(ctx),
1502
+ documentCount: operations.length,
1503
+ extra: {
1504
+ bulkWrite: true
1505
+ }
1506
+ }, async () => {
1507
+ const collectionName = this.resolveCollectionName(ctx);
1508
+ const session = this.resolveSession(ctx);
1509
+ const col = await this.getCollection(ctx);
1510
+ const middlewareCtx = this.buildMiddlewareContext('bulkUpdate', ctx, {
1511
+ bulkOperations: operations,
1512
+ meta: {
1513
+ transactional: true,
1514
+ bulkWrite: true
1515
+ }
1516
+ });
1517
+ middlewareCtx.collectionName =
1518
+ middlewareCtx.collectionName ?? collectionName;
1519
+ await this.runBeforeMiddlewares('bulkUpdate', middlewareCtx);
1520
+ const nextOperations = middlewareCtx.bulkOperations ?? operations;
1521
+ middlewareCtx.bulkOperations = nextOperations;
1522
+ const meta = this.buildOperationMeta(middlewareCtx.meta, {
1523
+ transactional: true,
1524
+ bulkWrite: true
1525
+ });
1526
+ middlewareCtx.meta = meta;
1527
+ await this.schema.executePre('bulkUpdate', this.buildHookPayload('bulkUpdate', ctx, {
1528
+ collectionName: middlewareCtx.collectionName,
1529
+ tenantId: middlewareCtx.tenantId,
1530
+ dbName: middlewareCtx.dbName,
1531
+ session,
1532
+ bulkOperations: nextOperations,
1533
+ meta
1534
+ }));
1535
+ const result = await col.bulkWrite(nextOperations, {
1536
+ ...options,
1537
+ ...(session ? { session } : {})
1538
+ });
1539
+ const bulkResultSummary = {
1540
+ acknowledged: result?.isOk?.() ?? true,
1541
+ matchedCount: result?.matchedCount,
1542
+ modifiedCount: result?.modifiedCount,
1543
+ upsertedCount: result?.upsertedCount,
1544
+ insertedCount: result?.insertedCount,
1545
+ deletedCount: result?.deletedCount
1546
+ };
1547
+ await this.schema.executePost('bulkUpdate', this.buildHookPayload('bulkUpdate', ctx, {
1548
+ collectionName: middlewareCtx.collectionName,
1549
+ tenantId: middlewareCtx.tenantId,
1550
+ dbName: middlewareCtx.dbName,
1551
+ session,
1552
+ bulkOperations: nextOperations,
1553
+ result: bulkResultSummary,
1554
+ meta
1555
+ }));
1556
+ middlewareCtx.result = bulkResultSummary;
1557
+ await this.runAfterMiddlewares('bulkUpdate', middlewareCtx);
1558
+ await this.invalidateCachePatterns('bulkWrite', middlewareCtx);
1559
+ await pubsub.publish(`${utils_1.DB_CHANGE_EVENT}`, {
1560
+ bulkWrite: {
1561
+ action: 'bulkWrite',
1562
+ collectionName: middlewareCtx.collectionName,
1563
+ result: bulkResultSummary,
1564
+ meta
1565
+ }
1566
+ });
1567
+ return result;
1568
+ }), ctx));
1569
+ }
1570
+ /**
1571
+ * Deletes a single document inside a transaction-aware runtime context.
1572
+ *
1573
+ * This transactional variant runs within the active Ambiten runtime context,
1574
+ * executes registered delete middlewares, triggers schema delete hooks with
1575
+ * transactional metadata, invalidates relevant cache patterns, and publishes a
1576
+ * database change event after completion.
1577
+ *
1578
+ * @param filter - MongoDB filter used to identify the document to delete.
1579
+ * @param ctx - Optional model execution context for tenant, database,
1580
+ * collection, session, and transaction-aware runtime overrides.
1581
+ * @returns `true` when a document was deleted, otherwise `false`.
1582
+ * @throws {Error} When the filter is missing or invalid.
1583
+ */
1584
+ async deleteWithTransaction(filter, ctx) {
1585
+ await this.init();
1586
+ if (!filter || typeof filter !== 'object') {
1587
+ throw new Error('Filter must be a valid object.');
1588
+ }
1589
+ return this.runWithModelContext(ctx, async () => this.withTransaction(async () => (0, instrumentation_1.measureQuery)({
1590
+ operation: 'deleteOne',
1591
+ collectionName: this.resolveCollectionName(ctx),
1592
+ filter
1593
+ }, async () => {
1594
+ const collectionName = this.resolveCollectionName(ctx);
1595
+ const session = this.resolveSession(ctx);
1596
+ const col = await this.getCollection(ctx);
1597
+ const meta = {
1598
+ transactional: true
1599
+ };
1600
+ const middlewareCtx = this.buildMiddlewareContext('deleteOne', ctx, {
1601
+ filter,
1602
+ meta
1603
+ });
1604
+ middlewareCtx.collectionName =
1605
+ middlewareCtx.collectionName ?? collectionName;
1606
+ await this.runBeforeMiddlewares('deleteOne', middlewareCtx);
1607
+ const effectiveFilter = (middlewareCtx.filter ?? filter);
1608
+ middlewareCtx.filter = effectiveFilter;
1609
+ const existingDoc = await col.findOne(effectiveFilter, session ? { session } : undefined);
1610
+ if (!existingDoc) {
1611
+ middlewareCtx.result = null;
1612
+ await this.runAfterMiddlewares('deleteOne', middlewareCtx);
1613
+ return false;
1614
+ }
1615
+ const payload = this.toModelResult(existingDoc);
1616
+ middlewareCtx.result = payload;
1617
+ await this.schema.triggerMiddleware('pre', 'deleteOne', {
1618
+ operation: 'deleteOne',
1619
+ collectionName: middlewareCtx.collectionName,
1620
+ ...payload
1621
+ });
1622
+ await this.schema.executePre('deleteOne', {
1623
+ operation: 'deleteOne',
1624
+ collectionName: middlewareCtx.collectionName,
1625
+ tenantId: middlewareCtx.tenantId,
1626
+ dbName: middlewareCtx.dbName,
1627
+ session,
1628
+ doc: existingDoc,
1629
+ result: payload,
1630
+ meta
1631
+ });
1632
+ const result = await col.deleteOne(effectiveFilter, session ? { session } : undefined);
1633
+ await this.schema.triggerMiddleware('post', 'deleteOne', {
1634
+ operation: 'deleteOne',
1635
+ collectionName: middlewareCtx.collectionName,
1636
+ ...payload
1637
+ });
1638
+ await this.schema.executePost('deleteOne', {
1639
+ operation: 'deleteOne',
1640
+ collectionName: middlewareCtx.collectionName,
1641
+ tenantId: middlewareCtx.tenantId,
1642
+ dbName: middlewareCtx.dbName,
1643
+ session,
1644
+ doc: existingDoc,
1645
+ result: payload,
1646
+ meta
1647
+ });
1648
+ await this.runAfterMiddlewares('deleteOne', middlewareCtx);
1649
+ await this.invalidateCachePatterns('deleteOne', middlewareCtx);
1650
+ await pubsub.publish(`${utils_1.DB_CHANGE_EVENT}`, {
1651
+ documentDeleted: {
1652
+ action: 'deleteOne',
1653
+ collectionName: middlewareCtx.collectionName,
1654
+ filter: effectiveFilter,
1655
+ meta
1656
+ }
1657
+ });
1658
+ return result.deletedCount === 1;
1659
+ }), ctx));
1660
+ }
1661
+ /**
1662
+ * Updates a single document inside a transaction-aware runtime context.
1663
+ *
1664
+ * This transactional variant runs within the active Ambiten runtime context,
1665
+ * executes registered update middlewares, validates the projected updated
1666
+ * document, triggers schema update hooks with transactional metadata,
1667
+ * invalidates relevant cache patterns, and publishes a database change event
1668
+ * after completion.
1669
+ *
1670
+ * @param filter - MongoDB filter used to identify the document to update.
1671
+ * @param update - MongoDB update document to apply.
1672
+ * @param ctx - Optional model execution context for tenant, database,
1673
+ * collection, session, and transaction-aware runtime overrides.
1674
+ * @returns The updated document as a normalized model result, or `null` if no
1675
+ * document matched the filter.
1676
+ * @throws {Error} When the filter is missing or invalid.
1677
+ * @throws {Error} When the update document is missing or invalid.
1678
+ */
1679
+ async updateWithTransaction(filter, update, ctx) {
1680
+ await this.init();
1681
+ if (!filter || typeof filter !== 'object') {
1682
+ throw new Error('Filter must be a valid object.');
1683
+ }
1684
+ if (!update || typeof update !== 'object') {
1685
+ throw new Error('Update must be a valid object.');
1686
+ }
1687
+ return this.runWithModelContext(ctx, async () => this.withTransaction(async () => (0, instrumentation_1.measureQuery)({
1688
+ operation: 'updateOne',
1689
+ collectionName: this.resolveCollectionName(ctx),
1690
+ filter,
1691
+ update
1692
+ }, async () => {
1693
+ const collectionName = this.resolveCollectionName(ctx);
1694
+ const session = this.resolveSession(ctx);
1695
+ const col = await this.getCollection(ctx);
1696
+ const meta = {
1697
+ transactional: true
1698
+ };
1699
+ const middlewareCtx = this.buildMiddlewareContext('updateOne', ctx, {
1700
+ filter,
1701
+ update,
1702
+ meta
1703
+ });
1704
+ middlewareCtx.collectionName =
1705
+ middlewareCtx.collectionName ?? collectionName;
1706
+ await this.runBeforeMiddlewares('updateOne', middlewareCtx);
1707
+ const effectiveFilter = (middlewareCtx.filter ?? filter);
1708
+ const effectiveUpdate = (middlewareCtx.update ?? update);
1709
+ middlewareCtx.filter = effectiveFilter;
1710
+ middlewareCtx.update = effectiveUpdate;
1711
+ const existingDoc = await col.findOne(effectiveFilter, session ? { session } : undefined);
1712
+ if (!existingDoc) {
1713
+ middlewareCtx.result = null;
1714
+ await this.runAfterMiddlewares('updateOne', middlewareCtx);
1715
+ return null;
1716
+ }
1717
+ const projectedUpdatedDoc = {
1718
+ ...existingDoc,
1719
+ ...(effectiveUpdate.$set ?? {})
1720
+ };
1721
+ this.validate(projectedUpdatedDoc);
1722
+ await this.schema.executePre('updateOne', {
1723
+ operation: 'updateOne',
1724
+ collectionName: middlewareCtx.collectionName,
1725
+ tenantId: middlewareCtx.tenantId,
1726
+ dbName: middlewareCtx.dbName,
1727
+ session,
1728
+ filter: effectiveFilter,
1729
+ update: effectiveUpdate,
1730
+ meta
1731
+ });
1732
+ await col.updateOne(effectiveFilter, effectiveUpdate, session ? { session } : undefined);
1733
+ const result = await col.findOne(effectiveFilter, session ? { session } : undefined);
1734
+ const payload = result
1735
+ ? this.toModelResult(result)
1736
+ : null;
1737
+ await this.schema.executePost('updateOne', {
1738
+ operation: 'updateOne',
1739
+ collectionName: middlewareCtx.collectionName,
1740
+ tenantId: middlewareCtx.tenantId,
1741
+ dbName: middlewareCtx.dbName,
1742
+ session,
1743
+ filter: effectiveFilter,
1744
+ update: effectiveUpdate,
1745
+ result: payload,
1746
+ meta
1747
+ });
1748
+ middlewareCtx.result = payload;
1749
+ await this.runAfterMiddlewares('updateOne', middlewareCtx);
1750
+ await this.invalidateCachePatterns('updateOne', middlewareCtx);
1751
+ await pubsub.publish(`${utils_1.DB_CHANGE_EVENT}`, {
1752
+ documentUpdated: {
1753
+ action: 'updateOne',
1754
+ collectionName: middlewareCtx.collectionName,
1755
+ filter: effectiveFilter,
1756
+ update: effectiveUpdate,
1757
+ result: payload,
1758
+ meta
1759
+ }
1760
+ });
1761
+ return (middlewareCtx.result ?? payload);
1762
+ }), ctx));
1763
+ }
1764
+ /**
1765
+ * Finds a single document matching the provided filter and updates it.
1766
+ *
1767
+ * This operation runs inside the active Ambiten runtime context, is instrumented
1768
+ * through {@link measureQuery}, executes registered findOneAndUpdate
1769
+ * middlewares, triggers schema update hooks, invalidates relevant cache
1770
+ * patterns, and publishes a database change event after completion.
1771
+ *
1772
+ * The updated document is returned in its post-update state when a matching
1773
+ * document is found.
1774
+ *
1775
+ * @param filter - MongoDB filter used to identify the document to update.
1776
+ * @param update - MongoDB update document to apply.
1777
+ * @param ctx - Optional model execution context for tenant, database,
1778
+ * collection, session, and related runtime overrides.
1779
+ * @returns The updated document as a normalized model result, or `null` if no
1780
+ * document matched the filter.
1781
+ * @throws {Error} When the filter is missing or invalid.
1782
+ * @throws {Error} When the update document is missing or invalid.
1783
+ */
1784
+ async findOneAndUpdate(filter, update, ctx) {
1785
+ await this.init();
1786
+ if (!filter || typeof filter !== 'object') {
1787
+ throw new Error('Filter must be a valid object.');
1788
+ }
1789
+ if (!update || typeof update !== 'object') {
1790
+ throw new Error('Update must be a valid object.');
1791
+ }
1792
+ return this.runWithModelContext(ctx, async () => (0, instrumentation_1.measureQuery)({
1793
+ operation: 'findOneAndUpdate',
1794
+ collectionName: this.resolveCollectionName(ctx),
1795
+ filter,
1796
+ update
1797
+ }, async () => {
1798
+ const collectionName = this.resolveCollectionName(ctx);
1799
+ const session = this.resolveSession(ctx);
1800
+ const col = await this.getCollection(ctx);
1801
+ const middlewareCtx = this.buildMiddlewareContext('findOneAndUpdate', ctx, {
1802
+ filter,
1803
+ update
1804
+ });
1805
+ middlewareCtx.collectionName =
1806
+ middlewareCtx.collectionName ?? collectionName;
1807
+ await this.runBeforeMiddlewares('findOneAndUpdate', middlewareCtx);
1808
+ const effectiveFilter = (middlewareCtx.filter ?? filter);
1809
+ const effectiveUpdate = (middlewareCtx.update ?? update);
1810
+ middlewareCtx.filter = effectiveFilter;
1811
+ middlewareCtx.update = effectiveUpdate;
1812
+ await this.schema.executePre('updateOne', {
1813
+ operation: 'findOneAndUpdate',
1814
+ collectionName: middlewareCtx.collectionName,
1815
+ tenantId: middlewareCtx.tenantId,
1816
+ dbName: middlewareCtx.dbName,
1817
+ session,
1818
+ filter: effectiveFilter,
1819
+ update: effectiveUpdate,
1820
+ meta: middlewareCtx.meta
1821
+ });
1822
+ const updateResult = await col.findOneAndUpdate(effectiveFilter, effectiveUpdate, {
1823
+ returnDocument: 'after',
1824
+ ...(session ? { session } : {})
1825
+ });
1826
+ const updatedDoc = updateResult &&
1827
+ typeof updateResult === 'object' &&
1828
+ 'value' in updateResult
1829
+ ? updateResult.value
1830
+ : updateResult;
1831
+ const payload = updatedDoc
1832
+ ? this.toModelResult(updatedDoc)
1833
+ : null;
1834
+ await this.schema.executePost('updateOne', {
1835
+ operation: 'findOneAndUpdate',
1836
+ collectionName: middlewareCtx.collectionName,
1837
+ tenantId: middlewareCtx.tenantId,
1838
+ dbName: middlewareCtx.dbName,
1839
+ session,
1840
+ filter: effectiveFilter,
1841
+ update: effectiveUpdate,
1842
+ result: payload,
1843
+ meta: middlewareCtx.meta
1844
+ });
1845
+ middlewareCtx.result = payload;
1846
+ await this.runAfterMiddlewares('findOneAndUpdate', middlewareCtx);
1847
+ await this.invalidateCachePatterns('findOneAndUpdate', middlewareCtx);
1848
+ await pubsub.publish(`${utils_1.DB_CHANGE_EVENT}`, {
1849
+ documentUpdated: {
1850
+ action: 'findOneAndUpdate',
1851
+ collectionName: middlewareCtx.collectionName,
1852
+ filter: effectiveFilter,
1853
+ update: effectiveUpdate,
1854
+ result: payload,
1855
+ meta: middlewareCtx.meta
1856
+ }
1857
+ });
1858
+ return (middlewareCtx.result ?? payload);
1859
+ }));
1860
+ }
1861
+ /**
1862
+ * Finds a single document matching the provided filter and deletes it.
1863
+ *
1864
+ * Supports both hard delete and middleware-driven soft delete flows.
1865
+ * When soft delete is enabled via middleware metadata, the matched document
1866
+ * is updated using the provided soft-delete update operation instead of being
1867
+ * physically removed from the collection.
1868
+ *
1869
+ * The operation:
1870
+ * - runs within the active Ambiten runtime context
1871
+ * - is instrumented through {@link measureQuery}
1872
+ * - executes registered before/after model middlewares
1873
+ * - triggers schema delete/update hooks as appropriate
1874
+ * - invalidates relevant cache patterns after mutation
1875
+ * - publishes a database change event through PubSub
1876
+ *
1877
+ * @param filter - MongoDB filter used to identify the document to delete.
1878
+ * @param ctx - Optional model execution context for tenant, database,
1879
+ * session, cache, and collection overrides.
1880
+ * @returns The deleted document as a model result, or `null` if no document matched.
1881
+ * @throws {Error} When the filter is missing or invalid.
1882
+ */
1883
+ async findOneAndDelete(filter, ctx) {
1884
+ await this.init();
1885
+ if (!filter || typeof filter !== 'object') {
1886
+ throw new Error('Filter must be a valid object.');
1887
+ }
1888
+ return this.runWithModelContext(ctx, async () => (0, instrumentation_1.measureQuery)({
1889
+ operation: 'findOneAndDelete',
1890
+ collectionName: this.resolveCollectionName(ctx),
1891
+ filter
1892
+ }, async () => {
1893
+ const collectionName = this.resolveCollectionName(ctx);
1894
+ const session = this.resolveSession(ctx);
1895
+ const col = await this.getCollection(ctx);
1896
+ const middlewareCtx = this.buildMiddlewareContext('findOneAndDelete', ctx, { filter });
1897
+ middlewareCtx.collectionName =
1898
+ middlewareCtx.collectionName ?? collectionName;
1899
+ await this.runBeforeMiddlewares('findOneAndDelete', middlewareCtx);
1900
+ const effectiveFilter = (middlewareCtx.filter ?? filter);
1901
+ middlewareCtx.filter = effectiveFilter;
1902
+ const meta = this.buildOperationMeta(middlewareCtx.meta);
1903
+ middlewareCtx.meta = meta;
1904
+ const existingDoc = await col.findOne(effectiveFilter, session ? { session } : undefined);
1905
+ if (!existingDoc) {
1906
+ middlewareCtx.result = null;
1907
+ await this.runAfterMiddlewares('findOneAndDelete', middlewareCtx);
1908
+ return null;
1909
+ }
1910
+ const existingPayload = this.toModelResult(existingDoc);
1911
+ middlewareCtx.result = existingPayload;
1912
+ if (meta.softDelete === true) {
1913
+ const softDeleteUpdate = middlewareCtx.update ?? this.buildSoftDeleteUpdate();
1914
+ middlewareCtx.update = softDeleteUpdate;
1915
+ const softDeleteMeta = this.buildOperationMeta(meta, {
1916
+ softDelete: true
1917
+ });
1918
+ await this.schema.executePre('updateOne', this.buildHookPayload('findOneAndDelete', ctx, {
1919
+ collectionName: middlewareCtx.collectionName,
1920
+ tenantId: middlewareCtx.tenantId,
1921
+ dbName: middlewareCtx.dbName,
1922
+ session,
1923
+ filter: effectiveFilter,
1924
+ update: softDeleteUpdate,
1925
+ result: existingPayload,
1926
+ meta: softDeleteMeta
1927
+ }));
1928
+ await col.updateOne(effectiveFilter, softDeleteUpdate, session ? { session } : undefined);
1929
+ await this.schema.executePost('updateOne', this.buildHookPayload('findOneAndDelete', ctx, {
1930
+ collectionName: middlewareCtx.collectionName,
1931
+ tenantId: middlewareCtx.tenantId,
1932
+ dbName: middlewareCtx.dbName,
1933
+ session,
1934
+ filter: effectiveFilter,
1935
+ update: softDeleteUpdate,
1936
+ result: existingPayload,
1937
+ meta: softDeleteMeta
1938
+ }));
1939
+ await this.runAfterMiddlewares('findOneAndDelete', middlewareCtx);
1940
+ await this.invalidateCachePatterns('findOneAndDelete', middlewareCtx);
1941
+ await pubsub.publish(`${utils_1.DB_CHANGE_EVENT}`, {
1942
+ documentDeleted: {
1943
+ action: 'softDeleteOne',
1944
+ collectionName: middlewareCtx.collectionName,
1945
+ filter: effectiveFilter,
1946
+ meta: softDeleteMeta
1947
+ }
1948
+ });
1949
+ return (middlewareCtx.result ?? existingPayload);
1950
+ }
1951
+ await this.schema.triggerMiddleware('pre', 'deleteOne', {
1952
+ operation: 'findOneAndDelete',
1953
+ collectionName: middlewareCtx.collectionName,
1954
+ ...existingPayload
1955
+ });
1956
+ await this.schema.executePre('deleteOne', this.buildHookPayload('findOneAndDelete', ctx, {
1957
+ collectionName: middlewareCtx.collectionName,
1958
+ tenantId: middlewareCtx.tenantId,
1959
+ dbName: middlewareCtx.dbName,
1960
+ session,
1961
+ doc: existingDoc,
1962
+ result: existingPayload,
1963
+ meta
1964
+ }));
1965
+ const deleteOptions = session ? { session } : {};
1966
+ const deleteResult = await col.findOneAndDelete(effectiveFilter, deleteOptions);
1967
+ const deletedDoc = deleteResult &&
1968
+ typeof deleteResult === 'object' &&
1969
+ 'value' in deleteResult
1970
+ ? deleteResult.value
1971
+ : deleteResult;
1972
+ const finalPayload = deletedDoc
1973
+ ? this.toModelResult(deletedDoc)
1974
+ : existingPayload;
1975
+ await this.schema.triggerMiddleware('post', 'deleteOne', {
1976
+ operation: 'findOneAndDelete',
1977
+ collectionName: middlewareCtx.collectionName,
1978
+ ...(finalPayload ?? {})
1979
+ });
1980
+ await this.schema.executePost('deleteOne', this.buildHookPayload('findOneAndDelete', ctx, {
1981
+ collectionName: middlewareCtx.collectionName,
1982
+ tenantId: middlewareCtx.tenantId,
1983
+ dbName: middlewareCtx.dbName,
1984
+ session,
1985
+ doc: existingDoc,
1986
+ result: finalPayload,
1987
+ meta
1988
+ }));
1989
+ middlewareCtx.result = finalPayload;
1990
+ await this.runAfterMiddlewares('findOneAndDelete', middlewareCtx);
1991
+ await this.invalidateCachePatterns('findOneAndDelete', middlewareCtx);
1992
+ await pubsub.publish(`${utils_1.DB_CHANGE_EVENT}`, {
1993
+ documentDeleted: {
1994
+ action: 'findOneAndDelete',
1995
+ collectionName: middlewareCtx.collectionName,
1996
+ filter: effectiveFilter,
1997
+ result: finalPayload,
1998
+ meta
1999
+ }
2000
+ });
2001
+ return (middlewareCtx.result ?? finalPayload);
2002
+ }));
2003
+ }
2004
+ /**
2005
+ * Finds a single document matching the provided filter and replaces it with the
2006
+ * supplied replacement document.
2007
+ *
2008
+ * This operation runs inside the active Ambiten runtime context, executes model
2009
+ * middlewares, triggers schema update hooks, invalidates relevant cache entries,
2010
+ * and publishes a database change event after a successful replacement.
2011
+ *
2012
+ * The replacement document is fully validated before being persisted.
2013
+ *
2014
+ * @param filter - MongoDB filter used to identify the document to replace.
2015
+ * @param replacement - The full replacement document that will overwrite the matched document.
2016
+ * @param ctx - Optional model execution context for tenant, database,
2017
+ * collection, session, and related runtime overrides.
2018
+ * @returns The replaced document in its updated form, or `null` if no document matched the filter.
2019
+ * @throws {Error} When the filter is missing or invalid.
2020
+ * @throws {Error} When the replacement document fails validation.
2021
+ */
2022
+ async findOneAndReplace(filter, replacement, ctx) {
2023
+ await this.init();
2024
+ if (!filter || typeof filter !== 'object') {
2025
+ throw new Error('Filter must be a valid object.');
2026
+ }
2027
+ if (!replacement || typeof replacement !== 'object') {
2028
+ throw new Error('Replacement document must be a valid object.');
2029
+ }
2030
+ return this.runWithModelContext(ctx, async () => (0, instrumentation_1.measureQuery)({
2031
+ operation: 'findOneAndReplace',
2032
+ collectionName: this.resolveCollectionName(ctx),
2033
+ filter
2034
+ }, async () => {
2035
+ const collectionName = this.resolveCollectionName(ctx);
2036
+ const session = this.resolveSession(ctx);
2037
+ const col = await this.getCollection(ctx);
2038
+ const middlewareCtx = this.buildMiddlewareContext('findOneAndReplace', ctx, {
2039
+ filter,
2040
+ doc: replacement
2041
+ });
2042
+ middlewareCtx.collectionName =
2043
+ middlewareCtx.collectionName ?? collectionName;
2044
+ await this.runBeforeMiddlewares('findOneAndReplace', middlewareCtx);
2045
+ const effectiveFilter = (middlewareCtx.filter ?? filter);
2046
+ const nextReplacement = (middlewareCtx.doc ?? replacement);
2047
+ middlewareCtx.filter = effectiveFilter;
2048
+ middlewareCtx.doc = nextReplacement;
2049
+ this.validate(nextReplacement);
2050
+ await this.schema.executePre('updateOne', {
2051
+ operation: 'findOneAndReplace',
2052
+ collectionName: middlewareCtx.collectionName,
2053
+ filter: effectiveFilter,
2054
+ doc: nextReplacement
2055
+ });
2056
+ const replaceResult = await col.findOneAndReplace(effectiveFilter, nextReplacement, {
2057
+ returnDocument: 'after',
2058
+ ...(session ? { session } : {})
2059
+ });
2060
+ const replacedDoc = replaceResult &&
2061
+ typeof replaceResult === 'object' &&
2062
+ 'value' in replaceResult
2063
+ ? replaceResult.value
2064
+ : replaceResult;
2065
+ const payload = replacedDoc
2066
+ ? this.toModelResult(replacedDoc)
2067
+ : null;
2068
+ await this.schema.executePost('updateOne', {
2069
+ operation: 'findOneAndReplace',
2070
+ collectionName: middlewareCtx.collectionName,
2071
+ filter: effectiveFilter,
2072
+ doc: nextReplacement,
2073
+ result: replacedDoc
2074
+ });
2075
+ middlewareCtx.result = payload;
2076
+ await this.runAfterMiddlewares('findOneAndReplace', middlewareCtx);
2077
+ await this.invalidateCachePatterns('findOneAndReplace', middlewareCtx);
2078
+ await pubsub.publish(`${utils_1.DB_CHANGE_EVENT}`, {
2079
+ documentUpdated: {
2080
+ action: 'findOneAndReplace',
2081
+ filter: effectiveFilter,
2082
+ replacement: nextReplacement
2083
+ }
2084
+ });
2085
+ return (middlewareCtx.result ?? payload);
2086
+ }));
2087
+ }
2088
+ /**
2089
+ * Finds a single document matching the provided filter and updates it. If no
2090
+ * matching document exists, a new one is inserted using MongoDB upsert
2091
+ * semantics.
2092
+ *
2093
+ * This operation runs within the active Ambiten runtime context, is instrumented
2094
+ * through {@link measureQuery}, executes registered model middlewares, triggers
2095
+ * schema update hooks, invalidates relevant cache patterns, and publishes a
2096
+ * database change event after completion.
2097
+ *
2098
+ * @param filter - MongoDB filter used to identify the document to update or insert.
2099
+ * @param update - MongoDB update document applied to the matched document or to the inserted document during upsert.
2100
+ * @param ctx - Optional model execution context for tenant, database,
2101
+ * collection, session, and related runtime overrides.
2102
+ * @returns The updated or newly upserted document in its final state, or `null`
2103
+ * if no document could be resolved from the operation result.
2104
+ * @throws {Error} When the filter is missing or invalid.
2105
+ * @throws {Error} When the update document is missing or invalid.
2106
+ */
2107
+ async findOneAndUpsert(filter, update, ctx) {
2108
+ await this.init();
2109
+ if (!filter || typeof filter !== 'object') {
2110
+ throw new Error('Filter must be a valid object.');
2111
+ }
2112
+ if (!update || typeof update !== 'object') {
2113
+ throw new Error('Update must be a valid object.');
2114
+ }
2115
+ return this.runWithModelContext(ctx, async () => (0, instrumentation_1.measureQuery)({
2116
+ operation: 'findOneAndUpsert',
2117
+ collectionName: this.resolveCollectionName(ctx),
2118
+ filter,
2119
+ update
2120
+ }, async () => {
2121
+ const collectionName = this.resolveCollectionName(ctx);
2122
+ const session = this.resolveSession(ctx);
2123
+ const col = await this.getCollection(ctx);
2124
+ const middlewareCtx = this.buildMiddlewareContext('findOneAndUpsert', ctx, {
2125
+ filter,
2126
+ update
2127
+ });
2128
+ middlewareCtx.collectionName =
2129
+ middlewareCtx.collectionName ?? collectionName;
2130
+ await this.runBeforeMiddlewares('findOneAndUpsert', middlewareCtx);
2131
+ const effectiveFilter = (middlewareCtx.filter ?? filter);
2132
+ const effectiveUpdate = (middlewareCtx.update ?? update);
2133
+ middlewareCtx.filter = effectiveFilter;
2134
+ middlewareCtx.update = effectiveUpdate;
2135
+ await this.schema.executePre('updateOne', {
2136
+ operation: 'findOneAndUpsert',
2137
+ collectionName: middlewareCtx.collectionName,
2138
+ filter: effectiveFilter,
2139
+ update: effectiveUpdate
2140
+ });
2141
+ const upsertResult = await col.findOneAndUpdate(effectiveFilter, effectiveUpdate, {
2142
+ upsert: true,
2143
+ returnDocument: 'after',
2144
+ ...(session ? { session } : {})
2145
+ });
2146
+ const updatedDoc = upsertResult &&
2147
+ typeof upsertResult === 'object' &&
2148
+ 'value' in upsertResult
2149
+ ? upsertResult.value
2150
+ : upsertResult;
2151
+ const payload = updatedDoc
2152
+ ? this.toModelResult(updatedDoc)
2153
+ : null;
2154
+ await this.schema.executePost('updateOne', {
2155
+ operation: 'findOneAndUpsert',
2156
+ collectionName: middlewareCtx.collectionName,
2157
+ filter: effectiveFilter,
2158
+ update: effectiveUpdate,
2159
+ result: updatedDoc
2160
+ });
2161
+ middlewareCtx.result = payload;
2162
+ await this.runAfterMiddlewares('findOneAndUpsert', middlewareCtx);
2163
+ await this.invalidateCachePatterns('findOneAndUpsert', middlewareCtx);
2164
+ await pubsub.publish(`${utils_1.DB_CHANGE_EVENT}`, {
2165
+ documentUpdated: {
2166
+ action: 'findOneAndUpsert',
2167
+ filter: effectiveFilter,
2168
+ update: effectiveUpdate
2169
+ }
2170
+ });
2171
+ return (middlewareCtx.result ?? payload);
2172
+ }));
2173
+ }
2174
+ /**
2175
+ * Finds a single document matching the provided filter and updates it inside a
2176
+ * transaction. If no document matches, a new one is inserted using MongoDB
2177
+ * upsert semantics.
2178
+ *
2179
+ * This transactional variant runs inside the active Ambiten runtime context,
2180
+ * executes registered model middlewares, triggers schema update hooks with
2181
+ * transactional metadata, invalidates relevant cache patterns, and publishes a
2182
+ * database change event after completion.
2183
+ *
2184
+ * @param filter - MongoDB filter used to identify the document to update or insert.
2185
+ * @param update - MongoDB update document applied to the matched document or inserted document.
2186
+ * @param ctx - Optional model execution context for tenant, database,
2187
+ * collection, session, and transaction-aware runtime overrides.
2188
+ * @returns The updated or newly inserted document in its final state, or `null`
2189
+ * if no document could be resolved from the operation result.
2190
+ * @throws {Error} When the filter is missing or invalid.
2191
+ * @throws {Error} When the update document is missing or invalid.
2192
+ */
2193
+ async findOneAndUpsertWithTransaction(filter, update, ctx) {
2194
+ await this.init();
2195
+ if (!filter || typeof filter !== 'object') {
2196
+ throw new Error('Filter must be a valid object.');
2197
+ }
2198
+ if (!update || typeof update !== 'object') {
2199
+ throw new Error('Update must be a valid object.');
2200
+ }
2201
+ return this.runWithModelContext(ctx, async () => this.withTransaction(async () => {
2202
+ const collectionName = this.resolveCollectionName(ctx);
2203
+ const col = await this.getCollection(ctx);
2204
+ const session = this.resolveSession(ctx);
2205
+ const middlewareCtx = this.buildMiddlewareContext('findOneAndUpsert', ctx, {
2206
+ filter,
2207
+ update,
2208
+ meta: {
2209
+ transactional: true
2210
+ }
2211
+ });
2212
+ middlewareCtx.collectionName =
2213
+ middlewareCtx.collectionName ?? collectionName;
2214
+ await this.runBeforeMiddlewares('findOneAndUpsert', middlewareCtx);
2215
+ const effectiveFilter = (middlewareCtx.filter ?? filter);
2216
+ const effectiveUpdate = (middlewareCtx.update ?? update);
2217
+ middlewareCtx.filter = effectiveFilter;
2218
+ middlewareCtx.update = effectiveUpdate;
2219
+ await this.schema.executePre('updateOne', {
2220
+ operation: 'findOneAndUpsert',
2221
+ collectionName: middlewareCtx.collectionName,
2222
+ filter: effectiveFilter,
2223
+ update: effectiveUpdate,
2224
+ meta: {
2225
+ transactional: true
2226
+ }
2227
+ });
2228
+ const upsertResult = await col.findOneAndUpdate(effectiveFilter, effectiveUpdate, {
2229
+ upsert: true,
2230
+ returnDocument: 'after',
2231
+ ...(session ? { session } : {})
2232
+ });
2233
+ const updatedDoc = upsertResult &&
2234
+ typeof upsertResult === 'object' &&
2235
+ 'value' in upsertResult
2236
+ ? upsertResult.value
2237
+ : upsertResult;
2238
+ const payload = updatedDoc
2239
+ ? this.toModelResult(updatedDoc)
2240
+ : null;
2241
+ await this.schema.executePost('updateOne', {
2242
+ operation: 'findOneAndUpsert',
2243
+ collectionName: middlewareCtx.collectionName,
2244
+ filter: effectiveFilter,
2245
+ update: effectiveUpdate,
2246
+ result: updatedDoc,
2247
+ meta: {
2248
+ transactional: true
2249
+ }
2250
+ });
2251
+ middlewareCtx.result = payload;
2252
+ await this.runAfterMiddlewares('findOneAndUpsert', middlewareCtx);
2253
+ await this.invalidateCachePatterns('findOneAndUpsert', middlewareCtx);
2254
+ await pubsub.publish(`${utils_1.DB_CHANGE_EVENT}`, {
2255
+ documentUpdated: {
2256
+ action: 'findOneAndUpsert',
2257
+ filter: effectiveFilter,
2258
+ update: effectiveUpdate,
2259
+ meta: {
2260
+ transactional: true
2261
+ }
2262
+ }
2263
+ });
2264
+ return (middlewareCtx.result ?? payload);
2265
+ }, ctx));
2266
+ }
2267
+ /**
2268
+ * Finds a single document matching the provided filter and updates it inside a
2269
+ * secure transaction. If no document matches, a new one is inserted using
2270
+ * MongoDB upsert semantics.
2271
+ *
2272
+ * This secure transactional variant enforces an admin-only authorization rule
2273
+ * before performing the operation. It runs inside the active Ambiten runtime
2274
+ * context, executes registered model middlewares, triggers schema update hooks
2275
+ * with transactional and security metadata, invalidates relevant cache patterns,
2276
+ * and publishes a database change event after completion.
2277
+ *
2278
+ * @param filter - MongoDB filter used to identify the document to update or insert.
2279
+ * @param update - MongoDB update document applied to the matched document or inserted document.
2280
+ * @param user - Authenticated user performing the operation. Must have the `admin` role.
2281
+ * @param ctx - Optional model execution context for tenant, database,
2282
+ * collection, session, and transaction-aware runtime overrides.
2283
+ * @returns The updated or newly inserted document in its final state, or `null`
2284
+ * if no document could be resolved from the operation result.
2285
+ * @throws {Error} When the user is not authorized to perform the operation.
2286
+ * @throws {Error} When the filter is missing or invalid.
2287
+ * @throws {Error} When the update document is missing or invalid.
2288
+ */
2289
+ async findOneAndUpsertWithTransactionSecure(filter, update, user, ctx) {
2290
+ await this.init();
2291
+ if (!filter || typeof filter !== 'object') {
2292
+ throw new Error('Filter must be a valid object.');
2293
+ }
2294
+ if (!update || typeof update !== 'object') {
2295
+ throw new Error('Update must be a valid object.');
2296
+ }
2297
+ if (!user) {
2298
+ throw new Error('User is required.');
2299
+ }
2300
+ return this.runWithModelContext(ctx, async () => this.withTransaction(async () => {
2301
+ if (user.role !== 'admin') {
2302
+ throw new Error('Unauthorized');
2303
+ }
2304
+ const meta = {
2305
+ transactional: true,
2306
+ secure: true,
2307
+ userId: user.id,
2308
+ userRole: user.role
2309
+ };
2310
+ const collectionName = this.resolveCollectionName(ctx);
2311
+ const col = await this.getCollection(ctx);
2312
+ const session = this.resolveSession(ctx);
2313
+ const middlewareCtx = this.buildMiddlewareContext('findOneAndUpsert', ctx, {
2314
+ filter,
2315
+ update,
2316
+ meta
2317
+ });
2318
+ middlewareCtx.collectionName =
2319
+ middlewareCtx.collectionName ?? collectionName;
2320
+ await this.runBeforeMiddlewares('findOneAndUpsert', middlewareCtx);
2321
+ const effectiveFilter = (middlewareCtx.filter ?? filter);
2322
+ const effectiveUpdate = (middlewareCtx.update ?? update);
2323
+ middlewareCtx.filter = effectiveFilter;
2324
+ middlewareCtx.update = effectiveUpdate;
2325
+ await this.schema.executePre('updateOne', {
2326
+ operation: 'findOneAndUpsert',
2327
+ collectionName: middlewareCtx.collectionName,
2328
+ filter: effectiveFilter,
2329
+ update: effectiveUpdate,
2330
+ meta
2331
+ });
2332
+ const upsertResult = await col.findOneAndUpdate(effectiveFilter, effectiveUpdate, {
2333
+ upsert: true,
2334
+ returnDocument: 'after',
2335
+ ...(session ? { session } : {})
2336
+ });
2337
+ const updatedDoc = upsertResult &&
2338
+ typeof upsertResult === 'object' &&
2339
+ 'value' in upsertResult
2340
+ ? upsertResult.value
2341
+ : upsertResult;
2342
+ const payload = updatedDoc
2343
+ ? this.toModelResult(updatedDoc)
2344
+ : null;
2345
+ await this.schema.executePost('updateOne', {
2346
+ operation: 'findOneAndUpsert',
2347
+ collectionName: middlewareCtx.collectionName,
2348
+ filter: effectiveFilter,
2349
+ update: effectiveUpdate,
2350
+ result: updatedDoc,
2351
+ meta
2352
+ });
2353
+ middlewareCtx.result = payload;
2354
+ await this.runAfterMiddlewares('findOneAndUpsert', middlewareCtx);
2355
+ await this.invalidateCachePatterns('findOneAndUpsert', middlewareCtx);
2356
+ await pubsub.publish(`${utils_1.DB_CHANGE_EVENT}`, {
2357
+ documentUpdated: {
2358
+ action: 'findOneAndUpsert',
2359
+ filter: effectiveFilter,
2360
+ update: effectiveUpdate,
2361
+ meta
2362
+ }
2363
+ });
2364
+ return (middlewareCtx.result ?? payload);
2365
+ }, ctx));
2366
+ }
2367
+ /**
2368
+ * Deletes a single document matching the provided filter using an admin-only
2369
+ * secure operation.
2370
+ *
2371
+ * This method enforces authorization before executing the delete flow. It runs
2372
+ * inside the active Ambiten runtime context, is instrumented through
2373
+ * {@link measureQuery}, executes registered model middlewares, triggers schema
2374
+ * delete hooks with security metadata, invalidates relevant cache patterns, and
2375
+ * publishes a database change event after a successful deletion.
2376
+ *
2377
+ * @param filter - MongoDB filter used to identify the document to delete.
2378
+ * @param user - Authenticated user performing the operation. Must have the `admin` role.
2379
+ * @param ctx - Optional model execution context for tenant, database,
2380
+ * collection, session, and related runtime overrides.
2381
+ * @returns A promise that resolves when the operation completes.
2382
+ * @throws {Error} When the user is not authorized to perform the operation.
2383
+ * @throws {Error} When the filter is missing or invalid.
2384
+ * @throws {Error} When the user payload is missing.
2385
+ */
2386
+ async deleteSecure(filter, user, ctx) {
2387
+ await this.init();
2388
+ if (!filter || typeof filter !== 'object') {
2389
+ throw new Error('Filter must be a valid object.');
2390
+ }
2391
+ if (!user) {
2392
+ throw new Error('User is required.');
2393
+ }
2394
+ return this.runWithModelContext(ctx, async () => (0, instrumentation_1.measureQuery)({
2395
+ operation: 'deleteOne',
2396
+ collectionName: this.resolveCollectionName(ctx),
2397
+ filter
2398
+ }, async () => {
2399
+ if (user.role !== 'admin') {
2400
+ throw new Error('Unauthorized');
2401
+ }
2402
+ const meta = {
2403
+ secure: true,
2404
+ userId: user.id,
2405
+ userRole: user.role
2406
+ };
2407
+ const collectionName = this.resolveCollectionName(ctx);
2408
+ const col = await this.getCollection(ctx);
2409
+ const session = this.resolveSession(ctx);
2410
+ const middlewareCtx = this.buildMiddlewareContext('deleteOne', ctx, {
2411
+ filter,
2412
+ meta
2413
+ });
2414
+ middlewareCtx.collectionName =
2415
+ middlewareCtx.collectionName ?? collectionName;
2416
+ await this.runBeforeMiddlewares('deleteOne', middlewareCtx);
2417
+ const effectiveFilter = (middlewareCtx.filter ?? filter);
2418
+ middlewareCtx.filter = effectiveFilter;
2419
+ const doc = await col.findOne(effectiveFilter, session ? { session } : undefined);
2420
+ if (!doc) {
2421
+ middlewareCtx.result = null;
2422
+ await this.runAfterMiddlewares('deleteOne', middlewareCtx);
2423
+ return;
2424
+ }
2425
+ const payload = this.toModelResult(doc);
2426
+ middlewareCtx.result = payload;
2427
+ await this.schema.executePre('deleteOne', {
2428
+ operation: 'deleteOne',
2429
+ collectionName: middlewareCtx.collectionName,
2430
+ ...doc,
2431
+ meta
2432
+ });
2433
+ await col.deleteOne(effectiveFilter, session ? { session } : undefined);
2434
+ await this.schema.executePost('deleteOne', {
2435
+ operation: 'deleteOne',
2436
+ collectionName: middlewareCtx.collectionName,
2437
+ ...doc,
2438
+ meta
2439
+ });
2440
+ await this.runAfterMiddlewares('deleteOne', middlewareCtx);
2441
+ await this.invalidateCachePatterns('deleteOne', middlewareCtx);
2442
+ await pubsub.publish(`${utils_1.DB_CHANGE_EVENT}`, {
2443
+ documentDeleted: {
2444
+ action: 'deleteOne',
2445
+ filter: effectiveFilter,
2446
+ meta
2447
+ }
2448
+ });
2449
+ }));
2450
+ }
2451
+ /**
2452
+ * Executes an aggregation pipeline against the model collection.
2453
+ *
2454
+ * This operation runs inside the active Ambiten runtime context, is instrumented
2455
+ * through {@link measureQuery}, executes registered aggregate middlewares,
2456
+ * supports optional query-result caching, and publishes a database change event
2457
+ * after execution.
2458
+ *
2459
+ * When caching is enabled through {@link QueryOptions}, the aggregation result
2460
+ * is stored and reused using a tenant-aware cache key derived from the
2461
+ * collection, pipeline, options, and runtime context.
2462
+ *
2463
+ * @typeParam U - The aggregation result document shape.
2464
+ * @param pipeline - MongoDB aggregation pipeline stages.
2465
+ * @param options - Optional MongoDB aggregation options.
2466
+ * @param externalSession - Optional explicit client session to use for the aggregation.
2467
+ * @param ctx - Optional model execution context for tenant, database,
2468
+ * collection, session, and related runtime overrides.
2469
+ * @param queryOptions - Optional query-level features such as caching.
2470
+ * @returns The aggregation result array.
2471
+ * @throws {Error} When the pipeline is missing or invalid.
2472
+ */
2473
+ async aggregate(pipeline, options = {}, externalSession, ctx, queryOptions) {
2474
+ await this.init();
2475
+ if (!Array.isArray(pipeline)) {
2476
+ throw new Error('Pipeline must be a valid array.');
2477
+ }
2478
+ return this.runWithModelContext(ctx, async () => (0, instrumentation_1.measureQuery)({
2479
+ operation: 'aggregate',
2480
+ collectionName: this.resolveCollectionName(ctx),
2481
+ pipeline
2482
+ }, async () => {
2483
+ const collectionName = this.resolveCollectionName(ctx);
2484
+ const col = await this.getCollection(ctx);
2485
+ const resolvedSession = this.resolveSessionStrict(ctx, externalSession);
2486
+ const middlewareCtx = this.buildMiddlewareContext('aggregate', ctx, {
2487
+ pipeline
2488
+ });
2489
+ middlewareCtx.collectionName =
2490
+ middlewareCtx.collectionName ?? collectionName;
2491
+ await this.runBeforeMiddlewares('aggregate', middlewareCtx);
2492
+ const effectivePipeline = (middlewareCtx.pipeline ?? pipeline);
2493
+ middlewareCtx.pipeline = effectivePipeline;
2494
+ const cache = queryOptions?.cache
2495
+ ? this.resolveCache(queryOptions)
2496
+ : null;
2497
+ await this.schema.executePre('aggregate', {
2498
+ operation: 'aggregate',
2499
+ collectionName: middlewareCtx.collectionName,
2500
+ tenantId: middlewareCtx.tenantId,
2501
+ dbName: middlewareCtx.dbName,
2502
+ session: resolvedSession,
2503
+ pipeline: effectivePipeline,
2504
+ meta: middlewareCtx.meta
2505
+ });
2506
+ let cacheKey = null;
2507
+ if (cache) {
2508
+ cacheKey = this.buildCacheKey('aggregate', {
2509
+ collectionName: middlewareCtx.collectionName,
2510
+ pipeline: effectivePipeline,
2511
+ options,
2512
+ tenantId: middlewareCtx.tenantId,
2513
+ dbName: middlewareCtx.dbName
2514
+ });
2515
+ const cached = await cache.get(cacheKey, {
2516
+ ttlSeconds: typeof queryOptions?.cache === 'object'
2517
+ ? queryOptions.cache.ttl
2518
+ : undefined,
2519
+ tenantId: middlewareCtx.tenantId,
2520
+ namespace: middlewareCtx.collectionName
2521
+ });
2522
+ if (cached) {
2523
+ middlewareCtx.result = cached;
2524
+ await this.runAfterMiddlewares('aggregate', middlewareCtx);
2525
+ return (middlewareCtx.result ?? cached);
2526
+ }
2527
+ }
2528
+ const cursor = col.aggregate(effectivePipeline, {
2529
+ ...options,
2530
+ ...(resolvedSession ? { session: resolvedSession } : {})
2531
+ });
2532
+ const result = await cursor.toArray();
2533
+ await this.schema.executePost('aggregate', {
2534
+ operation: 'aggregate',
2535
+ collectionName: middlewareCtx.collectionName,
2536
+ tenantId: middlewareCtx.tenantId,
2537
+ dbName: middlewareCtx.dbName,
2538
+ session: resolvedSession,
2539
+ pipeline: effectivePipeline,
2540
+ result,
2541
+ meta: middlewareCtx.meta
2542
+ });
2543
+ middlewareCtx.result = result;
2544
+ await this.runAfterMiddlewares('aggregate', middlewareCtx);
2545
+ const finalResult = (middlewareCtx.result ?? result);
2546
+ if (cache && cacheKey) {
2547
+ await cache.set(cacheKey, finalResult, {
2548
+ ttlSeconds: typeof queryOptions?.cache === 'object'
2549
+ ? queryOptions.cache.ttl
2550
+ : undefined,
2551
+ tenantId: middlewareCtx.tenantId,
2552
+ namespace: middlewareCtx.collectionName
2553
+ });
2554
+ }
2555
+ await pubsub.publish(`${utils_1.DB_CHANGE_EVENT}`, {
2556
+ dbChange: {
2557
+ action: 'aggregate',
2558
+ pipeline: effectivePipeline
2559
+ }
2560
+ });
2561
+ return finalResult;
2562
+ }));
2563
+ }
2564
+ /**
2565
+ * Executes an aggregation pipeline inside a transaction-aware runtime context.
2566
+ *
2567
+ * This transactional variant runs within the active Ambiten runtime context,
2568
+ * executes registered aggregate middlewares, triggers schema aggregate hooks,
2569
+ * instruments the query through {@link measureQuery}, and publishes a database
2570
+ * change event after execution.
2571
+ *
2572
+ * @typeParam U - The aggregation result document shape.
2573
+ * @param pipeline - MongoDB aggregation pipeline stages.
2574
+ * @param options - Optional MongoDB aggregation options.
2575
+ * @param ctx - Optional model execution context for tenant, database,
2576
+ * collection, session, and transaction-aware runtime overrides.
2577
+ * @returns The aggregation result array.
2578
+ * @throws {Error} When the pipeline is missing or invalid.
2579
+ */
2580
+ async aggregateWithTransaction(pipeline, options = {}, ctx) {
2581
+ await this.init();
2582
+ if (!Array.isArray(pipeline)) {
2583
+ throw new Error('Pipeline must be a valid array.');
2584
+ }
2585
+ return this.runWithModelContext(ctx, async () => this.withTransaction(async () => (0, instrumentation_1.measureQuery)({
2586
+ operation: 'aggregate',
2587
+ collectionName: this.resolveCollectionName(ctx),
2588
+ pipeline
2589
+ }, async () => {
2590
+ const collectionName = this.resolveCollectionName(ctx);
2591
+ const col = await this.getCollection(ctx);
2592
+ const session = this.resolveSession(ctx);
2593
+ const middlewareCtx = this.buildMiddlewareContext('aggregate', ctx, {
2594
+ pipeline,
2595
+ meta: {
2596
+ transactional: true
2597
+ }
2598
+ });
2599
+ middlewareCtx.collectionName =
2600
+ middlewareCtx.collectionName ?? collectionName;
2601
+ await this.runBeforeMiddlewares('aggregate', middlewareCtx);
2602
+ const effectivePipeline = (middlewareCtx.pipeline ?? pipeline);
2603
+ middlewareCtx.pipeline = effectivePipeline;
2604
+ await this.schema.executePre('aggregate', {
2605
+ operation: 'aggregate',
2606
+ collectionName: middlewareCtx.collectionName,
2607
+ tenantId: middlewareCtx.tenantId,
2608
+ dbName: middlewareCtx.dbName,
2609
+ session,
2610
+ pipeline: effectivePipeline,
2611
+ meta: {
2612
+ ...(middlewareCtx.meta ?? {}),
2613
+ transactional: true
2614
+ }
2615
+ });
2616
+ const cursor = col.aggregate(effectivePipeline, {
2617
+ ...options,
2618
+ ...(session ? { session } : {})
2619
+ });
2620
+ const result = await cursor.toArray();
2621
+ await this.schema.executePost('aggregate', {
2622
+ operation: 'aggregate',
2623
+ collectionName: middlewareCtx.collectionName,
2624
+ tenantId: middlewareCtx.tenantId,
2625
+ dbName: middlewareCtx.dbName,
2626
+ session,
2627
+ pipeline: effectivePipeline,
2628
+ result,
2629
+ meta: {
2630
+ ...(middlewareCtx.meta ?? {}),
2631
+ transactional: true
2632
+ }
2633
+ });
2634
+ middlewareCtx.result = result;
2635
+ await this.runAfterMiddlewares('aggregate', middlewareCtx);
2636
+ const finalResult = (middlewareCtx.result ?? result);
2637
+ await pubsub.publish(`${utils_1.DB_CHANGE_EVENT}`, {
2638
+ dbChange: {
2639
+ action: 'aggregate',
2640
+ pipeline: effectivePipeline,
2641
+ meta: {
2642
+ transactional: true
2643
+ }
2644
+ }
2645
+ });
2646
+ return finalResult;
2647
+ }), ctx));
2648
+ }
2649
+ /**
2650
+ * Executes the provided operation within a transaction-aware runtime context.
2651
+ *
2652
+ * Resolution order:
2653
+ * 1. Reuse an existing session from the merged model context when available.
2654
+ * 2. Start a provider-managed session when the active database provider
2655
+ * exposes a session factory.
2656
+ * 3. Fall back to {@link AmbitenContext.withTransaction} to resolve a client
2657
+ * and execute the operation inside a transaction.
2658
+ *
2659
+ * When an existing session is supplied, the current transaction boundary is
2660
+ * reused and no new transaction is started.
2661
+ *
2662
+ * @typeParam R - The operation result type.
2663
+ * @param operation - Callback executed with the resolved MongoDB client session.
2664
+ * @param ctx - Optional model execution context used to resolve tenant,
2665
+ * database, collection, and session overrides.
2666
+ * @returns The resolved operation result.
2667
+ */
2668
+ async withTransaction(operation, ctx) {
2669
+ const baseCtx = this.mergeCtx(ctx) ?? {};
2670
+ const collectionName = baseCtx.collectionName ?? this._collectionName;
2671
+ const existingSession = baseCtx.session;
2672
+ const runtimeBase = {
2673
+ tenantId: baseCtx.tenantId,
2674
+ dbName: baseCtx.dbName,
2675
+ collectionName
2676
+ };
2677
+ if (existingSession) {
2678
+ return context_1.AmbitenContext.run({
2679
+ ...runtimeBase,
2680
+ session: existingSession
2681
+ }, async () => operation(existingSession));
2682
+ }
2683
+ if (this._provider?.startSession) {
2684
+ const session = await this.getSession(baseCtx);
2685
+ return await context_1.AmbitenContext.run({
2686
+ ...runtimeBase,
2687
+ session
2688
+ }, async () => (0, context_1.runManualTransaction)(session, operation));
2689
+ }
2690
+ return context_1.AmbitenContext.run(runtimeBase, async () => context_1.AmbitenContext.withTransaction(operation));
2691
+ }
2692
+ /**
2693
+ * Executes the provided operation inside a transaction-aware Ambiten runtime context.
2694
+ *
2695
+ * This helper ensures the operation runs with a resolved MongoDB client session
2696
+ * and within the active Ambiten runtime context for the current tenant,
2697
+ * database, and collection. If a session already exists in the merged model
2698
+ * context, that session is reused and no new transaction boundary is created.
2699
+ *
2700
+ * Use this method when multiple model operations must be executed atomically
2701
+ * within a single transactional unit of work.
2702
+ *
2703
+ * @typeParam R - The operation result type.
2704
+ * @param operation - Callback executed with the resolved MongoDB client session.
2705
+ * @param ctx - Optional model execution context for tenant, database,
2706
+ * collection, and session overrides.
2707
+ * @returns The resolved result of the transactional operation.
2708
+ */
2709
+ async runInTransaction(operation, ctx) {
2710
+ await this.init();
2711
+ if (typeof operation !== 'function') {
2712
+ throw new Error('Operation must be a valid function.');
2713
+ }
2714
+ return this.runWithModelContext(ctx, async () => this.withTransaction(async (session) => operation(session), ctx));
2715
+ }
2716
+ async getSession(ctx) {
2717
+ if (!this._provider?.startSession) {
2718
+ throw new Error('Transaction support is not available. Provider does not implement startSession().');
2719
+ }
2720
+ return this._provider.startSession(this.mergeCtx(ctx));
2721
+ }
2722
+ /**
2723
+ * Creates a readable stream for an aggregation pipeline.
2724
+ *
2725
+ * This method runs inside the active Ambiten runtime context, is instrumented
2726
+ * through {@link measureQuery}, executes registered aggregate middlewares,
2727
+ * triggers schema aggregate hooks, and publishes a database change event after
2728
+ * the aggregation stream is created.
2729
+ *
2730
+ * This method instruments stream creation only. It does not track the full
2731
+ * lifecycle of stream consumption unless additional listeners are attached by
2732
+ * the caller or by a higher-level observability layer.
2733
+ *
2734
+ * @typeParam U - The aggregation result document shape.
2735
+ * @param pipeline - MongoDB aggregation pipeline stages.
2736
+ * @param options - Optional MongoDB aggregation options.
2737
+ * @param ctx - Optional model execution context for tenant, database,
2738
+ * collection, session, and related runtime overrides.
2739
+ * @returns A readable stream for the aggregation result set.
2740
+ * @throws {Error} When the pipeline is missing or invalid.
2741
+ */
2742
+ async streamAggregation(pipeline, options = {}, ctx) {
2743
+ await this.init();
2744
+ if (!Array.isArray(pipeline)) {
2745
+ throw new Error('Pipeline must be a valid array.');
2746
+ }
2747
+ return this.runWithModelContext(ctx, async () => (0, instrumentation_1.measureQuery)({
2748
+ operation: 'aggregate',
2749
+ collectionName: this.resolveCollectionName(ctx),
2750
+ pipeline,
2751
+ extra: {
2752
+ streaming: true
2753
+ }
2754
+ }, async () => {
2755
+ const collectionName = this.resolveCollectionName(ctx);
2756
+ const session = this.resolveSession(ctx);
2757
+ const col = await this.getCollection(ctx);
2758
+ const middlewareCtx = this.buildMiddlewareContext('aggregate', ctx, {
2759
+ pipeline,
2760
+ meta: {
2761
+ streaming: true
2762
+ }
2763
+ });
2764
+ middlewareCtx.collectionName =
2765
+ middlewareCtx.collectionName ?? collectionName;
2766
+ await this.runBeforeMiddlewares('aggregate', middlewareCtx);
2767
+ const effectivePipeline = (middlewareCtx.pipeline ?? pipeline);
2768
+ middlewareCtx.pipeline = effectivePipeline;
2769
+ const meta = {
2770
+ ...(middlewareCtx.meta ?? {}),
2771
+ streaming: true
2772
+ };
2773
+ await this.schema.triggerMiddleware('pre', 'aggregate', {
2774
+ operation: 'aggregate',
2775
+ collectionName: middlewareCtx.collectionName,
2776
+ pipeline: effectivePipeline,
2777
+ meta
2778
+ });
2779
+ await this.schema.executePre('aggregate', {
2780
+ operation: 'aggregate',
2781
+ collectionName: middlewareCtx.collectionName,
2782
+ tenantId: middlewareCtx.tenantId,
2783
+ dbName: middlewareCtx.dbName,
2784
+ session,
2785
+ pipeline: effectivePipeline,
2786
+ meta
2787
+ });
2788
+ const stream = col.aggregate(effectivePipeline, {
2789
+ ...options,
2790
+ ...(session ? { session } : {})
2791
+ }).stream();
2792
+ await this.schema.triggerMiddleware('post', 'aggregate', {
2793
+ operation: 'aggregate',
2794
+ collectionName: middlewareCtx.collectionName,
2795
+ result: stream,
2796
+ meta
2797
+ });
2798
+ await this.schema.executePost('aggregate', {
2799
+ operation: 'aggregate',
2800
+ collectionName: middlewareCtx.collectionName,
2801
+ tenantId: middlewareCtx.tenantId,
2802
+ dbName: middlewareCtx.dbName,
2803
+ session,
2804
+ pipeline: effectivePipeline,
2805
+ result: stream,
2806
+ meta
2807
+ });
2808
+ middlewareCtx.result = stream;
2809
+ await this.runAfterMiddlewares('aggregate', middlewareCtx);
2810
+ await pubsub.publish(`${utils_1.DB_CHANGE_EVENT}`, {
2811
+ dbChange: {
2812
+ action: 'aggregateStream',
2813
+ collectionName: middlewareCtx.collectionName,
2814
+ pipeline: effectivePipeline,
2815
+ meta
2816
+ }
2817
+ });
2818
+ return stream;
2819
+ }));
2820
+ }
2821
+ /**
2822
+ * Aggregates documents in the collection using a pipeline with caching.
2823
+ * @param {object[]} pipeline - The aggregation pipeline.
2824
+ * @param {string} cacheKey - The cache key.
2825
+ * @param {number} [cacheDuration=300] - The cache duration in seconds.
2826
+ * @returns {Promise<T[]>} The aggregation result as an array.
2827
+ * @throws {Error} If the pipeline is not valid or the cache key is not a string.
2828
+ */
2829
+ async aggregateWithCache(pipeline, cacheKey, cacheDuration = 300) {
2830
+ await this.init();
2831
+ if (!Array.isArray(pipeline)) {
2832
+ throw new Error('Pipeline must be an array of objects.');
2833
+ }
2834
+ const cachedResult = await redis_manager_1.redis.get(cacheKey);
2835
+ if (cachedResult) {
2836
+ console.info(`[info]: Cache hit: ${cacheKey}`);
2837
+ return JSON.parse(cachedResult);
2838
+ }
2839
+ else if (typeof cacheKey !== 'string') {
2840
+ throw new Error('Cache key must be a string.');
2841
+ }
2842
+ else {
2843
+ console.info(`[info]: Cache miss: ${cacheKey}`);
2844
+ }
2845
+ const col = await this.getCollection();
2846
+ const result = await col.aggregate(pipeline).toArray();
2847
+ await redis_manager_1.redis.set(cacheKey, JSON.stringify(result) || '');
2848
+ await redis_manager_1.redis.expire(cacheKey, cacheDuration);
2849
+ console.info('Cache set:', cacheKey);
2850
+ this.schema.triggerMiddleware('post', 'aggregate', {
2851
+ operation: 'aggregate',
2852
+ collectionName: this.resolveCollectionName(col),
2853
+ result: [result]
2854
+ });
2855
+ await pubsub.publish("DB_CHANGE", { dbChange: { action: "aggregate", result } });
2856
+ return result || [];
2857
+ }
2858
+ /**
2859
+ * Cursor-based pagination using _id comparison instead of skip.
2860
+ * @param filter - The filter for documents.
2861
+ * @param pageSize - Number of documents per page.
2862
+ * @param lastId - Last document _id from the previous page.
2863
+ * @returns {Promise<T[]>} Array of documents for the current page.
2864
+ * This method uses the _id field for pagination, which is more efficient than using skip.
2865
+ *
2866
+ */
2867
+ async paginatedFind(filter, pageSize, lastId) {
2868
+ await this.init();
2869
+ const query = filter;
2870
+ if (lastId) {
2871
+ query._id = { $gt: new mongodb_1.ObjectId(lastId) }; // Fetch only newer documents
2872
+ }
2873
+ const col = await this.getCollection();
2874
+ const results = await col.find(query).limit(pageSize).toArray();
2875
+ return results.map(({ _id, ...rest }) => rest);
2876
+ }
2877
+ ;
2878
+ /**
2879
+ * Opens a MongoDB change stream on the model collection and registers a change
2880
+ * callback.
2881
+ *
2882
+ * This helper initializes the model, resolves the collection within the active
2883
+ * Ambiten runtime context, creates a change stream, and subscribes the provided
2884
+ * callback to `"change"` events.
2885
+ *
2886
+ * The method is intentionally lightweight: it manages change stream creation,
2887
+ * but does not yet instrument the full lifecycle of emitted change events. That
2888
+ * can be added later by higher-level observability or evidence collection
2889
+ * layers.
2890
+ *
2891
+ * @param callback - Function invoked whenever a change event is emitted by the collection change stream.
2892
+ * @param ctx - Optional model execution context for tenant, database,
2893
+ * collection, session, and related runtime overrides.
2894
+ * @returns The active MongoDB change stream.
2895
+ * @throws {Error} When the callback is not a valid function.
2896
+ */
2897
+ async watchChanges(callback, ctx) {
2898
+ await this.init();
2899
+ if (typeof callback !== 'function') {
2900
+ throw new Error('Callback must be a valid function.');
2901
+ }
2902
+ return this.runWithModelContext(ctx, async () => {
2903
+ const collectionName = this.resolveCollectionName(ctx);
2904
+ const col = await this.getCollection(ctx);
2905
+ const changeStream = col.watch();
2906
+ changeStream.on('change', callback);
2907
+ await pubsub.publish(`${utils_1.DB_CHANGE_EVENT}`, {
2908
+ dbChange: {
2909
+ action: 'watchChanges',
2910
+ collectionName
2911
+ }
2912
+ });
2913
+ return changeStream;
2914
+ });
2915
+ }
2916
+ /**
2917
+ * Creates an index on the model collection.
2918
+ *
2919
+ * @param fields - Index field specification where each key maps to ascending
2920
+ * (`1`) or descending (`-1`) index order.
2921
+ * @param ctx - Optional model execution context for tenant, database,
2922
+ * collection, session, and related runtime overrides.
2923
+ * @returns A promise that resolves when the index has been created.
2924
+ * @throws {Error} When the field specification is missing or invalid.
2925
+ */
2926
+ async createIndex(fields, ctx) {
2927
+ await this.init();
2928
+ if (!fields || typeof fields !== 'object') {
2929
+ throw new Error('Index fields must be a valid object.');
2930
+ }
2931
+ const entries = Object.entries(fields);
2932
+ if (entries.length === 0) {
2933
+ throw new Error('Index fields must contain at least one entry.');
2934
+ }
2935
+ for (const [, direction] of entries) {
2936
+ if (direction !== 1 && direction !== -1) {
2937
+ throw new Error('Index direction must be either 1 or -1.');
2938
+ }
2939
+ }
2940
+ return this.runWithModelContext(ctx, async () => {
2941
+ const col = await this.getCollection(ctx);
2942
+ await col.createIndex(fields);
2943
+ });
2944
+ }
2945
+ /**
2946
+ * Drops an index from the collection by its name.
2947
+ * @param {string} indexName - The name of the index to drop.
2948
+ * @returns {Promise<void>} Resolves when the index is dropped.
2949
+ */
2950
+ async dropIndex(indexName) {
2951
+ await this.init();
2952
+ const col = await this.getCollection();
2953
+ await col.dropIndex(indexName);
2954
+ }
2955
+ // ============== Invalidation ==============//
2956
+ async invalidateCachePatterns(operation, ctx) {
2957
+ const tenantId = ctx.tenantId ?? 'default';
2958
+ const dbName = ctx.dbName ?? 'default';
2959
+ const collectionName = ctx.collectionName ?? this._collectionName;
2960
+ const basePatterns = [
2961
+ `Ambiten:${tenantId}:${dbName}:${collectionName}:find:*`,
2962
+ `Ambiten:${tenantId}:${dbName}:${collectionName}:findOne:*`
2963
+ ];
2964
+ const patterns = operation === 'aggregate'
2965
+ ? [
2966
+ ...basePatterns,
2967
+ `Ambiten:${tenantId}:${dbName}:${collectionName}:aggregate:*`
2968
+ ]
2969
+ : [
2970
+ ...basePatterns,
2971
+ `Ambiten:${tenantId}:${dbName}:${collectionName}:aggregate:*`
2972
+ ];
2973
+ await Promise.all([...new Set(patterns)].map((pattern) => AmbitenModel.invalidatePattern(pattern)));
2974
+ }
2975
+ /**
2976
+ * Invalidates all cache entries matching the provided Redis key pattern.
2977
+ *
2978
+ * This method uses Redis `SCAN` to iterate over matching keys in batches and
2979
+ * deletes them using a pipelined multi-operation for better efficiency.
2980
+ *
2981
+ * @param pattern - Redis key pattern to invalidate.
2982
+ * @returns The number of successfully deleted keys.
2983
+ * @throws {Error} When the pattern is invalid or cache invalidation fails.
2984
+ */
2985
+ static async invalidatePattern(pattern) {
2986
+ if (typeof pattern !== 'string' || pattern.trim().length === 0) {
2987
+ throw new Error('Pattern must be a non-empty string.');
2988
+ }
2989
+ const normalizedPattern = pattern.trim();
2990
+ const scanCount = 100;
2991
+ let deletedCount = 0;
2992
+ let cursor = 0;
2993
+ try {
2994
+ do {
2995
+ const result = await redis_manager_1.redis.scan(cursor, {
2996
+ MATCH: normalizedPattern,
2997
+ COUNT: scanCount
2998
+ });
2999
+ const nextCursor = Array.isArray(result) ? Number(result[0]) : 0;
3000
+ const keys = Array.isArray(result) ? result[1] : [];
3001
+ if (Array.isArray(keys) && keys.length > 0) {
3002
+ const multi = redis_manager_1.redis.multi();
3003
+ for (const key of keys) {
3004
+ multi.del(key);
3005
+ }
3006
+ const execResult = await multi.exec();
3007
+ if (Array.isArray(execResult)) {
3008
+ deletedCount += execResult.filter((entry) => {
3009
+ if (Array.isArray(entry)) {
3010
+ const [err, value] = entry;
3011
+ return !err && value === 1;
3012
+ }
3013
+ return entry === 1;
3014
+ }).length;
3015
+ }
3016
+ }
3017
+ cursor = nextCursor;
3018
+ } while (cursor !== 0);
3019
+ return deletedCount;
3020
+ }
3021
+ catch (error) {
3022
+ const message = error instanceof Error ? error.message : String(error);
3023
+ throw new Error(`Failed to invalidate cache pattern "${normalizedPattern}": ${message}`);
3024
+ }
3025
+ }
3026
+ getContext() {
3027
+ return { ctx: {} };
3028
+ }
3029
+ /**
3030
+ * Invalidates cache entries for the current model scope using a pattern.
3031
+ *
3032
+ * This helper resolves tenant, database, and collection scope from the
3033
+ * provided context and applies the pattern within the Ambiten cache namespace.
3034
+ *
3035
+ * @param pattern - Partial cache pattern (e.g. "find:*", "aggregate:*").
3036
+ * @param ctx - Optional model execution context.
3037
+ * @returns Number of deleted cache entries.
3038
+ * @throws {Error} When the pattern is invalid.
3039
+ */
3040
+ async invalidateModelPattern(pattern, ctx) {
3041
+ await this.init();
3042
+ if (typeof pattern !== 'string' || pattern.trim().length === 0) {
3043
+ throw new Error('Pattern must be a non-empty string.');
3044
+ }
3045
+ return this.runWithModelContext(ctx, async () => {
3046
+ const tenantId = ctx?.tenantId ?? 'default';
3047
+ const dbName = ctx?.dbName ?? 'default';
3048
+ const collectionName = ctx?.collectionName ?? this._collectionName;
3049
+ const fullPattern = `Ambiten:${tenantId}:${dbName}:${collectionName}:${pattern.trim()}`;
3050
+ return AmbitenModel.invalidatePattern(fullPattern);
3051
+ });
3052
+ }
3053
+ /**
3054
+ * Invalidates cache entries related to a specific document within the current
3055
+ * model scope.
3056
+ *
3057
+ * This helper is useful only when document-specific cache keys are part of the
3058
+ * active cache strategy.
3059
+ *
3060
+ * @param doc - The document whose cache entries should be invalidated.
3061
+ * @param ctx - Optional model execution context.
3062
+ * @returns A promise that resolves when invalidation completes.
3063
+ * @throws {Error} When the document is missing a valid `_id`.
3064
+ */
3065
+ async invalidateDocumentCache(doc, ctx) {
3066
+ await this.init();
3067
+ if (!doc || typeof doc !== 'object' || !('_id' in doc) || !doc._id) {
3068
+ throw new Error('Document must contain a valid _id.');
3069
+ }
3070
+ return this.runWithModelContext(ctx, async () => {
3071
+ const merged = this.mergeCtx(ctx);
3072
+ const tenantId = merged?.tenantId ?? 'default';
3073
+ const dbName = merged?.dbName ?? 'default';
3074
+ const collectionName = merged?.collectionName ?? this._collectionName;
3075
+ const pattern = `Ambiten:${tenantId}:${dbName}:${collectionName}:*${String(doc._id)}*`;
3076
+ return AmbitenModel.invalidatePattern(pattern);
3077
+ });
3078
+ }
3079
+ /**
3080
+ * Returns cache statistics for the active Redis cache backend.
3081
+ *
3082
+ * This helper retrieves Redis memory and keyspace statistics, along with Ambiten
3083
+ * cache hit/miss counters. When a tenant identifier is provided, it also counts
3084
+ * cache keys scoped to that tenant within the Ambiten cache namespace.
3085
+ *
3086
+ * @param tenantId - Optional tenant identifier used to count tenant-scoped cache keys.
3087
+ * @returns Cache statistics for the current Redis backend.
3088
+ * @throws {Error} When cache statistics cannot be retrieved.
3089
+ */
3090
+ static async getCacheStats(tenantId) {
3091
+ try {
3092
+ const memoryInfo = await redis_manager_1.redis.info('memory');
3093
+ const keyspaceInfo = await redis_manager_1.redis.info('keyspace');
3094
+ const matches = Array.from(keyspaceInfo.matchAll(/db\d+:keys=(\d+)/g));
3095
+ const totalKeys = matches.reduce((sum, match) => {
3096
+ return sum + Number(match[1] ?? 0);
3097
+ }, 0);
3098
+ const memoryMatch = memoryInfo.match(/used_memory:(\d+)/);
3099
+ const memoryUsageBytes = memoryMatch ? Number(memoryMatch[1]) : 0;
3100
+ const hitsRaw = (await redis_manager_1.redis.get('Ambiten:cache:stats:hits')) ?? '0';
3101
+ const missesRaw = (await redis_manager_1.redis.get('Ambiten:cache:stats:misses')) ?? '0';
3102
+ const totalHits = Number.parseInt(hitsRaw, 10) || 0;
3103
+ const totalMisses = Number.parseInt(missesRaw, 10) || 0;
3104
+ const totalRequests = totalHits + totalMisses;
3105
+ const hitRate = totalRequests > 0
3106
+ ? Math.round((totalHits / totalRequests) * 100)
3107
+ : 0;
3108
+ const missRate = totalRequests > 0
3109
+ ? Math.round((totalMisses / totalRequests) * 100)
3110
+ : 0;
3111
+ let tenantKeys;
3112
+ if (tenantId) {
3113
+ const pattern = `Ambiten:${tenantId}:*`;
3114
+ let cursor = 0;
3115
+ let count = 0;
3116
+ do {
3117
+ const result = await redis_manager_1.redis.scan(cursor, {
3118
+ MATCH: pattern,
3119
+ COUNT: 100
3120
+ });
3121
+ const nextCursor = Array.isArray(result) ? Number(result[0]) : 0;
3122
+ const keys = Array.isArray(result) ? result[1] : [];
3123
+ if (Array.isArray(keys)) {
3124
+ count += keys.length;
3125
+ }
3126
+ cursor = nextCursor;
3127
+ } while (cursor !== 0);
3128
+ tenantKeys = count;
3129
+ }
3130
+ return {
3131
+ totalKeys,
3132
+ tenantKeys,
3133
+ memoryUsageMb: Math.round(memoryUsageBytes / 1024 / 1024),
3134
+ memoryUsageBytes,
3135
+ hitRate,
3136
+ missRate,
3137
+ totalHits,
3138
+ totalMisses,
3139
+ totalRequests,
3140
+ tenantId: tenantId ?? null,
3141
+ timestamp: new Date().toISOString()
3142
+ };
3143
+ }
3144
+ catch (error) {
3145
+ const message = error instanceof Error ? error.message : String(error);
3146
+ throw new Error(`Failed to get cache stats: ${message}`);
3147
+ }
3148
+ }
3149
+ /**
3150
+ * Runs a custom command on the collection.
3151
+ * @param {string} command - The command to run.
3152
+ * @param {...any} args - The arguments for the command.
3153
+ * @returns {Promise<any>} The result of the command.
3154
+ */
3155
+ async runCommand(command, ...args) {
3156
+ await this.init();
3157
+ const dbCommand = { [command]: 1, ...args };
3158
+ const col = await this.getCollection();
3159
+ return col.db.command(dbCommand);
3160
+ }
3161
+ /**
3162
+ * Runs garbage collection for expired documents in the model collection.
3163
+ *
3164
+ * This operation uses the model GC configuration to locate expired documents
3165
+ * and either soft-delete them or permanently delete them. When configured,
3166
+ * expired documents may be archived before deletion.
3167
+ *
3168
+ * The operation runs inside the active Ambiten runtime context, is instrumented
3169
+ * through {@link measureQuery}, executes registered GC-related middlewares,
3170
+ * triggers schema update/delete hooks as appropriate, invalidates relevant
3171
+ * cache patterns, and publishes a garbage collection event after completion.
3172
+ *
3173
+ * @param ctx - Optional model execution context for tenant, database,
3174
+ * collection, session, and related runtime overrides.
3175
+ * @returns A promise that resolves when garbage collection completes.
3176
+ */
3177
+ async runGC(ctx) {
3178
+ await this.init();
3179
+ const config = this.resolveSchemaGCConfig();
3180
+ if (!config || !config.ttlField || !config.expiresIn) {
3181
+ return;
3182
+ }
3183
+ return this.runWithModelContext(ctx, async () => (0, instrumentation_1.measureQuery)({
3184
+ operation: 'runGC',
3185
+ collectionName: this.resolveCollectionName(ctx)
3186
+ }, async () => {
3187
+ const collectionName = this.resolveCollectionName(ctx);
3188
+ const session = this.resolveSession(ctx);
3189
+ const col = await this.getCollection(ctx);
3190
+ const meta = {
3191
+ gc: true
3192
+ };
3193
+ const middlewareCtx = this.buildMiddlewareContext('runGC', ctx, { meta });
3194
+ middlewareCtx.collectionName =
3195
+ middlewareCtx.collectionName ?? collectionName;
3196
+ await this.runBeforeMiddlewares('runGC', middlewareCtx);
3197
+ const expireDate = new Date(Date.now() - Number(config.expiresIn));
3198
+ const gcFilter = {
3199
+ [config.ttlField]: { $lt: expireDate }
3200
+ };
3201
+ const effectiveFilter = (middlewareCtx.filter ?? gcFilter);
3202
+ middlewareCtx.filter = effectiveFilter;
3203
+ const expiredDocs = await col.find(effectiveFilter, session ? { session } : undefined).toArray();
3204
+ if (expiredDocs.length === 0) {
3205
+ middlewareCtx.result = {
3206
+ scanned: 0,
3207
+ affected: 0,
3208
+ archived: 0,
3209
+ deleted: 0,
3210
+ softDeleted: 0
3211
+ };
3212
+ await this.runAfterMiddlewares('runGC', middlewareCtx);
3213
+ return;
3214
+ }
3215
+ let archived = 0;
3216
+ let deleted = 0;
3217
+ let softDeleted = 0;
3218
+ if (config.softDelete) {
3219
+ const softDeleteUpdate = {
3220
+ $set: {
3221
+ deletedAt: new Date(),
3222
+ isDeleted: true
3223
+ }
3224
+ };
3225
+ await this.schema.executePre('updateMany', {
3226
+ operation: 'runGC',
3227
+ collectionName: middlewareCtx.collectionName,
3228
+ tenantId: middlewareCtx.tenantId,
3229
+ dbName: middlewareCtx.dbName,
3230
+ session,
3231
+ filter: effectiveFilter,
3232
+ update: softDeleteUpdate,
3233
+ docs: expiredDocs,
3234
+ meta: {
3235
+ ...(middlewareCtx.meta ?? {}),
3236
+ gc: true,
3237
+ softDelete: true
3238
+ }
3239
+ });
3240
+ const result = await col.updateMany(effectiveFilter, softDeleteUpdate, session ? { session } : undefined);
3241
+ softDeleted = result.modifiedCount;
3242
+ await this.schema.executePost('updateMany', {
3243
+ operation: 'runGC',
3244
+ collectionName: middlewareCtx.collectionName,
3245
+ tenantId: middlewareCtx.tenantId,
3246
+ dbName: middlewareCtx.dbName,
3247
+ session,
3248
+ filter: effectiveFilter,
3249
+ update: softDeleteUpdate,
3250
+ docs: expiredDocs,
3251
+ result: {
3252
+ scanned: expiredDocs.length,
3253
+ affected: softDeleted,
3254
+ archived: 0,
3255
+ deleted: 0,
3256
+ softDeleted
3257
+ },
3258
+ meta: {
3259
+ ...(middlewareCtx.meta ?? {}),
3260
+ gc: true,
3261
+ softDelete: true
3262
+ }
3263
+ });
3264
+ }
3265
+ else {
3266
+ if (config.archiveBeforeDelete) {
3267
+ const archiveDocs = expiredDocs.map((doc) => ({
3268
+ ...doc,
3269
+ _archivedAt: new Date(),
3270
+ _from: col.collectionName
3271
+ }));
3272
+ await col.db.collection('Ambiten_archives').insertMany(archiveDocs, session ? { session } : undefined);
3273
+ archived = archiveDocs.length;
3274
+ }
3275
+ await this.schema.triggerMiddleware('pre', 'deleteMany', {
3276
+ operation: 'runGC',
3277
+ collectionName: middlewareCtx.collectionName,
3278
+ docs: expiredDocs
3279
+ });
3280
+ await this.schema.executePre('deleteMany', {
3281
+ operation: 'runGC',
3282
+ collectionName: middlewareCtx.collectionName,
3283
+ tenantId: middlewareCtx.tenantId,
3284
+ dbName: middlewareCtx.dbName,
3285
+ session,
3286
+ docs: expiredDocs,
3287
+ result: this.toModelResults(expiredDocs),
3288
+ meta: {
3289
+ ...(middlewareCtx.meta ?? {}),
3290
+ gc: true
3291
+ }
3292
+ });
3293
+ const result = await col.deleteMany(effectiveFilter, session ? { session } : undefined);
3294
+ deleted = result.deletedCount ?? 0;
3295
+ await this.schema.triggerMiddleware('post', 'deleteMany', {
3296
+ operation: 'runGC',
3297
+ collectionName: middlewareCtx.collectionName,
3298
+ docs: expiredDocs
3299
+ });
3300
+ await this.schema.executePost('deleteMany', {
3301
+ operation: 'runGC',
3302
+ collectionName: middlewareCtx.collectionName,
3303
+ tenantId: middlewareCtx.tenantId,
3304
+ dbName: middlewareCtx.dbName,
3305
+ session,
3306
+ docs: expiredDocs,
3307
+ result: {
3308
+ scanned: expiredDocs.length,
3309
+ affected: deleted,
3310
+ archived,
3311
+ deleted,
3312
+ softDeleted: 0
3313
+ },
3314
+ meta: {
3315
+ ...(middlewareCtx.meta ?? {}),
3316
+ gc: true
3317
+ }
3318
+ });
3319
+ }
3320
+ const gcResult = {
3321
+ scanned: expiredDocs.length,
3322
+ affected: softDeleted || deleted,
3323
+ archived,
3324
+ deleted,
3325
+ softDeleted
3326
+ };
3327
+ middlewareCtx.result = gcResult;
3328
+ await this.runAfterMiddlewares('runGC', middlewareCtx);
3329
+ await this.invalidateCachePatterns('runGC', middlewareCtx);
3330
+ await pubsub.publish(`${utils_1.DB_CHANGE_EVENT}`, {
3331
+ garbageCollection: {
3332
+ action: 'runGC',
3333
+ collectionName: middlewareCtx.collectionName,
3334
+ ...gcResult,
3335
+ meta: {
3336
+ ...(middlewareCtx.meta ?? {}),
3337
+ gc: true
3338
+ }
3339
+ }
3340
+ });
3341
+ }));
3342
+ }
3343
+ /**
3344
+ * Starts the automatic garbage collection process.
3345
+ * @param {number} intervalMs - The interval in milliseconds for the garbage collection to run.
3346
+ */
3347
+ startAutoGC(intervalMs = 3600000) {
3348
+ setInterval(() => this.runGC().catch(console.error), intervalMs);
3349
+ }
3350
+ /**
3351
+ * Resolves instrumentation context for model operations by combining explicit
3352
+ * model context with the active runtime context.
3353
+ *
3354
+ * This helper is used to provide stable metadata for logging, observers,
3355
+ * tracing, and query instrumentation.
3356
+ *
3357
+ * @param ctx - Optional model execution context.
3358
+ * @returns Resolved instrumentation metadata for the current operation.
3359
+ */
3360
+ resolveInstrumentationContext(ctx) {
3361
+ const runtimeCtx = context_1.AmbitenContext.get();
3362
+ const merged = this.mergeCtx(ctx);
3363
+ return {
3364
+ tenantId: merged?.tenantId,
3365
+ dbName: merged?.dbName,
3366
+ collectionName: merged?.collectionName ?? this._collectionName,
3367
+ session: merged?.session,
3368
+ requestId: runtimeCtx.requestId
3369
+ };
3370
+ }
3371
+ /**
3372
+ * Builds normalized operation metadata by merging existing middleware metadata
3373
+ * with operation-specific overrides.
3374
+ *
3375
+ * @param base - Existing operation metadata.
3376
+ * @param overrides - Additional metadata flags or values to apply.
3377
+ * @returns Normalized operation metadata.
3378
+ */
3379
+ buildOperationMeta(base, overrides) {
3380
+ return {
3381
+ ...(base ?? {}),
3382
+ ...(overrides ?? {}),
3383
+ extra: {
3384
+ ...(base?.extra ?? {}),
3385
+ ...(overrides?.extra ?? {})
3386
+ }
3387
+ };
3388
+ }
3389
+ /**
3390
+ * Builds a normalized hook payload for schema middleware and instrumentation.
3391
+ *
3392
+ * @param operation - Operation name.
3393
+ * @param ctx - Optional model execution context.
3394
+ * @param payload - Additional hook payload fields.
3395
+ * @returns Normalized hook payload.
3396
+ */
3397
+ buildHookPayload(operation, ctx, payload = {}) {
3398
+ const instrumentation = this.resolveInstrumentationContext(ctx);
3399
+ return {
3400
+ operation,
3401
+ collectionName: payload.collectionName ?? instrumentation.collectionName,
3402
+ tenantId: payload.tenantId ?? instrumentation.tenantId,
3403
+ dbName: payload.dbName ?? instrumentation.dbName,
3404
+ session: payload.session ?? instrumentation.session,
3405
+ filter: payload.filter,
3406
+ update: payload.update,
3407
+ doc: payload.doc,
3408
+ docs: payload.docs,
3409
+ pipeline: payload.pipeline,
3410
+ result: payload.result,
3411
+ meta: payload.meta
3412
+ };
3413
+ }
3414
+ //=============== Cache ====================//
3415
+ /**
3416
+ * Resolves a cache adapter for the current query options.
3417
+ *
3418
+ * This helper returns a lightweight Redis-backed cache interface when caching
3419
+ * is enabled in the provided query options. Cache failures are treated as
3420
+ * non-fatal so query execution can continue even when Redis is unavailable or
3421
+ * a cache payload is invalid.
3422
+ *
3423
+ * @param options - Optional query options that may enable caching.
3424
+ * @returns A cache adapter when caching is enabled, otherwise `null`.
3425
+ */
3426
+ resolveCache(options) {
3427
+ if (!options?.cache) {
3428
+ return null;
3429
+ }
3430
+ return {
3431
+ get: async (key, meta) => {
3432
+ try {
3433
+ const cached = await redis_manager_1.redis.get(key);
3434
+ if (!cached) {
3435
+ await AmbitenModel.trackCacheMiss();
3436
+ return null;
3437
+ }
3438
+ const parsed = JSON.parse(cached);
3439
+ await AmbitenModel.trackCacheHit();
3440
+ return parsed;
3441
+ }
3442
+ catch {
3443
+ return null;
3444
+ }
3445
+ },
3446
+ set: async (key, value, meta) => {
3447
+ try {
3448
+ const ttl = meta?.ttlSeconds ?? 300;
3449
+ await redis_manager_1.redis.set(key, JSON.stringify(value), {
3450
+ EX: ttl
3451
+ });
3452
+ }
3453
+ catch {
3454
+ // Best-effort cache write. Intentionally ignored.
3455
+ }
3456
+ }
3457
+ };
3458
+ }
3459
+ /**
3460
+ * Builds a deterministic cache key for a query operation.
3461
+ *
3462
+ * The cache key is scoped by tenant, database, collection, and operation, and
3463
+ * uses a normalized serialized payload to ensure stable cache identity across
3464
+ * semantically equivalent query inputs.
3465
+ *
3466
+ * @param operation - Query operation name.
3467
+ * @param payload - Cache identity payload for the operation.
3468
+ * @returns A deterministic tenant-aware cache key.
3469
+ */
3470
+ buildCacheKey(operation, payload) {
3471
+ const tenantId = String(payload.tenantId ?? 'default');
3472
+ const dbName = String(payload.dbName ?? 'default');
3473
+ const collectionName = String(payload.collectionName ?? this._collectionName);
3474
+ const { tenantId: _tenantId, dbName: _dbName, collectionName: _collectionName, ...rest } = payload;
3475
+ const normalizedPayload = this.sortObjectDeep(rest);
3476
+ const serialized = JSON.stringify(normalizedPayload);
3477
+ return `Ambiten:${tenantId}:${dbName}:${collectionName}:${operation}:${serialized}`;
3478
+ }
3479
+ static async trackCacheMiss() {
3480
+ try {
3481
+ const today = new Date().toISOString().slice(0, 10);
3482
+ const retentionSeconds = 86400 * 7;
3483
+ const pipeline = redis_manager_1.redis.multi();
3484
+ pipeline.incr('Ambiten:cache:stats:misses');
3485
+ pipeline.incr(`Ambiten:cache:stats:misses:${today}`);
3486
+ pipeline.expire(`Ambiten:cache:stats:misses:${today}`, retentionSeconds);
3487
+ await pipeline.exec();
3488
+ }
3489
+ catch {
3490
+ // Best-effort telemetry. Intentionally ignored.
3491
+ }
3492
+ }
3493
+ static async trackCacheHit() {
3494
+ try {
3495
+ const today = new Date().toISOString().slice(0, 10);
3496
+ const retentionSeconds = 86400 * 7;
3497
+ const pipeline = redis_manager_1.redis.multi();
3498
+ pipeline.incr('Ambiten:cache:stats:hits');
3499
+ pipeline.incr(`Ambiten:cache:stats:hits:${today}`);
3500
+ pipeline.expire(`Ambiten:cache:stats:hits:${today}`, retentionSeconds);
3501
+ await pipeline.exec();
3502
+ }
3503
+ catch {
3504
+ // Best-effort telemetry. Intentionally ignored.
3505
+ }
3506
+ }
3507
+ static async cacheResult(key, data, ttl = 3600) {
3508
+ if (typeof key !== 'string' || key.trim().length === 0) {
3509
+ throw new Error('Cache key must be a non-empty string.');
3510
+ }
3511
+ if (!Number.isFinite(ttl) || ttl <= 0) {
3512
+ throw new Error('TTL must be a positive number.');
3513
+ }
3514
+ try {
3515
+ await redis_manager_1.redis.set(key, JSON.stringify(data));
3516
+ await redis_manager_1.redis.expire(key, ttl);
3517
+ }
3518
+ catch {
3519
+ // Best-effort cache write. Intentionally ignored.
3520
+ }
3521
+ }
3522
+ /**
3523
+ * Clears a cached result by its key.
3524
+ * @param {string} key - The cache key.
3525
+ * @returns {Promise<void>} Resolves when the cache is cleared.
3526
+ */
3527
+ static async clearCache(key) {
3528
+ if (!key || typeof key !== 'string') {
3529
+ throw new Error('Cache key must be a non-empty string');
3530
+ }
3531
+ try {
3532
+ await redis_manager_1.redis.del(key);
3533
+ }
3534
+ catch (error) {
3535
+ console.error('Error clearing cache:', error);
3536
+ throw new Error(`Failed to clear cache for key "${key}": ${error}`);
3537
+ }
3538
+ }
3539
+ /**
3540
+ * Warms cache entries for selected model queries.
3541
+ *
3542
+ * When query definitions are provided, this helper executes the corresponding
3543
+ * `find` operations and stores the results using Ambiten's scoped cache key
3544
+ * strategy. When no queries are provided, it warms a default collection-wide
3545
+ * `find({})` cache entry, subject to a safety threshold for large collections.
3546
+ *
3547
+ * This helper is best suited for controlled cache priming scenarios such as
3548
+ * startup warmup, scheduled maintenance, or high-traffic query preparation.
3549
+ *
3550
+ * @param queries - Optional list of query definitions to warm.
3551
+ * @param defaultTtl - Default TTL in seconds for warmed entries.
3552
+ * @param ctx - Optional model execution context for tenant, database,
3553
+ * collection, session, and related runtime overrides.
3554
+ * @returns A promise that resolves when cache warming completes.
3555
+ * @throws {Error} When cache warming fails.
3556
+ */
3557
+ async warmCache(queries, defaultTtl = 3600, ctx) {
3558
+ await this.init();
3559
+ if (!Number.isFinite(defaultTtl) || defaultTtl <= 0) {
3560
+ throw new Error('Default TTL must be a positive number.');
3561
+ }
3562
+ return this.runWithModelContext(ctx, async () => {
3563
+ const cache = this.resolveCache({ cache: { ttl: defaultTtl } });
3564
+ if (!cache) {
3565
+ return;
3566
+ }
3567
+ const instrumentation = this.resolveInstrumentationContext(ctx);
3568
+ const col = await this.getCollection(ctx);
3569
+ const session = this.resolveSession(ctx);
3570
+ const warmQueries = queries && queries.length > 0
3571
+ ? queries
3572
+ : [{ filter: {}, ttl: defaultTtl }];
3573
+ if ((!queries || queries.length === 0)) {
3574
+ const totalDocs = await col.countDocuments({}, session ? { session } : undefined);
3575
+ if (totalDocs > 1000) {
3576
+ return;
3577
+ }
3578
+ }
3579
+ for (const query of warmQueries) {
3580
+ const filter = (query.filter ?? {});
3581
+ const ttl = query.ttl ?? defaultTtl;
3582
+ const docs = await col.find(filter, session ? { session } : undefined).toArray();
3583
+ const payload = this.toModelResults(docs);
3584
+ const cacheKey = this.buildCacheKey('find', {
3585
+ collectionName: instrumentation.collectionName,
3586
+ filter,
3587
+ tenantId: instrumentation.tenantId,
3588
+ dbName: instrumentation.dbName
3589
+ });
3590
+ await cache.set(cacheKey, payload, {
3591
+ ttlSeconds: ttl,
3592
+ tenantId: instrumentation.tenantId,
3593
+ namespace: instrumentation.collectionName
3594
+ });
3595
+ }
3596
+ });
3597
+ }
3598
+ // We want to avoids cache-key drift from key ordering.
3599
+ sortObjectDeep(value) {
3600
+ if (Array.isArray(value)) {
3601
+ return value.map((item) => this.sortObjectDeep(item));
3602
+ }
3603
+ if (value instanceof Date) {
3604
+ return value.toISOString();
3605
+ }
3606
+ if (value && typeof value === 'object') {
3607
+ return Object.keys(value)
3608
+ .sort()
3609
+ .reduce((acc, key) => {
3610
+ acc[key] = this.sortObjectDeep(value[key]);
3611
+ return acc;
3612
+ }, {});
3613
+ }
3614
+ return value;
3615
+ }
3616
+ toModelResult(doc) {
3617
+ const { _id, ...rest } = doc;
3618
+ return {
3619
+ ...rest,
3620
+ _id: _id.toString()
3621
+ };
3622
+ }
3623
+ toModelResults(docs = []) {
3624
+ return docs.map((doc) => this.toModelResult(doc));
3625
+ }
3626
+ /**
3627
+ * Restores a single soft-deleted document matching the provided filter.
3628
+ *
3629
+ * This operation runs inside the active Ambiten runtime context, is instrumented
3630
+ * through {@link measureQuery}, executes registered restore middlewares,
3631
+ * triggers schema update hooks, invalidates relevant cache patterns, and
3632
+ * publishes a document restoration event after completion.
3633
+ *
3634
+ * The actual restore behavior is driven by the update document returned from
3635
+ * {@link buildRestoreUpdate}.
3636
+ *
3637
+ * @param filter - MongoDB filter used to identify the document to restore.
3638
+ * @param ctx - Optional model execution context for tenant, database,
3639
+ * collection, session, and related runtime overrides.
3640
+ * @returns A promise that resolves when the operation completes.
3641
+ * @throws {Error} When the filter is missing or invalid.
3642
+ */
3643
+ async restoreOne(filter, ctx) {
3644
+ await this.init();
3645
+ if (!filter || typeof filter !== 'object') {
3646
+ throw new Error('Filter must be a valid object.');
3647
+ }
3648
+ return this.runWithModelContext(ctx, async () => (0, instrumentation_1.measureQuery)({
3649
+ operation: 'restoreOne',
3650
+ collectionName: this.resolveCollectionName(ctx),
3651
+ filter
3652
+ }, async () => {
3653
+ const collectionName = this.resolveCollectionName(ctx);
3654
+ const session = this.resolveSession(ctx);
3655
+ const col = await this.getCollection(ctx);
3656
+ const meta = {
3657
+ restore: true
3658
+ };
3659
+ const middlewareCtx = this.buildMiddlewareContext('restoreOne', ctx, {
3660
+ filter,
3661
+ meta
3662
+ });
3663
+ middlewareCtx.collectionName =
3664
+ middlewareCtx.collectionName ?? collectionName;
3665
+ await this.runBeforeMiddlewares('restoreOne', middlewareCtx);
3666
+ const effectiveFilter = (middlewareCtx.filter ?? filter);
3667
+ middlewareCtx.filter = effectiveFilter;
3668
+ const restoreUpdate = this.buildRestoreUpdate();
3669
+ const existingDoc = await col.findOne(effectiveFilter, session ? { session } : undefined);
3670
+ if (!existingDoc) {
3671
+ middlewareCtx.result = null;
3672
+ await this.runAfterMiddlewares('restoreOne', middlewareCtx);
3673
+ return;
3674
+ }
3675
+ await this.schema.executePre('restoreOne', {
3676
+ operation: 'restoreOne',
3677
+ collectionName: middlewareCtx.collectionName,
3678
+ tenantId: middlewareCtx.tenantId,
3679
+ dbName: middlewareCtx.dbName,
3680
+ session,
3681
+ filter: effectiveFilter,
3682
+ update: restoreUpdate,
3683
+ meta
3684
+ });
3685
+ const result = await col.updateOne(effectiveFilter, restoreUpdate, session ? { session } : undefined);
3686
+ const restoredDoc = await col.findOne(effectiveFilter, session ? { session } : undefined);
3687
+ const restoreResult = {
3688
+ acknowledged: result.acknowledged,
3689
+ matchedCount: result.matchedCount,
3690
+ modifiedCount: result.modifiedCount,
3691
+ upsertedCount: result.upsertedCount,
3692
+ upsertedId: result.upsertedId,
3693
+ restored: restoredDoc
3694
+ ? this.toModelResult(restoredDoc)
3695
+ : null
3696
+ };
3697
+ await this.schema.executePost('restoreOne', {
3698
+ operation: 'restoreOne',
3699
+ collectionName: middlewareCtx.collectionName,
3700
+ tenantId: middlewareCtx.tenantId,
3701
+ dbName: middlewareCtx.dbName,
3702
+ session,
3703
+ filter: effectiveFilter,
3704
+ update: restoreUpdate,
3705
+ result: restoreResult,
3706
+ meta
3707
+ });
3708
+ middlewareCtx.result = restoreResult;
3709
+ await this.runAfterMiddlewares('restoreOne', middlewareCtx);
3710
+ await this.invalidateCachePatterns('restoreOne', middlewareCtx);
3711
+ await pubsub.publish(`${utils_1.DB_CHANGE_EVENT}`, {
3712
+ documentRestored: {
3713
+ action: 'restoreOne',
3714
+ collectionName: middlewareCtx.collectionName,
3715
+ filter: effectiveFilter,
3716
+ result: restoreResult,
3717
+ meta
3718
+ }
3719
+ });
3720
+ }));
3721
+ }
3722
+ /**
3723
+ * Restores multiple soft-deleted documents matching the provided filter.
3724
+ *
3725
+ * This operation runs inside the active Ambiten runtime context, is instrumented
3726
+ * through {@link measureQuery}, executes registered restore middlewares,
3727
+ * triggers schema update hooks, invalidates relevant cache patterns, and
3728
+ * publishes a document restoration event after completion.
3729
+ *
3730
+ * The actual restore behavior is driven by the update document returned from
3731
+ * {@link buildRestoreUpdate}.
3732
+ *
3733
+ * @param filter - MongoDB filter used to identify the documents to restore.
3734
+ * @param ctx - Optional model execution context for tenant, database,
3735
+ * collection, session, and related runtime overrides.
3736
+ * @returns A promise that resolves when the operation completes.
3737
+ * @throws {Error} When the filter is missing or invalid.
3738
+ */
3739
+ async restoreMany(filter, ctx) {
3740
+ await this.init();
3741
+ if (!filter || typeof filter !== 'object') {
3742
+ throw new Error('Filter must be a valid object.');
3743
+ }
3744
+ return this.runWithModelContext(ctx, async () => (0, instrumentation_1.measureQuery)({
3745
+ operation: 'restoreMany',
3746
+ collectionName: this.resolveCollectionName(ctx),
3747
+ filter
3748
+ }, async () => {
3749
+ const collectionName = this.resolveCollectionName(ctx);
3750
+ const session = this.resolveSession(ctx);
3751
+ const col = await this.getCollection(ctx);
3752
+ const meta = {
3753
+ restore: true
3754
+ };
3755
+ const middlewareCtx = this.buildMiddlewareContext('restoreMany', ctx, {
3756
+ filter,
3757
+ meta
3758
+ });
3759
+ middlewareCtx.collectionName =
3760
+ middlewareCtx.collectionName ?? collectionName;
3761
+ await this.runBeforeMiddlewares('restoreMany', middlewareCtx);
3762
+ const effectiveFilter = (middlewareCtx.filter ?? filter);
3763
+ middlewareCtx.filter = effectiveFilter;
3764
+ const restoreUpdate = this.buildRestoreUpdate();
3765
+ const existingDocs = await col.find(effectiveFilter, session ? { session } : undefined).toArray();
3766
+ if (existingDocs.length === 0) {
3767
+ middlewareCtx.result = {
3768
+ restoredCount: 0,
3769
+ docs: []
3770
+ };
3771
+ await this.runAfterMiddlewares('restoreMany', middlewareCtx);
3772
+ return;
3773
+ }
3774
+ await this.schema.executePre('updateMany', {
3775
+ operation: 'restoreMany',
3776
+ collectionName: middlewareCtx.collectionName,
3777
+ tenantId: middlewareCtx.tenantId,
3778
+ dbName: middlewareCtx.dbName,
3779
+ session,
3780
+ filter: effectiveFilter,
3781
+ update: restoreUpdate,
3782
+ docs: existingDocs,
3783
+ meta
3784
+ });
3785
+ const result = await col.updateMany(effectiveFilter, restoreUpdate, session ? { session } : undefined);
3786
+ const restoredDocs = await col.find(effectiveFilter, session ? { session } : undefined).toArray();
3787
+ const restoreResult = {
3788
+ acknowledged: result.acknowledged,
3789
+ matchedCount: existingDocs.length,
3790
+ modifiedCount: result.modifiedCount,
3791
+ restoredCount: result.modifiedCount,
3792
+ docs: this.toModelResults(restoredDocs)
3793
+ };
3794
+ await this.schema.executePost('updateMany', {
3795
+ operation: 'restoreMany',
3796
+ collectionName: middlewareCtx.collectionName,
3797
+ tenantId: middlewareCtx.tenantId,
3798
+ dbName: middlewareCtx.dbName,
3799
+ session,
3800
+ filter: effectiveFilter,
3801
+ update: restoreUpdate,
3802
+ docs: restoredDocs,
3803
+ result: restoreResult,
3804
+ meta
3805
+ });
3806
+ middlewareCtx.result = restoreResult;
3807
+ await this.runAfterMiddlewares('restoreMany', middlewareCtx);
3808
+ await this.invalidateCachePatterns('restoreMany', middlewareCtx);
3809
+ await pubsub.publish(`${utils_1.DB_CHANGE_EVENT}`, {
3810
+ documentRestored: {
3811
+ action: 'restoreMany',
3812
+ collectionName: middlewareCtx.collectionName,
3813
+ filter: effectiveFilter,
3814
+ result: restoreResult,
3815
+ meta
3816
+ }
3817
+ });
3818
+ }));
3819
+ }
3820
+ buildRestoreUpdate() {
3821
+ const deletedAtField = this._softDeleteConfig?.deletedAtField ?? 'deletedAt';
3822
+ const isDeletedField = this._softDeleteConfig?.isDeletedField ?? 'isDeleted';
3823
+ return {
3824
+ $set: {
3825
+ [deletedAtField]: null,
3826
+ [isDeletedField]: false
3827
+ }
3828
+ };
3829
+ }
3830
+ //================ Middleware Context and Execution =================//
3831
+ buildMiddlewareContext(operation, ctx, payload = {}) {
3832
+ const merged = this.mergeCtx(ctx);
3833
+ return {
3834
+ operation,
3835
+ modelName: this.constructor.name,
3836
+ collectionName: payload.collectionName ??
3837
+ merged?.collectionName ??
3838
+ this._collectionName,
3839
+ tenantId: payload.tenantId ?? merged?.tenantId,
3840
+ dbName: payload.dbName ?? merged?.dbName,
3841
+ session: payload.session ?? merged?.session,
3842
+ withDeleted: payload.withDeleted ?? merged?.withDeleted,
3843
+ onlyDeleted: payload.onlyDeleted ?? merged?.onlyDeleted,
3844
+ hardDelete: payload.hardDelete ?? merged?.hardDelete,
3845
+ filter: payload.filter,
3846
+ update: payload.update,
3847
+ doc: payload.doc,
3848
+ docs: payload.docs,
3849
+ pipeline: payload.pipeline,
3850
+ bulkUpdates: payload.bulkUpdates,
3851
+ bulkOperations: payload.bulkOperations,
3852
+ result: payload.result,
3853
+ meta: payload.meta
3854
+ };
3855
+ }
3856
+ setSoftDeleteConfig(config) {
3857
+ this._softDeleteConfig = config;
3858
+ }
3859
+ /**
3860
+ * Builds the update document used for soft delete operations.
3861
+ *
3862
+ * The generated update sets the configured deleted timestamp field and
3863
+ * deleted-state flag field to mark the document as logically deleted without
3864
+ * physically removing it from the collection.
3865
+ *
3866
+ * @returns MongoDB update document for soft delete flows.
3867
+ */
3868
+ buildSoftDeleteUpdate() {
3869
+ const deletedAtField = this._softDeleteConfig?.deletedAtField ?? 'deletedAt';
3870
+ const isDeletedField = this._softDeleteConfig?.isDeletedField ?? 'isDeleted';
3871
+ return {
3872
+ $set: {
3873
+ [deletedAtField]: new Date(),
3874
+ [isDeletedField]: true
3875
+ }
3876
+ };
3877
+ }
3878
+ // Middleware runners/execution methods
3879
+ async runBeforeMiddlewares(operation, ctx) {
3880
+ const handlers = this.beforeMiddlewares.get(operation) ?? [];
3881
+ for (const handler of handlers) {
3882
+ await handler(ctx);
3883
+ }
3884
+ }
3885
+ async runAfterMiddlewares(operation, ctx) {
3886
+ const handlers = this.afterMiddlewares.get(operation) ?? [];
3887
+ for (const handler of handlers) {
3888
+ await handler(ctx);
3889
+ }
3890
+ }
3891
+ // Middleware registration methods
3892
+ before(operation, handler) {
3893
+ const existing = this.beforeMiddlewares.get(operation) ?? [];
3894
+ existing.push(handler);
3895
+ this.beforeMiddlewares.set(operation, existing);
3896
+ return this;
3897
+ }
3898
+ after(operation, handler) {
3899
+ const existing = this.afterMiddlewares.get(operation) ?? [];
3900
+ existing.push(handler);
3901
+ this.afterMiddlewares.set(operation, existing);
3902
+ return this;
3903
+ }
3904
+ beforeFind(handler) {
3905
+ return this.before('find', handler);
3906
+ }
3907
+ afterFind(handler) {
3908
+ return this.after('find', handler);
3909
+ }
3910
+ beforeFindOne(handler) {
3911
+ return this.before('findOne', handler);
3912
+ }
3913
+ afterFindOne(handler) {
3914
+ return this.after('findOne', handler);
3915
+ }
3916
+ beforeSave(handler) {
3917
+ return this.before('create', handler);
3918
+ }
3919
+ afterSave(handler) {
3920
+ return this.after('create', handler);
3921
+ }
3922
+ beforeUpdateOne(handler) {
3923
+ return this.before('updateOne', handler);
3924
+ }
3925
+ afterUpdateOne(handler) {
3926
+ return this.after('updateOne', handler);
3927
+ }
3928
+ beforeDeleteOne(handler) {
3929
+ return this.before('deleteOne', handler);
3930
+ }
3931
+ afterDeleteOne(handler) {
3932
+ return this.after('deleteOne', handler);
3933
+ }
3934
+ beforeDeleteMany(handler) {
3935
+ return this.before('deleteMany', handler);
3936
+ }
3937
+ afterDeleteMany(handler) {
3938
+ return this.after('deleteMany', handler);
3939
+ }
3940
+ beforeBulkInsert(handler) {
3941
+ return this.before('bulkInsert', handler);
3942
+ }
3943
+ afterBulkInsert(handler) {
3944
+ return this.after('bulkInsert', handler);
3945
+ }
3946
+ beforeBulkUpdate(handler) {
3947
+ return this.before('bulkUpdate', handler);
3948
+ }
3949
+ afterBulkUpdate(handler) {
3950
+ return this.after('bulkUpdate', handler);
3951
+ }
3952
+ beforeAggregate(handler) {
3953
+ return this.before('aggregate', handler);
3954
+ }
3955
+ afterAggregate(handler) {
3956
+ return this.after('aggregate', handler);
3957
+ }
3958
+ beforeFindOneAndUpdate(handler) {
3959
+ return this.before('findOneAndUpdate', handler);
3960
+ }
3961
+ afterFindOneAndUpdate(handler) {
3962
+ return this.after('findOneAndUpdate', handler);
3963
+ }
3964
+ beforeFindOneAndDelete(handler) {
3965
+ return this.before('findOneAndDelete', handler);
3966
+ }
3967
+ afterFindOneAndDelete(handler) {
3968
+ return this.after('findOneAndDelete', handler);
3969
+ }
3970
+ beforeFindOneAndReplace(handler) {
3971
+ return this.before('findOneAndReplace', handler);
3972
+ }
3973
+ afterFindOneAndReplace(handler) {
3974
+ return this.after('findOneAndReplace', handler);
3975
+ }
3976
+ beforeFindOneAndUpsert(handler) {
3977
+ return this.before('findOneAndUpsert', handler);
3978
+ }
3979
+ afterFindOneAndUpsert(handler) {
3980
+ return this.after('findOneAndUpsert', handler);
3981
+ }
3982
+ }
3983
+ exports.AmbitenModel = AmbitenModel;
3984
+ ;