joist-core 2.2.0 → 2.3.0-next.2

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 (233) hide show
  1. package/build/Aliases.d.ts.map +1 -1
  2. package/build/Aliases.js.map +1 -1
  3. package/build/BaseEntity.d.ts +2 -0
  4. package/build/BaseEntity.d.ts.map +1 -1
  5. package/build/BaseEntity.js +1 -1
  6. package/build/BaseEntity.js.map +1 -1
  7. package/build/Entity.d.ts +1 -0
  8. package/build/Entity.d.ts.map +1 -1
  9. package/build/EntityFilter.d.ts +29 -10
  10. package/build/EntityFilter.d.ts.map +1 -1
  11. package/build/EntityFilter.js +18 -0
  12. package/build/EntityFilter.js.map +1 -1
  13. package/build/EntityGraphQLFilter.d.ts +3 -0
  14. package/build/EntityGraphQLFilter.d.ts.map +1 -1
  15. package/build/EntityGraphQLFilter.js.map +1 -1
  16. package/build/EntityManager.d.ts +36 -50
  17. package/build/EntityManager.d.ts.map +1 -1
  18. package/build/EntityManager.js +248 -124
  19. package/build/EntityManager.js.map +1 -1
  20. package/build/EntityMetadata.d.ts +48 -2
  21. package/build/EntityMetadata.d.ts.map +1 -1
  22. package/build/EntityMetadata.js +17 -0
  23. package/build/EntityMetadata.js.map +1 -1
  24. package/build/IndexManager.d.ts +5 -5
  25. package/build/IndexManager.d.ts.map +1 -1
  26. package/build/IndexManager.js +48 -32
  27. package/build/IndexManager.js.map +1 -1
  28. package/build/InstanceData.d.ts +16 -2
  29. package/build/InstanceData.d.ts.map +1 -1
  30. package/build/InstanceData.js +37 -4
  31. package/build/InstanceData.js.map +1 -1
  32. package/build/IsLoadedCache.d.ts.map +1 -1
  33. package/build/IsLoadedCache.js +31 -25
  34. package/build/IsLoadedCache.js.map +1 -1
  35. package/build/JoinRows.d.ts +54 -12
  36. package/build/JoinRows.d.ts.map +1 -1
  37. package/build/JoinRows.js +114 -44
  38. package/build/JoinRows.js.map +1 -1
  39. package/build/QueryParser.collectionJoins.js +8 -2
  40. package/build/QueryParser.collectionJoins.js.map +1 -1
  41. package/build/QueryParser.d.ts +15 -3
  42. package/build/QueryParser.d.ts.map +1 -1
  43. package/build/QueryParser.js +293 -190
  44. package/build/QueryParser.js.map +1 -1
  45. package/build/QueryParser.pruning.d.ts.map +1 -1
  46. package/build/QueryParser.pruning.js +8 -1
  47. package/build/QueryParser.pruning.js.map +1 -1
  48. package/build/QueryParser.test.d.ts +2 -0
  49. package/build/QueryParser.test.d.ts.map +1 -0
  50. package/build/QueryParser.test.js +27 -0
  51. package/build/QueryParser.test.js.map +1 -0
  52. package/build/ReactionsManager.d.ts +2 -1
  53. package/build/ReactionsManager.d.ts.map +1 -1
  54. package/build/ReactionsManager.js +53 -49
  55. package/build/ReactionsManager.js.map +1 -1
  56. package/build/Todo.d.ts +4 -0
  57. package/build/Todo.d.ts.map +1 -1
  58. package/build/Todo.js.map +1 -1
  59. package/build/batchloaders/BatchLoader.d.ts.map +1 -1
  60. package/build/batchloaders/BatchLoader.js +6 -1
  61. package/build/batchloaders/BatchLoader.js.map +1 -1
  62. package/build/batchloaders/enumCollectionBatchLoader.d.ts +7 -0
  63. package/build/batchloaders/enumCollectionBatchLoader.d.ts.map +1 -0
  64. package/build/batchloaders/enumCollectionBatchLoader.js +54 -0
  65. package/build/batchloaders/enumCollectionBatchLoader.js.map +1 -0
  66. package/build/batchloaders/manyToManyBatchLoader.d.ts.map +1 -1
  67. package/build/batchloaders/manyToManyBatchLoader.js +10 -2
  68. package/build/batchloaders/manyToManyBatchLoader.js.map +1 -1
  69. package/build/batchloaders/populateBatchLoader.d.ts.map +1 -1
  70. package/build/batchloaders/populateBatchLoader.js +26 -8
  71. package/build/batchloaders/populateBatchLoader.js.map +1 -1
  72. package/build/batchloaders/recursiveChildrenBatchLoader.d.ts.map +1 -1
  73. package/build/batchloaders/recursiveChildrenBatchLoader.js +2 -0
  74. package/build/batchloaders/recursiveChildrenBatchLoader.js.map +1 -1
  75. package/build/batchloaders/recursiveM2mBatchLoader.d.ts.map +1 -1
  76. package/build/batchloaders/recursiveM2mBatchLoader.js +1 -3
  77. package/build/batchloaders/recursiveM2mBatchLoader.js.map +1 -1
  78. package/build/batchloaders/recursiveParentsBatchLoader.d.ts.map +1 -1
  79. package/build/batchloaders/recursiveParentsBatchLoader.js +2 -0
  80. package/build/batchloaders/recursiveParentsBatchLoader.js.map +1 -1
  81. package/build/changes.d.ts +13 -0
  82. package/build/changes.d.ts.map +1 -1
  83. package/build/changes.js +49 -0
  84. package/build/changes.js.map +1 -1
  85. package/build/configure.d.ts +5 -3
  86. package/build/configure.d.ts.map +1 -1
  87. package/build/configure.js +70 -0
  88. package/build/configure.js.map +1 -1
  89. package/build/dataloaders/fastWhereFilterHash.d.ts +15 -0
  90. package/build/dataloaders/fastWhereFilterHash.d.ts.map +1 -0
  91. package/build/dataloaders/fastWhereFilterHash.js +164 -0
  92. package/build/dataloaders/fastWhereFilterHash.js.map +1 -0
  93. package/build/dataloaders/fastWhereFilterHash.test.d.ts +2 -0
  94. package/build/dataloaders/fastWhereFilterHash.test.d.ts.map +1 -0
  95. package/build/dataloaders/fastWhereFilterHash.test.js +59 -0
  96. package/build/dataloaders/fastWhereFilterHash.test.js.map +1 -0
  97. package/build/dataloaders/findCountDataLoader.d.ts +13 -4
  98. package/build/dataloaders/findCountDataLoader.d.ts.map +1 -1
  99. package/build/dataloaders/findCountDataLoader.js +58 -15
  100. package/build/dataloaders/findCountDataLoader.js.map +1 -1
  101. package/build/dataloaders/findDataLoader.d.ts +49 -10
  102. package/build/dataloaders/findDataLoader.d.ts.map +1 -1
  103. package/build/dataloaders/findDataLoader.js +217 -108
  104. package/build/dataloaders/findDataLoader.js.map +1 -1
  105. package/build/dataloaders/findIdsDataLoader.d.ts +1 -2
  106. package/build/dataloaders/findIdsDataLoader.d.ts.map +1 -1
  107. package/build/dataloaders/findIdsDataLoader.js +18 -17
  108. package/build/dataloaders/findIdsDataLoader.js.map +1 -1
  109. package/build/dataloaders/findOrCreateDataLoader.d.ts.map +1 -1
  110. package/build/dataloaders/findOrCreateDataLoader.js +3 -3
  111. package/build/dataloaders/findOrCreateDataLoader.js.map +1 -1
  112. package/build/dataloaders/findPaginatedDataLoader.d.ts +7 -0
  113. package/build/dataloaders/findPaginatedDataLoader.d.ts.map +1 -0
  114. package/build/dataloaders/findPaginatedDataLoader.js +84 -0
  115. package/build/dataloaders/findPaginatedDataLoader.js.map +1 -0
  116. package/build/dataloaders/manyToManyFindDataLoader.js +7 -1
  117. package/build/dataloaders/manyToManyFindDataLoader.js.map +1 -1
  118. package/build/drivers/EntityWriter.js +13 -7
  119. package/build/drivers/EntityWriter.js.map +1 -1
  120. package/build/drivers/buildRawQuery.d.ts +6 -4
  121. package/build/drivers/buildRawQuery.d.ts.map +1 -1
  122. package/build/drivers/buildRawQuery.js +8 -4
  123. package/build/drivers/buildRawQuery.js.map +1 -1
  124. package/build/fields.d.ts +2 -0
  125. package/build/fields.d.ts.map +1 -1
  126. package/build/fields.js +18 -6
  127. package/build/fields.js.map +1 -1
  128. package/build/index.d.ts +3 -1
  129. package/build/index.d.ts.map +1 -1
  130. package/build/index.js +4 -1
  131. package/build/index.js.map +1 -1
  132. package/build/keys.d.ts.map +1 -1
  133. package/build/keys.js +8 -6
  134. package/build/keys.js.map +1 -1
  135. package/build/loadHints.d.ts +13 -4
  136. package/build/loadHints.d.ts.map +1 -1
  137. package/build/loadHints.js +63 -9
  138. package/build/loadHints.js.map +1 -1
  139. package/build/loadLens.d.ts +11 -0
  140. package/build/loadLens.d.ts.map +1 -1
  141. package/build/loadLens.js +43 -10
  142. package/build/loadLens.js.map +1 -1
  143. package/build/newEntity.d.ts.map +1 -1
  144. package/build/newEntity.js +16 -14
  145. package/build/newEntity.js.map +1 -1
  146. package/build/newTestInstance.d.ts.map +1 -1
  147. package/build/newTestInstance.js +3 -0
  148. package/build/newTestInstance.js.map +1 -1
  149. package/build/normalizeHints.d.ts +7 -0
  150. package/build/normalizeHints.d.ts.map +1 -1
  151. package/build/normalizeHints.js +40 -2
  152. package/build/normalizeHints.js.map +1 -1
  153. package/build/preloading/JsonAggregatePreloader.d.ts.map +1 -1
  154. package/build/preloading/JsonAggregatePreloader.js +91 -9
  155. package/build/preloading/JsonAggregatePreloader.js.map +1 -1
  156. package/build/preloading/canPreload.d.ts +2 -2
  157. package/build/preloading/canPreload.d.ts.map +1 -1
  158. package/build/preloading/canPreload.js +3 -0
  159. package/build/preloading/canPreload.js.map +1 -1
  160. package/build/preloading/partitionHint.d.ts.map +1 -1
  161. package/build/preloading/partitionHint.js +10 -4
  162. package/build/preloading/partitionHint.js.map +1 -1
  163. package/build/reactiveHints.d.ts +2 -2
  164. package/build/reactiveHints.d.ts.map +1 -1
  165. package/build/reactiveHints.js +9 -0
  166. package/build/reactiveHints.js.map +1 -1
  167. package/build/relations/EnumCollection.d.ts +81 -0
  168. package/build/relations/EnumCollection.d.ts.map +1 -0
  169. package/build/relations/EnumCollection.js +211 -0
  170. package/build/relations/EnumCollection.js.map +1 -0
  171. package/build/relations/ManyToManyCollection.d.ts +1 -0
  172. package/build/relations/ManyToManyCollection.d.ts.map +1 -1
  173. package/build/relations/ManyToManyCollection.js +5 -2
  174. package/build/relations/ManyToManyCollection.js.map +1 -1
  175. package/build/relations/ManyToManyLargeCollection.d.ts +3 -2
  176. package/build/relations/ManyToManyLargeCollection.d.ts.map +1 -1
  177. package/build/relations/ManyToManyLargeCollection.js +7 -4
  178. package/build/relations/ManyToManyLargeCollection.js.map +1 -1
  179. package/build/relations/OneToManyCollection.d.ts.map +1 -1
  180. package/build/relations/OneToManyCollection.js +2 -2
  181. package/build/relations/OneToManyCollection.js.map +1 -1
  182. package/build/relations/OneToManyLargeCollection.d.ts.map +1 -1
  183. package/build/relations/OneToManyLargeCollection.js +2 -1
  184. package/build/relations/OneToManyLargeCollection.js.map +1 -1
  185. package/build/relations/ReactiveManyToMany.d.ts +1 -0
  186. package/build/relations/ReactiveManyToMany.d.ts.map +1 -1
  187. package/build/relations/ReactiveManyToMany.js +4 -1
  188. package/build/relations/ReactiveManyToMany.js.map +1 -1
  189. package/build/relations/ReactiveManyToManyOtherSide.d.ts +1 -0
  190. package/build/relations/ReactiveManyToManyOtherSide.d.ts.map +1 -1
  191. package/build/relations/ReactiveManyToManyOtherSide.js +4 -1
  192. package/build/relations/ReactiveManyToManyOtherSide.js.map +1 -1
  193. package/build/relations/RecursiveCollection.d.ts +2 -6
  194. package/build/relations/RecursiveCollection.d.ts.map +1 -1
  195. package/build/relations/RecursiveCollection.js +9 -17
  196. package/build/relations/RecursiveCollection.js.map +1 -1
  197. package/build/relations/RecursiveCycleError.d.ts +13 -0
  198. package/build/relations/RecursiveCycleError.d.ts.map +1 -0
  199. package/build/relations/RecursiveCycleError.js +15 -0
  200. package/build/relations/RecursiveCycleError.js.map +1 -0
  201. package/build/relations/hasOneThrough.d.ts.map +1 -1
  202. package/build/relations/hasOneThrough.js +6 -4
  203. package/build/relations/hasOneThrough.js.map +1 -1
  204. package/build/relations/hasProperty.d.ts +2 -1
  205. package/build/relations/hasProperty.d.ts.map +1 -1
  206. package/build/relations/hasProperty.js +15 -5
  207. package/build/relations/hasProperty.js.map +1 -1
  208. package/build/relations/index.d.ts +1 -0
  209. package/build/relations/index.d.ts.map +1 -1
  210. package/build/relations/index.js +5 -2
  211. package/build/relations/index.js.map +1 -1
  212. package/build/rules.js +1 -1
  213. package/build/rules.js.map +1 -1
  214. package/build/scopes.d.ts +185 -0
  215. package/build/scopes.d.ts.map +1 -0
  216. package/build/scopes.js +302 -0
  217. package/build/scopes.js.map +1 -0
  218. package/build/temporal.d.ts +32 -2
  219. package/build/temporal.d.ts.map +1 -1
  220. package/build/temporal.js +18 -0
  221. package/build/temporal.js.map +1 -1
  222. package/build/upsert.d.ts.map +1 -1
  223. package/build/upsert.js +9 -2
  224. package/build/upsert.js.map +1 -1
  225. package/build/utils.d.ts +2 -0
  226. package/build/utils.d.ts.map +1 -1
  227. package/build/utils.js +12 -0
  228. package/build/utils.js.map +1 -1
  229. package/package.json +8 -10
  230. package/build/caches.d.ts +0 -6
  231. package/build/caches.d.ts.map +0 -1
  232. package/build/caches.js +0 -42
  233. package/build/caches.js.map +0 -1
@@ -29,7 +29,6 @@ const IndexManager_1 = require("./IndexManager");
29
29
  // We alias `Entity => EntityW` to denote "Entity wide" i.e. the non-narrowed Entity
30
30
  const loadBatchLoader_1 = require("./batchloaders/loadBatchLoader");
31
31
  const populateBatchLoader_1 = require("./batchloaders/populateBatchLoader");
32
- const caches_1 = require("./caches");
33
32
  const config_1 = require("./config");
34
33
  const configure_1 = require("./configure");
35
34
  const findByUniqueDataLoader_1 = require("./dataloaders/findByUniqueDataLoader");
@@ -37,20 +36,23 @@ const findCountDataLoader_1 = require("./dataloaders/findCountDataLoader");
37
36
  const findDataLoader_1 = require("./dataloaders/findDataLoader");
38
37
  const findIdsDataLoader_1 = require("./dataloaders/findIdsDataLoader");
39
38
  const findOrCreateDataLoader_1 = require("./dataloaders/findOrCreateDataLoader");
39
+ const findPaginatedDataLoader_1 = require("./dataloaders/findPaginatedDataLoader");
40
40
  const Entity_1 = require("./Entity");
41
41
  const FlushLock_1 = require("./FlushLock");
42
42
  const index_1 = require("./index");
43
43
  const IsLoadedCache_1 = require("./IsLoadedCache");
44
44
  const JoinRows_1 = require("./JoinRows");
45
+ const loadHints_1 = require("./loadHints");
45
46
  const newEntity_1 = require("./newEntity");
46
47
  const newTestInstance_1 = require("./newTestInstance");
47
48
  const PluginManager_1 = require("./PluginManager");
48
49
  const ReactionsManager_1 = require("./ReactionsManager");
49
50
  const reactiveHints_1 = require("./reactiveHints");
50
51
  const relations_1 = require("./relations");
51
- const hasAsyncMethod_1 = require("./relations/hasAsyncMethod");
52
52
  const AsyncProperty_1 = require("./relations/AsyncProperty");
53
+ const hasAsyncMethod_1 = require("./relations/hasAsyncMethod");
53
54
  const RecursiveCollection_1 = require("./relations/RecursiveCollection");
55
+ const scopes_1 = require("./scopes");
54
56
  const Todo_1 = require("./Todo");
55
57
  const trusted_1 = require("./trusted");
56
58
  const upsert_1 = require("./upsert");
@@ -87,6 +89,8 @@ class EntityManager {
87
89
  txn;
88
90
  entityLimit = defaultEntityLimit;
89
91
  #entitiesArray = [];
92
+ // Incrementally track dirty entities so we don't have to scan `#entityArray` during flush
93
+ #maybePendingFlushEntities = new Set();
90
94
  // Indexes the currently loaded entities by their tagged ids and `toTaggedString` ids (i.e. `a#`). This fixes
91
95
  // real-world performance issues where `findExistingInstance` scanning `#entities` was an `O(n^2)`.
92
96
  #entitiesById = new Map();
@@ -105,6 +109,7 @@ class EntityManager {
105
109
  * so that both see the most accurate state.
106
110
  */
107
111
  #pendingDeletes = [];
112
+ #hasAnyDeletes = false;
108
113
  #dataloaders = {};
109
114
  #batchLoaders = {};
110
115
  #joinRows = {};
@@ -154,6 +159,20 @@ class EntityManager {
154
159
  indexManager: this.#indexManager,
155
160
  isLoadedCache: this.#isLoadedCache,
156
161
  pluginManager,
162
+ markMaybePending(entity) {
163
+ em.#maybePendingFlushEntities.add(entity);
164
+ },
165
+ unmarkMaybePending(entity) {
166
+ em.#maybePendingFlushEntities.delete(entity);
167
+ },
168
+ hasAnyDeletes() {
169
+ return em.#hasAnyDeletes;
170
+ },
171
+ pendingDeleteIds(type) {
172
+ return em.#pendingDeletes
173
+ .filter((entity) => entity instanceof type && entity.idMaybe !== undefined)
174
+ .map((entity) => (0, index_1.keyToNumber)((0, index_1.getMetadata)(entity), entity.id));
175
+ },
157
176
  isMerging(entity) {
158
177
  return em.#merging?.has(entity) ?? false;
159
178
  },
@@ -219,7 +238,8 @@ class EntityManager {
219
238
  }
220
239
  for (const joinRows of Object.values(this.#joinRows)) {
221
240
  const todo = joinRows.toTodo();
222
- if (todo) {
241
+ // Enum m2ms (EnumCollections) aren't entity-to-entity, so skip them here.
242
+ if (todo && !todo.m2m.otherEnum) {
223
243
  for (const row of todo.newRows) {
224
244
  const entities = Object.values(row.columns);
225
245
  changes.push({ kind: "m2m", op: "add", joinTableName: todo.m2m.joinTableName, entities });
@@ -246,10 +266,11 @@ class EntityManager {
246
266
  }
247
267
  async find(type, where, options) {
248
268
  const { populate, ...rest } = options || {};
249
- const settings = { where, ...rest };
250
- const result = await (0, findDataLoader_1.findDataLoader)(this, type, settings, populate)
251
- .load(settings)
252
- .catch(function find(err) {
269
+ const normalized = (0, index_1.mergeFindOptions)(where, rest);
270
+ const settings = { where: normalized.where, ...normalized.options };
271
+ const result = await (hasPaginationSettings(normalized.options)
272
+ ? (0, findPaginatedDataLoader_1.findPaginatedDataLoader)(this, type, settings, populate)
273
+ : (0, findDataLoader_1.findDataLoader)(this, type, settings, populate)).catch(function find(err) {
253
274
  throw appendStack(err, new Error());
254
275
  });
255
276
  if (populate) {
@@ -257,37 +278,52 @@ class EntityManager {
257
278
  }
258
279
  return result;
259
280
  }
260
- async findPaginated(type, where, options) {
261
- const { populate, limit, offset, ...rest } = options || {};
262
- const meta = (0, index_1.getMetadata)(type);
263
- const query = (0, index_1.parseFindQuery)(meta, where, rest);
264
- const rows = await this.executeFind(meta, "findPaginated", query, { ...rest, limit, offset, checkLimit: false });
265
- const result = this.hydrate(type, rows);
266
- if (populate) {
267
- await this.populate(result, populate);
268
- }
269
- return result;
270
- }
271
281
  /** Runs the post-parse find pipeline: plugins mutate the logical AST, then Joist optimizes/prunes before SQL. */
272
282
  async executeFind(meta, operation, parsed, settings) {
273
- const { checkLimit, ...driverSettings } = settings;
283
+ const { checkLimit, findSettings } = this.prepareFind(meta, operation, parsed, settings);
284
+ return this.executePreparedFind(meta, operation, parsed, findSettings, checkLimit);
285
+ }
286
+ /** Executes a query that has already had find hooks and optimizations applied. */
287
+ async executePreparedFind(meta, operation, parsed, findSettings, checkLimit) {
274
288
  const { pluginManager } = getEmInternalApi(this);
275
- pluginManager.beforeFind(meta, operation, parsed, driverSettings);
276
- (0, index_1.optimizeCollectionJoins)(parsed, settings);
277
- const rows = await this.driver.executeFind(this, parsed, driverSettings);
289
+ const rows = await this.driver.executeFind(this, parsed, findSettings);
278
290
  // Check by default unless explicitly disabled or the caller removed the LIMIT via `limit: undefined`
279
- const shouldCheck = checkLimit ?? !("limit" in settings && settings.limit === undefined);
291
+ const shouldCheck = checkLimit ?? !("limit" in findSettings && findSettings.limit === undefined);
280
292
  if (shouldCheck && rows.length >= this.entityLimit) {
281
293
  throw new Error(`Query returned more than ${this.entityLimit} entityLimit rows`);
282
294
  }
283
295
  pluginManager.afterFind(meta, operation, rows);
284
296
  return rows;
285
297
  }
286
- async findGql(type, where, options) {
287
- return this.find(type, where, options);
298
+ /**
299
+ * Runs pre-SQL find hooks and optimizations against a parsed query.
300
+ *
301
+ * This allows plugins to see "pre-batched" / "logical" query ASTs, instead of our
302
+ * more complicated `_find` batched queries. The flow would be:
303
+ *
304
+ * - A loader calls `prepareFind(originalQuery)`
305
+ * - Plugins inspect/modify the query as/if needed
306
+ * - The loader crafts a new, more complicated query that embeds the originalQuery
307
+ * - The loader calls `executePreparedFind` with the 2nd query
308
+ */
309
+ prepareFind(meta, operation, parsed, settings) {
310
+ const { checkLimit, ...findSettings } = settings;
311
+ const { pluginManager } = getEmInternalApi(this);
312
+ // Plugins may mutate the settings object, so return the post-hook version that loaders must reuse.
313
+ pluginManager.beforeFind(meta, operation, parsed, findSettings);
314
+ (0, index_1.optimizeCollectionJoins)(parsed, settings);
315
+ return { checkLimit, findSettings };
288
316
  }
289
- async findGqlPaginated(type, where, options) {
290
- return this.findPaginated(type, where, options);
317
+ async findGql(type, where, options) {
318
+ if (!options) {
319
+ return this.find(type, where);
320
+ }
321
+ const normalized = { ...options };
322
+ if ("limit" in normalized)
323
+ normalized.limit = normalized.limit ?? undefined;
324
+ if ("offset" in normalized)
325
+ normalized.offset = normalized.offset ?? undefined;
326
+ return this.find(type, where, normalized);
291
327
  }
292
328
  async findOne(type, where, options) {
293
329
  const list = await this.find(type, where, options);
@@ -329,6 +365,9 @@ class EntityManager {
329
365
  }
330
366
  else {
331
367
  const [entity] = this.hydrate(type, [row]);
368
+ if (this.#hasAnyDeletes && entity.isDeletedEntity) {
369
+ return undefined;
370
+ }
332
371
  if (populate) {
333
372
  await this.populate(entity, populate);
334
373
  }
@@ -344,17 +383,16 @@ class EntityManager {
344
383
  * Note: this method is not currently auto-batched, so it will cause N+1s if called in a loop.
345
384
  */
346
385
  async findCount(type, where, options = {}) {
347
- const settings = { where, ...options };
348
- let count = await (0, findCountDataLoader_1.findCountDataLoader)(this, type, settings)
349
- .load(settings)
350
- .catch(function findCount(err) {
386
+ const normalized = (0, findCountDataLoader_1.mergeCountOptions)(where, options);
387
+ const settings = { where: normalized.where, ...normalized.options };
388
+ let count = await (0, findCountDataLoader_1.findCountDataLoader)(this, type, settings).catch(function findCount(err) {
351
389
  throw appendStack(err, new Error());
352
390
  });
353
391
  // If the user is do "count all", we can adjust the number up/down based on
354
392
  // WIP creates/deletes. We can't do this if the WHERE clause is populated b/c
355
393
  // then we'd also have to eval each created/deleted entity against the WHERE
356
394
  // clause before knowing if it should adjust teh amount.
357
- const isSelectAll = Object.keys(where).length === 0;
395
+ const isSelectAll = (0, scopes_1.isSelectAllFilter)(where, options.conditions);
358
396
  if (isSelectAll) {
359
397
  const tagged = this.#entitiesByTag.get((0, index_1.getMetadata)(type).tagName) ?? [];
360
398
  for (const entity of tagged) {
@@ -378,10 +416,9 @@ class EntityManager {
378
416
  * as `em.find`.
379
417
  */
380
418
  async findIds(type, where, options = {}) {
381
- const settings = { where, ...options };
382
- return (0, findIdsDataLoader_1.findIdsDataLoader)(this, type, settings)
383
- .load(settings)
384
- .catch(function findIds(err) {
419
+ const normalized = (0, findCountDataLoader_1.mergeCountOptions)(where, options);
420
+ const settings = { where: normalized.where, ...normalized.options };
421
+ return (0, findIdsDataLoader_1.findIdsDataLoader)(this, type, settings).catch(function findIds(err) {
385
422
  throw appendStack(err, new Error());
386
423
  });
387
424
  }
@@ -485,6 +522,7 @@ class EntityManager {
485
522
  case "primaryKey":
486
523
  case "o2m":
487
524
  case "m2m":
525
+ case "m2mEnum":
488
526
  case "o2o":
489
527
  case "lo2m":
490
528
  return undefined;
@@ -613,6 +651,7 @@ class EntityManager {
613
651
  else if (kind === "primitive" ||
614
652
  kind === "primaryKey" ||
615
653
  kind === "enum" ||
654
+ kind === "m2mEnum" ||
616
655
  kind === "poly" ||
617
656
  kind === "m2o" ||
618
657
  kind === "lo2m") {
@@ -670,48 +709,86 @@ class EntityManager {
670
709
  }
671
710
  async loadAll(type, _ids, hint) {
672
711
  const meta = (0, index_1.getMetadata)(type);
673
- const ids = _ids.map((id) => (0, index_1.tagId)(meta, id));
674
- const idsToLoad = ids.filter((id) => !this.findExistingInstance(id));
675
- if (idsToLoad.length > 0) {
712
+ // Use pre-allocated arrays/for loops instead of `.filter`s since this can be a hot spot
713
+ const ids = new Array(_ids.length);
714
+ const entities = new Array(_ids.length);
715
+ let idsToLoad;
716
+ let positionsToLoad;
717
+ for (let i = 0; i < _ids.length; i++) {
718
+ const id = (0, index_1.tagId)(meta, _ids[i]);
719
+ ids[i] = id;
720
+ const entity = this.findExistingInstance(id);
721
+ if (entity) {
722
+ entities[i] = entity;
723
+ }
724
+ else {
725
+ (idsToLoad ??= []).push(id);
726
+ (positionsToLoad ??= []).push(i);
727
+ }
728
+ }
729
+ if (idsToLoad && idsToLoad.length > 0) {
676
730
  await (0, loadBatchLoader_1.loadBatchLoader)(this, meta)
677
731
  .loadAll(idsToLoad.map((id) => ({ taggedId: id, hint })))
678
732
  .catch(function loadAll(err) {
679
733
  throw appendStack(err, new Error());
680
734
  });
735
+ for (const i of positionsToLoad) {
736
+ entities[i] = this.findExistingInstance(ids[i]);
737
+ }
681
738
  }
682
- const entities = [];
683
- for (const id of ids) {
684
- const entity = this.findExistingInstance(id);
685
- if (entity)
686
- entities.push(entity);
739
+ let idsNotFound;
740
+ for (let i = 0; i < entities.length; i++) {
741
+ if (entities[i] === undefined) {
742
+ (idsNotFound ??= []).push(ids[i]);
743
+ }
687
744
  }
688
- if (entities.length !== ids.length) {
689
- const idsNotFound = ids.filter((_, i) => entities[i] === undefined);
745
+ if (idsNotFound) {
690
746
  throw new NotFoundError(`${idsNotFound.join(",")} were not found`);
691
747
  }
748
+ const loadedEntities = entities;
692
749
  if (hint) {
693
- await this.populate(entities, hint);
750
+ await this.populate(loadedEntities, hint);
694
751
  }
695
752
  if (meta.inheritanceType === "sti" && meta.baseType) {
696
- const wrongType = entities.filter((e) => !(e instanceof meta.cstr));
753
+ const wrongType = loadedEntities.filter((e) => !(e instanceof meta.cstr));
697
754
  if (wrongType.length > 0) {
698
755
  throw new Error(`${wrongType.join(", ")} were not of type ${meta.cstr.name}`);
699
756
  }
700
757
  }
701
- return entities;
758
+ return loadedEntities;
702
759
  }
703
760
  async loadAllIfExists(type, _ids, hint) {
704
761
  const meta = (0, index_1.getMetadata)(type);
705
- const ids = _ids.map((id) => (0, index_1.tagId)(meta, id));
706
- const idsToLoad = ids.filter((id) => !this.findExistingInstance(id));
707
- if (idsToLoad.length > 0) {
762
+ // Use pre-allocated arrays/for loops instead of `.filter`s since this can be a hot spot
763
+ const ids = new Array(_ids.length);
764
+ const entities = [];
765
+ let idsToLoad;
766
+ // Ensure the ids are tagged, and find any not-yet-loaded
767
+ for (let i = 0; i < _ids.length; i++) {
768
+ const id = (0, index_1.tagId)(meta, _ids[i]);
769
+ ids[i] = id;
770
+ const entity = this.findExistingInstance(id);
771
+ if (entity) {
772
+ entities.push(entity);
773
+ }
774
+ else {
775
+ (idsToLoad ??= []).push(id);
776
+ }
777
+ }
778
+ if (idsToLoad && idsToLoad.length > 0) {
708
779
  await (0, loadBatchLoader_1.loadBatchLoader)(this, meta)
709
780
  .loadAll(idsToLoad.map((id) => ({ taggedId: id, hint })))
710
781
  .catch(function loadAllIfExists(err) {
711
782
  throw appendStack(err, new Error());
712
783
  });
784
+ // Now that everything is loaded, recalc `entities`
785
+ entities.length = 0;
786
+ for (const id of ids) {
787
+ const entity = this.findExistingInstance(id);
788
+ if (entity)
789
+ entities.push(entity);
790
+ }
713
791
  }
714
- const entities = ids.map((id) => this.findExistingInstance(id)).filter(Boolean);
715
792
  if (hint) {
716
793
  await this.populate(entities, hint);
717
794
  }
@@ -762,6 +839,12 @@ class EntityManager {
762
839
  if (list.length === 0) {
763
840
  return !fn ? entityOrList : fn(entityOrList);
764
841
  }
842
+ // Avoid building a HintTree/batchloader for persisted entities when everything is already loaded; in the phase 9
843
+ // benchmark this moved already-loaded populates from ~1.10ms to ~0.89ms, and nested already-loaded populates from
844
+ // ~2.39ms to ~1.94ms.
845
+ if (!opts.forceReload && list.every((entity) => !entity.isNewEntity && (0, loadHints_1.isLoadedForPopulate)(entity, hintOpt))) {
846
+ return fn ? fn(entityOrList) : entityOrList;
847
+ }
765
848
  const meta = (0, index_1.getMetadata)(list[0]);
766
849
  if (this.#preloader) {
767
850
  // If we can preload, prevent promise deadlocking by one large-batch preload populate (which can't have
@@ -810,9 +893,11 @@ class EntityManager {
810
893
  delete(entityOrArray) {
811
894
  for (const entity of (0, utils_1.toArray)(entityOrArray)) {
812
895
  // Early return if already deleted.
813
- const alreadyMarked = (0, BaseEntity_1.getInstanceData)(entity).markDeleted(entity);
896
+ const alreadyMarked = (0, BaseEntity_1.getInstanceData)(entity).markDeleted();
814
897
  if (!alreadyMarked)
815
898
  continue;
899
+ // This monotonic flag lets find hot paths skip scanning results until a delete has ever happened in this EM.
900
+ this.#hasAnyDeletes = true;
816
901
  // Any derived fields that read this entity will need recalc-d
817
902
  this.#rm.queueAllDownstreamFields(entity, "deleted");
818
903
  // Synchronously unhook the entity if the relations are loaded
@@ -906,7 +991,7 @@ class EntityManager {
906
991
  const pendingHooks = new Set();
907
992
  // Subset of pendingFlush entities that had hooks invoked in a prior `runHooksOnPendingEntities`
908
993
  const alreadyRanHooks = new Set();
909
- findPendingFlushEntities(this.entities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks);
994
+ findPendingFlushEntities(this.#maybePendingFlushEntities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks);
910
995
  // If we're re-looping for AsyncReactiveField, make sure to bump updatedAt
911
996
  // each time, so that for an INSERT-then-UPDATE the triggers don't think the
912
997
  // UPDATE forgot to self-bump updatedAt, and then "helpfully" bump it for us.
@@ -930,9 +1015,13 @@ class EntityManager {
930
1015
  // fields, they'd be via the synchronous getter and would not be stale.
931
1016
  recalcSynchronousDerivedFields(todos);
932
1017
  // The hooks could have deleted this-loop or prior-loop entities, so re-cascade again.
933
- await this.flushDeletes();
934
- // The hooks could have changed fields, so recalc again.
935
- await this.#rm.recalcPendingReactables("reactables");
1018
+ // Reactions (the recalc below) can themselves `em.delete` entities, so keep draining
1019
+ // until both deletes and recalcs have settled.
1020
+ do {
1021
+ await this.flushDeletes();
1022
+ // The hooks could have changed fields, so recalc again.
1023
+ await this.#rm.recalcPendingReactables("reactables");
1024
+ } while (this.#pendingDeletes.length > 0);
936
1025
  // We may have reactables that failed earlier, but will succeed now that hooks have been run and cascade
937
1026
  // deletes have been processed
938
1027
  if (this.#rm.hasPendingTypeErrors) {
@@ -945,7 +1034,7 @@ class EntityManager {
945
1034
  hooksInvoked.add(e);
946
1035
  pendingHooks.clear();
947
1036
  // See if the hooks mutated any new, not-yet-hooksInvoked entities
948
- findPendingFlushEntities(this.entities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks);
1037
+ findPendingFlushEntities(this.#maybePendingFlushEntities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks);
949
1038
  // The final run of recalcPendingReactables could have left us with pending type errors and no entities in
950
1039
  // pendingHooks. If so, we need to re-run recalcPendingTypeErrors to get those errors to transition into
951
1040
  // suppressed errors so that we will fail after simpleValidation.
@@ -1004,7 +1093,7 @@ class EntityManager {
1004
1093
  // The driver will handle the right thing if we're already in an existing transaction.
1005
1094
  await this.driver.transaction(this, async () => {
1006
1095
  do {
1007
- if (Object.keys(entityTodos).length > 0 || Object.keys(joinRowTodos)) {
1096
+ if (Object.keys(entityTodos).length > 0 || Object.keys(joinRowTodos).length > 0) {
1008
1097
  await this.driver.flush(this, entityTodos, joinRowTodos);
1009
1098
  }
1010
1099
  // Now that we've flushed, we can let plugins know what we've done.
@@ -1080,7 +1169,7 @@ class EntityManager {
1080
1169
  for (const e of createdThenDeleted)
1081
1170
  (0, BaseEntity_1.getInstanceData)(e).fixupCreatedThenDeleted();
1082
1171
  this.#merging?.clear();
1083
- return [...allFlushedEntities];
1172
+ return [...allFlushedEntities].sort((a, b) => (0, BaseEntity_1.getInstanceData)(a).entityIndex - (0, BaseEntity_1.getInstanceData)(b).entityIndex);
1084
1173
  }
1085
1174
  catch (e) {
1086
1175
  if (e instanceof RecursiveCollection_1.RecursiveCycleError) {
@@ -1103,8 +1192,9 @@ class EntityManager {
1103
1192
  throw new index_1.ValidationErrors(message);
1104
1193
  }
1105
1194
  }
1106
- if (e instanceof InMemoryRollbackError)
1107
- return [...allFlushedEntities];
1195
+ if (e instanceof InMemoryRollbackError) {
1196
+ return [...allFlushedEntities].sort((a, b) => (0, BaseEntity_1.getInstanceData)(a).entityIndex - (0, BaseEntity_1.getInstanceData)(b).entityIndex);
1197
+ }
1108
1198
  throw e;
1109
1199
  }
1110
1200
  finally {
@@ -1212,10 +1302,13 @@ class EntityManager {
1212
1302
  */
1213
1303
  hydrate(type, rows, options) {
1214
1304
  const maybeBaseMeta = (0, index_1.getMetadata)(type);
1305
+ const taggedIdPrefix = `${maybeBaseMeta.tagName}:`;
1306
+ const overwriteExisting = options?.overwriteExisting === true;
1215
1307
  let i = 0;
1216
1308
  const entities = new Array(rows.length);
1217
1309
  for (const row of rows) {
1218
- const taggedId = (0, index_1.keyToTaggedId)(maybeBaseMeta, row["id"]) || (0, utils_1.fail)("No id column was available");
1310
+ const id = row["id"];
1311
+ const taggedId = id === undefined || id === null ? (0, utils_1.fail)("No id column was available") : `${taggedIdPrefix}${id}`;
1219
1312
  // See if this is already in our UoW
1220
1313
  let entity = this.findExistingInstance(taggedId);
1221
1314
  if (!entity) {
@@ -1224,23 +1317,36 @@ class EntityManager {
1224
1317
  // Pass id as a hint that we're in hydrate mode
1225
1318
  entity = (0, newEntity_1.newEntity)(this, (0, index_1.asConcreteCstr)(meta.cstr), false);
1226
1319
  (0, BaseEntity_1.getInstanceData)(entity).row = row;
1227
- this.#doRegister(entity, taggedId);
1320
+ this.#doRegister(entity, taggedId, meta, true);
1228
1321
  }
1229
- else if (options?.overwriteExisting === true) {
1322
+ else if (overwriteExisting) {
1230
1323
  // Usually if the entity already exists, we don't write over it, but in this case we assume that
1231
1324
  // `EntityManager.refresh` is telling us to explicitly load the latest data.
1232
1325
  // First swap out the old row with the new row
1233
- (0, BaseEntity_1.getInstanceData)(entity).row = row;
1326
+ const instanceData = (0, BaseEntity_1.getInstanceData)(entity);
1327
+ instanceData.row = row;
1234
1328
  // And then only refresh the data keys that have already been serde-d from rows
1235
1329
  // (this keeps us from deserializing data out of rows that we don't need).
1236
- const { data, originalData } = (0, BaseEntity_1.getInstanceData)(entity);
1237
- const changedFields = entity.changes.fieldsWithoutRelations;
1238
- for (const fieldName of Object.keys(data)) {
1239
- const serde = (0, index_1.getMetadata)(entity).allFields[fieldName].serde ?? (0, utils_1.fail)(`Missing serde for ${fieldName}`);
1240
- serde.setOnEntity(data, row);
1241
- // Make the field look not-dirty
1242
- if (changedFields.includes(fieldName)) {
1243
- delete originalData[fieldName];
1330
+ const { data } = instanceData;
1331
+ const dataKeys = Object.keys(data);
1332
+ if (dataKeys.length > 0) {
1333
+ const allFields = (0, index_1.getMetadata)(entity).allFields;
1334
+ const changedFields = entity.changes.fieldsWithoutRelations;
1335
+ if (changedFields.length === 0) {
1336
+ for (const fieldName of dataKeys) {
1337
+ const serde = allFields[fieldName].serde ?? (0, utils_1.fail)(`Missing serde for ${fieldName}`);
1338
+ serde.setOnEntity(data, row);
1339
+ }
1340
+ }
1341
+ else {
1342
+ for (const fieldName of dataKeys) {
1343
+ const serde = allFields[fieldName].serde ?? (0, utils_1.fail)(`Missing serde for ${fieldName}`);
1344
+ serde.setOnEntity(data, row);
1345
+ // Make the field look not-dirty
1346
+ if (changedFields.includes(fieldName)) {
1347
+ instanceData.markFieldClean(fieldName);
1348
+ }
1349
+ }
1244
1350
  }
1245
1351
  }
1246
1352
  }
@@ -1249,8 +1355,9 @@ class EntityManager {
1249
1355
  return entities;
1250
1356
  }
1251
1357
  touch(entityOrEntities) {
1252
- for (const entity of (0, utils_1.toArray)(entityOrEntities))
1253
- (0, BaseEntity_1.getInstanceData)(entity).isTouched = true;
1358
+ for (const entity of (0, utils_1.toArray)(entityOrEntities)) {
1359
+ (0, BaseEntity_1.getInstanceData)(entity).markTouched();
1360
+ }
1254
1361
  }
1255
1362
  async recalc(entityOrEntities) {
1256
1363
  // Look for async reactive fields
@@ -1327,9 +1434,16 @@ class EntityManager {
1327
1434
  // Run the beforeDelete hook before we unhook the entity
1328
1435
  const todos = (0, Todo_1.createTodos)(entities);
1329
1436
  await beforeDelete(this.ctx, todos);
1330
- // For all relations, unhook the entity from the other side
1331
- // (...we're using `concat` because `.push(...reallyBigArray)` with ~100k relations can blow the stack size
1332
- relationsToCleanup = relationsToCleanup.concat(entities.flatMap(index_1.getRelations));
1437
+ // For all relations, unhook the entity from the other side; this append path is optimized for
1438
+ // large deletes with ~100k relations by avoiding `flatMap` intermediates and `concat` copies.
1439
+ for (const entity of entities) {
1440
+ const relations = (0, index_1.getRelations)(entity);
1441
+ const start = relationsToCleanup.length;
1442
+ relationsToCleanup.length += relations.length;
1443
+ for (let i = 0; i < relations.length; i++) {
1444
+ relationsToCleanup[start + i] = relations[i];
1445
+ }
1446
+ }
1333
1447
  entities = this.#pendingDeletes;
1334
1448
  this.#pendingDeletes = [];
1335
1449
  }
@@ -1385,9 +1499,9 @@ class EntityManager {
1385
1499
  return entities.filter((e) => e instanceof cstr && !e.isDeletedEntity);
1386
1500
  }
1387
1501
  if (this.#indexManager.shouldIndexType(entities.length)) {
1388
- this.#indexManager.enableIndexingForType(meta, entities);
1502
+ this.#indexManager.enableIndexingForType(meta, entities, where);
1389
1503
  return (this.#indexManager
1390
- .findMatching(meta, where)
1504
+ .findMatching(meta, entities, where)
1391
1505
  // Still filter by `instanceof cstr` to handle subtyping
1392
1506
  .filter((e) => e instanceof cstr && !e.isDeletedEntity));
1393
1507
  }
@@ -1487,7 +1601,8 @@ class EntityManager {
1487
1601
  continue;
1488
1602
  const { originalData: oldOriginalData, data: oldData } = oldInstanceData;
1489
1603
  const newEntity = mapEntity(oldEntity);
1490
- const { originalData: newOriginalData, data: newData } = newEntity.__data;
1604
+ const newInstanceData = newEntity.__data;
1605
+ const { data: newData } = newInstanceData;
1491
1606
  // for new entities, anything in `data` is changed and should be copied across. for existing entities, we
1492
1607
  // only care about changed fields, which are enumerated by originalData
1493
1608
  const maybeEntity = (value) => ((0, Entity_1.isEntity)(value) ? mapEntity(value) : value);
@@ -1495,7 +1610,7 @@ class EntityManager {
1495
1610
  for (const field of fields) {
1496
1611
  // copy over originalData so .changes is consistent across ems
1497
1612
  if (field in oldOriginalData)
1498
- newOriginalData[field] = maybeEntity(oldOriginalData[field]);
1613
+ newInstanceData.markFieldDirty(field, maybeEntity(oldOriginalData[field]));
1499
1614
  newData[field] = maybeEntity(oldData[field]);
1500
1615
  }
1501
1616
  }
@@ -1505,7 +1620,7 @@ class EntityManager {
1505
1620
  const newEntity = mapEntity(oldEntity);
1506
1621
  if (oldEntity.isDeletedEntity) {
1507
1622
  // If the old entity was deleted, that should be persisted in the new em
1508
- newEntity.__data.markDeleted(newEntity);
1623
+ newEntity.__data.markDeleted();
1509
1624
  // deleted entities will fail if you try to `get` their relations, so skip them since they should be cleared
1510
1625
  // out regardless
1511
1626
  continue;
@@ -1650,11 +1765,11 @@ class EntityManager {
1650
1765
  }
1651
1766
  }
1652
1767
  /** Registers a newly-instantiated entity with our EntityManager; only called by #doCreate and hydrate. */
1653
- #doRegister(entity, id) {
1768
+ #doRegister(entity, id, meta, skipDuplicateCheck = false) {
1654
1769
  // Keep our indexes up to date...
1655
1770
  const maybeId = id ?? entity.idTaggedMaybe;
1656
1771
  if (maybeId) {
1657
- if (this.findExistingInstance(maybeId) !== undefined) {
1772
+ if (!skipDuplicateCheck && this.findExistingInstance(maybeId) !== undefined) {
1658
1773
  throw new Error(`Entity ${entity} has a duplicate instance already loaded`);
1659
1774
  }
1660
1775
  this.#entitiesById.set(maybeId, entity);
@@ -1663,8 +1778,9 @@ class EntityManager {
1663
1778
  // Also register by the `a#1` style tagged string for new entities
1664
1779
  this.#entitiesById.set(entity.toTaggedString(), entity);
1665
1780
  }
1781
+ (0, BaseEntity_1.getInstanceData)(entity).entityIndex = this.#entitiesArray.length;
1666
1782
  this.#entitiesArray.push(entity);
1667
- const meta = (0, index_1.getMetadata)(entity);
1783
+ meta ??= (0, index_1.getMetadata)(entity);
1668
1784
  const set = this.#entitiesByTag.get(meta.tagName) ?? [];
1669
1785
  if (set.length === 0)
1670
1786
  this.#entitiesByTag.set(meta.tagName, set);
@@ -1678,8 +1794,6 @@ class EntityManager {
1678
1794
  .join(", ");
1679
1795
  throw new Error(`More than ${this.entityLimit} entities have been instantiated (top entities: ${topTypes})`);
1680
1796
  }
1681
- // If indexing is enabled for this type, add it...
1682
- this.#indexManager.maybeIndexEntity(entity);
1683
1797
  }
1684
1798
  }
1685
1799
  exports.EntityManager = EntityManager;
@@ -1763,14 +1877,14 @@ async function validateReactiveRules(em, logger, todos, joinRowTodos) {
1763
1877
  const p1 = Object.values(todos).flatMap((todo) => {
1764
1878
  const entities = [...todo.inserts, ...todo.updates, ...todo.deletes];
1765
1879
  // Find each statically-declared reactive rule for the given entity type
1766
- const rules = (0, caches_1.getReactiveRules)(todo.metadata);
1880
+ const rules = todo.metadata.reactiveRules;
1767
1881
  return rules.map((rule) => {
1768
1882
  // Of all changed entities of this type, how many specifically trigger this rule?
1769
1883
  const triggered = entities.filter((e) => {
1770
1884
  // If the rule is for a different subtype, skip it
1771
1885
  if (!(e instanceof rule.source))
1772
1886
  return false;
1773
- // Any new-or-deleted entity fires every rule (getReactiveRules has already filtered out read-only)
1887
+ // Any new-or-deleted entity fires every rule (reactiveRules has already filtered out read-only)
1774
1888
  if (e.isNewEntity || e.isDeletedEntity)
1775
1889
  return true;
1776
1890
  // Otherwise see if the changed fields overlaps with the rule's fields
@@ -1786,22 +1900,26 @@ async function validateReactiveRules(em, logger, todos, joinRowTodos) {
1786
1900
  });
1787
1901
  });
1788
1902
  const p2 = Object.values(joinRowTodos).flatMap((todo) => {
1789
- const entities = [...todo.newRows, ...todo.deletedRows].flatMap((jr) => Object.values(jr.columns));
1790
- // Do the first side
1791
- const p1 = (0, caches_1.getReactiveRules)(todo.m2m.meta)
1792
- .filter((rule) => rule.fields.includes(todo.m2m.fieldName))
1903
+ // For enum m2ms the columns include the enum's numeric id, so keep only the entity values.
1904
+ const entities = [...todo.newRows, ...todo.deletedRows].flatMap((jr) => Object.values(jr.columns)).filter(Entity_1.isEntity);
1905
+ // The owning-entity side (applies to both entity-to-entity and entity-to-enum m2ms).
1906
+ const a = todo.m2m.meta
1907
+ .reactiveRules.filter((rule) => rule.fields.includes(todo.m2m.fieldName))
1793
1908
  .map((rule) => {
1794
1909
  const triggered = entities.filter((e) => e instanceof todo.m2m.meta.cstr);
1795
1910
  return followAndQueue(triggered, rule);
1796
1911
  });
1797
- // And the second side
1798
- const p2 = (0, caches_1.getReactiveRules)(todo.m2m.otherMeta)
1799
- .filter((rule) => rule.fields.includes(todo.m2m.otherFieldName))
1800
- .map((rule) => {
1801
- const triggered = entities.filter((e) => e instanceof todo.m2m.otherMeta.cstr);
1802
- return followAndQueue(triggered, rule);
1803
- });
1804
- return [...p1, ...p2];
1912
+ // The "other" side only exists for entity-to-entity m2ms; enums have no reverse field.
1913
+ const { otherMeta, otherFieldName } = todo.m2m;
1914
+ const b = otherMeta
1915
+ ? otherMeta
1916
+ .reactiveRules.filter((rule) => rule.fields.includes(otherFieldName))
1917
+ .map((rule) => {
1918
+ const triggered = entities.filter((e) => e instanceof otherMeta.cstr);
1919
+ return followAndQueue(triggered, rule);
1920
+ })
1921
+ : [];
1922
+ return [...a, ...b];
1805
1923
  });
1806
1924
  (0, utils_1.failIfAnyRejected)(await Promise.allSettled([...p1, ...p2]));
1807
1925
  // Now that we've found the fn+entities to run, run them and collect any errors
@@ -1883,7 +2001,11 @@ function entitiesFromTodos(entityTodos, joinRowTodos) {
1883
2001
  }
1884
2002
  for (const todo of Object.values(joinRowTodos)) {
1885
2003
  [...todo.newRows, ...todo.deletedRows].forEach((row) => {
1886
- Object.values(row.columns).forEach((entity) => entities.add(entity));
2004
+ // For enum m2ms, a column value is the enum's numeric id rather than an entity.
2005
+ Object.values(row.columns).forEach((value) => {
2006
+ if ((0, Entity_1.isEntity)(value))
2007
+ entities.add(value);
2008
+ });
1887
2009
  });
1888
2010
  }
1889
2011
  return [...entities];
@@ -2025,7 +2147,7 @@ function maybeBumpUpdatedAt(rm, todos, now) {
2025
2147
  // it has changed. This is technically true, but this will break the oplock SQL generation,
2026
2148
  // so force the field to be dirty.
2027
2149
  const orm = (0, BaseEntity_1.getInstanceData)(e);
2028
- orm.originalData[updatedAt] = (0, fields_1.getField)(e, updatedAt);
2150
+ orm.markFieldDirty(updatedAt, (0, fields_1.getField)(e, updatedAt));
2029
2151
  const serde = todo.metadata.fields[updatedAt].serde;
2030
2152
  orm.data[updatedAt] = serde.mapFromNow(now);
2031
2153
  rm.queueDownstreamReactables(e, updatedAt);
@@ -2062,17 +2184,13 @@ function findConcreteMeta(maybeBaseMeta, row) {
2062
2184
  throw new Error(`${maybeBaseMeta.type} ${(0, index_1.tagId)(maybeBaseMeta, row.id)} must be instantiated via a subtype`);
2063
2185
  }
2064
2186
  // Look for the CTI __class from the driver telling us which subtype to instantiate
2065
- return maybeBaseMeta.subTypes.find((st) => st.type === row.__class) ?? maybeBaseMeta;
2187
+ return maybeBaseMeta.subTypesByType.get(row.__class) ?? maybeBaseMeta;
2066
2188
  }
2067
2189
  else if (maybeBaseMeta.inheritanceType === "sti") {
2068
2190
  // Look for the STI discriminator value
2069
2191
  const baseMeta = (0, index_1.getBaseMeta)(maybeBaseMeta);
2070
- const field = baseMeta.fields[baseMeta.stiDiscriminatorField];
2071
- if (field.kind !== "enum")
2072
- throw new Error("Discriminator field must be an enum");
2073
- const columnName = field.serde.columns[0].columnName;
2074
- const value = row[columnName];
2075
- return baseMeta.subTypes.find((st) => st.stiDiscriminatorValue === value) ?? baseMeta;
2192
+ const value = row[baseMeta.stiDiscriminatorColumnName];
2193
+ return baseMeta.subTypesByStiValue.get(value) ?? baseMeta;
2076
2194
  }
2077
2195
  else {
2078
2196
  throw new Error("Unknown inheritance type");
@@ -2081,7 +2199,7 @@ function findConcreteMeta(maybeBaseMeta, row) {
2081
2199
  /** Sets the `Animal.type` enum to the right subtype value. */
2082
2200
  function setStiDiscriminatorValue(baseMeta, entity) {
2083
2201
  const typeName = entity.constructor.name;
2084
- const st = baseMeta.subTypes.find((st) => st.type === typeName);
2202
+ const st = baseMeta.subTypesByType.get(typeName);
2085
2203
  if (st) {
2086
2204
  const field = baseMeta.fields[baseMeta.stiDiscriminatorField];
2087
2205
  const code = field.enumDetailType.findById(st.stiDiscriminatorValue).code;
@@ -2091,19 +2209,25 @@ function setStiDiscriminatorValue(baseMeta, entity) {
2091
2209
  entity[baseMeta.stiDiscriminatorField] = undefined;
2092
2210
  }
2093
2211
  }
2094
- function findPendingFlushEntities(entities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks) {
2095
- for (const e of entities) {
2096
- if ((0, BaseEntity_1.getInstanceData)(e).pendingOperation !== "none") {
2097
- if (!hooksInvoked.has(e)) {
2098
- pendingHooks.add(e);
2099
- }
2100
- else {
2101
- alreadyRanHooks.add(e);
2102
- }
2103
- pendingFlush.add(e);
2212
+ function findPendingFlushEntities(maybePendingFlushEntities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks) {
2213
+ for (const e of maybePendingFlushEntities) {
2214
+ if ((0, BaseEntity_1.getInstanceData)(e).pendingOperation === "none") {
2215
+ maybePendingFlushEntities.delete(e);
2216
+ continue;
2104
2217
  }
2218
+ if (!hooksInvoked.has(e)) {
2219
+ pendingHooks.add(e);
2220
+ }
2221
+ else {
2222
+ alreadyRanHooks.add(e);
2223
+ }
2224
+ pendingFlush.add(e);
2105
2225
  }
2106
2226
  }
2227
+ /** Returns true if the caller explicitly asked `find` to use SQL pagination. */
2228
+ function hasPaginationSettings(options) {
2229
+ return "limit" in options || "offset" in options;
2230
+ }
2107
2231
  /** An error we throw to get knex to `ROLLBACK`, but then catch. */
2108
2232
  class InMemoryRollbackError extends Error {
2109
2233
  }