joist-core 2.1.0 → 2.2.0-next.1

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 (165) hide show
  1. package/build/ConditionBuilder.d.ts +1 -22
  2. package/build/ConditionBuilder.d.ts.map +1 -1
  3. package/build/ConditionBuilder.js +6 -72
  4. package/build/ConditionBuilder.js.map +1 -1
  5. package/build/EntityFilter.d.ts +4 -2
  6. package/build/EntityFilter.d.ts.map +1 -1
  7. package/build/EntityGraphQLFilter.d.ts +2 -0
  8. package/build/EntityGraphQLFilter.d.ts.map +1 -1
  9. package/build/EntityGraphQLFilter.js.map +1 -1
  10. package/build/EntityManager.d.ts +25 -39
  11. package/build/EntityManager.d.ts.map +1 -1
  12. package/build/EntityManager.js +96 -44
  13. package/build/EntityManager.js.map +1 -1
  14. package/build/EntityMetadata.d.ts +2 -0
  15. package/build/EntityMetadata.d.ts.map +1 -1
  16. package/build/EntityMetadata.js.map +1 -1
  17. package/build/InstanceData.d.ts +7 -1
  18. package/build/InstanceData.d.ts.map +1 -1
  19. package/build/InstanceData.js +36 -7
  20. package/build/InstanceData.js.map +1 -1
  21. package/build/IsLoadedCache.d.ts +1 -1
  22. package/build/IsLoadedCache.d.ts.map +1 -1
  23. package/build/IsLoadedCache.js +6 -1
  24. package/build/IsLoadedCache.js.map +1 -1
  25. package/build/JoinRows.d.ts +1 -1
  26. package/build/JoinRows.js +6 -1
  27. package/build/JoinRows.js.map +1 -1
  28. package/build/PluginManager.d.ts +12 -0
  29. package/build/PluginManager.d.ts.map +1 -1
  30. package/build/PluginManager.js +18 -2
  31. package/build/PluginManager.js.map +1 -1
  32. package/build/QueryParser.collectionJoins.d.ts +27 -0
  33. package/build/QueryParser.collectionJoins.d.ts.map +1 -0
  34. package/build/QueryParser.collectionJoins.js +460 -0
  35. package/build/QueryParser.collectionJoins.js.map +1 -0
  36. package/build/QueryParser.collectionJoins.test.d.ts +2 -0
  37. package/build/QueryParser.collectionJoins.test.d.ts.map +1 -0
  38. package/build/QueryParser.collectionJoins.test.js +772 -0
  39. package/build/QueryParser.collectionJoins.test.js.map +1 -0
  40. package/build/QueryParser.d.ts +67 -9
  41. package/build/QueryParser.d.ts.map +1 -1
  42. package/build/QueryParser.js +39 -33
  43. package/build/QueryParser.js.map +1 -1
  44. package/build/QueryParser.pruning.d.ts +4 -2
  45. package/build/QueryParser.pruning.d.ts.map +1 -1
  46. package/build/QueryParser.pruning.js +80 -10
  47. package/build/QueryParser.pruning.js.map +1 -1
  48. package/build/QueryParser.pruning.test.d.ts +2 -0
  49. package/build/QueryParser.pruning.test.d.ts.map +1 -0
  50. package/build/QueryParser.pruning.test.js +106 -0
  51. package/build/QueryParser.pruning.test.js.map +1 -0
  52. package/build/QueryVisitor.d.ts.map +1 -1
  53. package/build/QueryVisitor.js +22 -0
  54. package/build/QueryVisitor.js.map +1 -1
  55. package/build/ReactionsManager.js +2 -2
  56. package/build/batchloaders/BatchLoader.d.ts.map +1 -1
  57. package/build/batchloaders/BatchLoader.js +4 -1
  58. package/build/batchloaders/BatchLoader.js.map +1 -1
  59. package/build/batchloaders/manyToManyBatchLoader.js +3 -1
  60. package/build/batchloaders/manyToManyBatchLoader.js.map +1 -1
  61. package/build/batchloaders/recursiveM2mBatchLoader.d.ts.map +1 -1
  62. package/build/batchloaders/recursiveM2mBatchLoader.js +3 -1
  63. package/build/batchloaders/recursiveM2mBatchLoader.js.map +1 -1
  64. package/build/changes.d.ts.map +1 -1
  65. package/build/changes.js +1 -4
  66. package/build/changes.js.map +1 -1
  67. package/build/config.d.ts.map +1 -1
  68. package/build/config.js +18 -10
  69. package/build/config.js.map +1 -1
  70. package/build/configure.js +2 -2
  71. package/build/dataloaders/findCountDataLoader.js +2 -2
  72. package/build/dataloaders/findCountDataLoader.js.map +1 -1
  73. package/build/dataloaders/findDataLoader.d.ts.map +1 -1
  74. package/build/dataloaders/findDataLoader.js +13 -18
  75. package/build/dataloaders/findDataLoader.js.map +1 -1
  76. package/build/dataloaders/findIdsDataLoader.js +2 -2
  77. package/build/dataloaders/findIdsDataLoader.js.map +1 -1
  78. package/build/dataloaders/findOrCreateDataLoader.d.ts.map +1 -1
  79. package/build/dataloaders/findOrCreateDataLoader.js +6 -1
  80. package/build/dataloaders/findOrCreateDataLoader.js.map +1 -1
  81. package/build/dataloaders/findPaginatedDataLoader.d.ts +8 -0
  82. package/build/dataloaders/findPaginatedDataLoader.d.ts.map +1 -0
  83. package/build/dataloaders/findPaginatedDataLoader.js +71 -0
  84. package/build/dataloaders/findPaginatedDataLoader.js.map +1 -0
  85. package/build/defaults.d.ts.map +1 -1
  86. package/build/defaults.js +49 -42
  87. package/build/defaults.js.map +1 -1
  88. package/build/drivers/buildRawQuery.d.ts +6 -4
  89. package/build/drivers/buildRawQuery.d.ts.map +1 -1
  90. package/build/drivers/buildRawQuery.js +6 -5
  91. package/build/drivers/buildRawQuery.js.map +1 -1
  92. package/build/drivers/buildUtils.d.ts +7 -2
  93. package/build/drivers/buildUtils.d.ts.map +1 -1
  94. package/build/drivers/buildUtils.js +14 -5
  95. package/build/drivers/buildUtils.js.map +1 -1
  96. package/build/fields.d.ts.map +1 -1
  97. package/build/fields.js +16 -7
  98. package/build/fields.js.map +1 -1
  99. package/build/index.d.ts +2 -1
  100. package/build/index.d.ts.map +1 -1
  101. package/build/index.js +7 -5
  102. package/build/index.js.map +1 -1
  103. package/build/json.d.ts +3 -3
  104. package/build/json.d.ts.map +1 -1
  105. package/build/json.js +4 -4
  106. package/build/json.js.map +1 -1
  107. package/build/loadHints.d.ts +4 -4
  108. package/build/loadHints.d.ts.map +1 -1
  109. package/build/loadLens.js +1 -1
  110. package/build/loadLens.js.map +1 -1
  111. package/build/preloading/JsonAggregatePreloader.js +6 -2
  112. package/build/preloading/JsonAggregatePreloader.js.map +1 -1
  113. package/build/reactiveHints.d.ts +24 -7
  114. package/build/reactiveHints.d.ts.map +1 -1
  115. package/build/reactiveHints.js +45 -28
  116. package/build/reactiveHints.js.map +1 -1
  117. package/build/relations/AbstractRelationImpl.d.ts +7 -2
  118. package/build/relations/AbstractRelationImpl.d.ts.map +1 -1
  119. package/build/relations/AbstractRelationImpl.js.map +1 -1
  120. package/build/relations/AsyncProperty.d.ts +36 -0
  121. package/build/relations/AsyncProperty.d.ts.map +1 -0
  122. package/build/relations/AsyncProperty.js +80 -0
  123. package/build/relations/AsyncProperty.js.map +1 -0
  124. package/build/relations/{ReactiveQueryField.d.ts → AsyncReactiveField.d.ts} +10 -10
  125. package/build/relations/AsyncReactiveField.d.ts.map +1 -0
  126. package/build/relations/{ReactiveQueryField.js → AsyncReactiveField.js} +19 -19
  127. package/build/relations/{ReactiveQueryField.js.map → AsyncReactiveField.js.map} +1 -1
  128. package/build/relations/ReactiveField.d.ts +7 -9
  129. package/build/relations/ReactiveField.d.ts.map +1 -1
  130. package/build/relations/ReactiveField.js +5 -10
  131. package/build/relations/ReactiveField.js.map +1 -1
  132. package/build/relations/ReactiveGetter.d.ts +5 -5
  133. package/build/relations/ReactiveGetter.d.ts.map +1 -1
  134. package/build/relations/ReactiveGetter.js +3 -3
  135. package/build/relations/ReactiveGetter.js.map +1 -1
  136. package/build/relations/ReactiveReference.d.ts +2 -2
  137. package/build/relations/ReactiveReference.d.ts.map +1 -1
  138. package/build/relations/ReactiveReference.js +100 -36
  139. package/build/relations/ReactiveReference.js.map +1 -1
  140. package/build/relations/{hasAsyncProperty.d.ts → hasProperty.d.ts} +12 -12
  141. package/build/relations/hasProperty.d.ts.map +1 -0
  142. package/build/relations/{hasAsyncProperty.js → hasProperty.js} +20 -20
  143. package/build/relations/hasProperty.js.map +1 -0
  144. package/build/relations/index.d.ts +3 -2
  145. package/build/relations/index.d.ts.map +1 -1
  146. package/build/relations/index.js +16 -11
  147. package/build/relations/index.js.map +1 -1
  148. package/build/resurrection.d.ts +10 -0
  149. package/build/resurrection.d.ts.map +1 -0
  150. package/build/resurrection.js +93 -0
  151. package/build/resurrection.js.map +1 -0
  152. package/build/rules.js +3 -3
  153. package/build/trusted.d.ts +1 -1
  154. package/build/trusted.d.ts.map +1 -1
  155. package/build/trusted.js +1 -1
  156. package/build/trusted.js.map +1 -1
  157. package/build/upsert.d.ts.map +1 -1
  158. package/build/upsert.js +26 -10
  159. package/build/upsert.js.map +1 -1
  160. package/build/withLoaded.js +5 -5
  161. package/build/withLoaded.js.map +1 -1
  162. package/package.json +9 -9
  163. package/build/relations/ReactiveQueryField.d.ts.map +0 -1
  164. package/build/relations/hasAsyncProperty.d.ts.map +0 -1
  165. package/build/relations/hasAsyncProperty.js.map +0 -1
@@ -37,6 +37,7 @@ const findCountDataLoader_1 = require("./dataloaders/findCountDataLoader");
37
37
  const findDataLoader_1 = require("./dataloaders/findDataLoader");
38
38
  const findIdsDataLoader_1 = require("./dataloaders/findIdsDataLoader");
39
39
  const findOrCreateDataLoader_1 = require("./dataloaders/findOrCreateDataLoader");
40
+ const findPaginatedDataLoader_1 = require("./dataloaders/findPaginatedDataLoader");
40
41
  const Entity_1 = require("./Entity");
41
42
  const FlushLock_1 = require("./FlushLock");
42
43
  const index_1 = require("./index");
@@ -48,6 +49,7 @@ 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");
52
+ const AsyncProperty_1 = require("./relations/AsyncProperty");
51
53
  const hasAsyncMethod_1 = require("./relations/hasAsyncMethod");
52
54
  const RecursiveCollection_1 = require("./relations/RecursiveCollection");
53
55
  const Todo_1 = require("./Todo");
@@ -246,7 +248,10 @@ class EntityManager {
246
248
  async find(type, where, options) {
247
249
  const { populate, ...rest } = options || {};
248
250
  const settings = { where, ...rest };
249
- const result = await (0, findDataLoader_1.findDataLoader)(this, type, settings, populate)
251
+ const loader = hasPaginationSettings(rest)
252
+ ? (0, findPaginatedDataLoader_1.findPaginatedDataLoader)(this, type, settings, populate)
253
+ : (0, findDataLoader_1.findDataLoader)(this, type, settings, populate);
254
+ const result = await loader
250
255
  .load(settings)
251
256
  .catch(function find(err) {
252
257
  throw appendStack(err, new Error());
@@ -256,30 +261,51 @@ class EntityManager {
256
261
  }
257
262
  return result;
258
263
  }
259
- async findPaginated(type, where, options) {
260
- const { populate, limit, offset, ...rest } = options || {};
261
- const meta = (0, index_1.getMetadata)(type);
262
- const query = (0, index_1.parseFindQuery)(meta, where, rest);
263
- const rows = await this.executeFind(meta, "findPaginated", query, { limit, offset });
264
- // check row limit
265
- const result = this.hydrate(type, rows);
266
- if (populate) {
267
- await this.populate(result, populate);
268
- }
269
- return result;
270
- }
264
+ /** Runs the post-parse find pipeline: plugins mutate the logical AST, then Joist optimizes/prunes before SQL. */
271
265
  async executeFind(meta, operation, parsed, settings) {
266
+ const { checkLimit, driverSettings } = this.prepareFind(meta, operation, parsed, settings);
267
+ return this.executePreparedFind(meta, operation, parsed, driverSettings, checkLimit);
268
+ }
269
+ /** Executes a query that has already had find hooks and optimizations applied. */
270
+ async executePreparedFind(meta, operation, parsed, driverSettings, checkLimit) {
272
271
  const { pluginManager } = getEmInternalApi(this);
273
- pluginManager.beforeFind(meta, operation, parsed, settings);
274
- const rows = await this.driver.executeFind(this, parsed, settings);
272
+ const rows = await this.driver.executeFind(this, parsed, driverSettings);
273
+ // Check by default unless explicitly disabled or the caller removed the LIMIT via `limit: undefined`
274
+ const shouldCheck = checkLimit ?? !("limit" in driverSettings && driverSettings.limit === undefined);
275
+ if (shouldCheck && rows.length >= this.entityLimit) {
276
+ throw new Error(`Query returned more than ${this.entityLimit} entityLimit rows`);
277
+ }
275
278
  pluginManager.afterFind(meta, operation, rows);
276
279
  return rows;
277
280
  }
278
- async findGql(type, where, options) {
279
- return this.find(type, where, options);
281
+ /**
282
+ * Runs pre-SQL find hooks and optimizations against a parsed query.
283
+ *
284
+ * This allows plugins to see "pre-batched" / "logical" query ASTs, instead of our
285
+ * more complicated `_find` batched queries. The flow would be:
286
+ *
287
+ * - A loader calls `prepareFind(originalQuery)`
288
+ * - Plugins inspect/modify the query as/if needed
289
+ * - The loader crafts a new, more complicated query that embeds the originalQuery
290
+ * - The loader calls `executePreparedFind` with the 2nd query
291
+ */
292
+ prepareFind(meta, operation, parsed, settings) {
293
+ const { checkLimit, ...driverSettings } = settings;
294
+ const { pluginManager } = getEmInternalApi(this);
295
+ pluginManager.beforeFind(meta, operation, parsed, driverSettings);
296
+ (0, index_1.optimizeCollectionJoins)(parsed, settings);
297
+ return { checkLimit, driverSettings };
280
298
  }
281
- async findGqlPaginated(type, where, options) {
282
- return this.findPaginated(type, where, options);
299
+ async findGql(type, where, options) {
300
+ if (!options) {
301
+ return this.find(type, where);
302
+ }
303
+ const normalized = { ...options };
304
+ if ("limit" in normalized)
305
+ normalized.limit = normalized.limit ?? undefined;
306
+ if ("offset" in normalized)
307
+ normalized.offset = normalized.offset ?? undefined;
308
+ return this.find(type, where, normalized);
283
309
  }
284
310
  async findOne(type, where, options) {
285
311
  const list = await this.find(type, where, options);
@@ -878,11 +904,11 @@ class EntityManager {
878
904
  const createdThenDeleted = new Set();
879
905
  // We'll only invoke hooks once/entity (the 1st time that entity goes through runHooksOnPendingEntities)
880
906
  const hooksInvoked = new Set();
881
- // Make sure two ReactiveQueryFields don't ping-pong each other forever
907
+ // Make sure two AsyncReactiveFields don't ping-pong each other forever
882
908
  let hookLoops = 0;
883
909
  let now = getNow();
884
910
  const suppressedDefaultTypeErrors = [];
885
- // Make a lambda that we can invoke multiple times, if we loop for ReactiveQueryFields
911
+ // Make a lambda that we can invoke multiple times, if we loop for AsyncReactiveFields
886
912
  const runHooksOnPendingEntities = async () => {
887
913
  if (hookLoops++ >= 10)
888
914
  throw new Error("runHooksOnPendingEntities has ran 10 iterations, aborting");
@@ -899,18 +925,18 @@ class EntityManager {
899
925
  // Subset of pendingFlush entities that had hooks invoked in a prior `runHooksOnPendingEntities`
900
926
  const alreadyRanHooks = new Set();
901
927
  findPendingFlushEntities(this.entities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks);
902
- // If we're re-looping for ReactiveQueryField, make sure to bump updatedAt
928
+ // If we're re-looping for AsyncReactiveField, make sure to bump updatedAt
903
929
  // each time, so that for an INSERT-then-UPDATE the triggers don't think the
904
930
  // UPDATE forgot to self-bump updatedAt, and then "helpfully" bump it for us.
905
931
  if (alreadyRanHooks.size > 0) {
906
- maybeBumpUpdatedAt((0, Todo_1.createTodos)([...alreadyRanHooks]), now);
932
+ maybeBumpUpdatedAt(this.#rm, (0, Todo_1.createTodos)([...alreadyRanHooks]), now);
907
933
  }
908
934
  // Run hooks in a series of loops until things "settle down"
909
935
  while (pendingHooks.size > 0) {
910
936
  await this.#fl.allowWrites(async () => {
911
937
  let todos = (0, Todo_1.createTodos)([...pendingHooks]);
912
938
  await (0, defaults_1.setAsyncDefaults)(suppressedDefaultTypeErrors, this.ctx, Todo_1.Todo.groupInsertsByTypeAndSubType(todos));
913
- maybeBumpUpdatedAt(todos, now);
939
+ maybeBumpUpdatedAt(this.#rm, todos, now);
914
940
  // Run our hooks
915
941
  for (const group of maybeSetupHookOrdering(todos)) {
916
942
  await beforeCreate(this.ctx, group);
@@ -955,24 +981,30 @@ class EntityManager {
955
981
  return !createThenDelete;
956
982
  });
957
983
  };
958
- const runValidation = async (entityTodos, joinRowTodos) => {
984
+ const { pluginManager } = getEmInternalApi(this);
985
+ const runValidation = async (entityTodos, joinRowTodos, validate) => {
986
+ const changedEntities = entitiesFromTodos(entityTodos, joinRowTodos);
959
987
  try {
960
988
  this.#isValidating = true;
961
- // Run simple rules first b/c it includes not-null/required rules, so that then when we run
962
- // `validateReactiveRules` next, the app's lambdas won't see fundamentally invalid entities & NPE.
963
- await validateSimpleRules(entityTodos);
964
- // After we've let any "author is not set" simple rules fail before prematurely throwing
965
- // the "of course that caused an NPE" `TypeError`s, if all the authors *were* valid/set,
966
- // and we still have TypeErrors (from derived valeus), they were real, unrelated errors
967
- // that the user should see.
968
- if (suppressedDefaultTypeErrors.length > 0)
969
- throw suppressedDefaultTypeErrors[0];
970
- await validateReactiveRules(this, this.#rm.logger, entityTodos, joinRowTodos);
989
+ await pluginManager.beforeValidate(changedEntities);
990
+ if (validate) {
991
+ // Run simple rules first b/c it includes not-null/required rules, so that then when we run
992
+ // `validateReactiveRules` next, the app's lambdas won't see fundamentally invalid entities & NPE.
993
+ await validateSimpleRules(entityTodos);
994
+ // After we've let any "author is not set" simple rules fail before prematurely throwing
995
+ // the "of course that caused an NPE" `TypeError`s, if all the authors *were* valid/set,
996
+ // and we still have TypeErrors (from derived valeus), they were real, unrelated errors
997
+ // that the user should see.
998
+ if (suppressedDefaultTypeErrors.length > 0)
999
+ throw suppressedDefaultTypeErrors[0];
1000
+ await validateReactiveRules(this, this.#rm.logger, entityTodos, joinRowTodos);
1001
+ await afterValidation(this.ctx, entityTodos);
1002
+ }
1003
+ await pluginManager.afterValidate(changedEntities);
971
1004
  }
972
1005
  finally {
973
1006
  this.#isValidating = false;
974
1007
  }
975
- await afterValidation(this.ctx, entityTodos);
976
1008
  };
977
1009
  // Run hooks (in iterative loops if hooks mutate new entities) on pending entities
978
1010
  let entitiesToFlush = await runHooksOnPendingEntities();
@@ -982,13 +1014,10 @@ class EntityManager {
982
1014
  // the full set of entities that will be INSERT/UPDATE/DELETE-d in the database.
983
1015
  let entityTodos = (0, Todo_1.createTodos)(entitiesToFlush);
984
1016
  let joinRowTodos = (0, Todo_1.combineJoinRows)(this.#joinRows);
985
- if (!skipValidation) {
986
- await runValidation(entityTodos, joinRowTodos);
987
- }
1017
+ await runValidation(entityTodos, joinRowTodos, !skipValidation);
988
1018
  this.#rm.throwIfAnySuppressedTypeErrors();
989
1019
  if (suppressedDefaultTypeErrors.length > 0)
990
1020
  throw suppressedDefaultTypeErrors[0];
991
- const { pluginManager } = getEmInternalApi(this);
992
1021
  if (Object.keys(entityTodos).length > 0 || Object.keys(joinRowTodos).length > 0) {
993
1022
  // The driver will handle the right thing if we're already in an existing transaction.
994
1023
  await this.driver.transaction(this, async () => {
@@ -1009,7 +1038,7 @@ class EntityManager {
1009
1038
  // Actually do the recalc
1010
1039
  await this.#fl.allowWrites(async () => {
1011
1040
  await this.#rm.recalcPendingReactables("reactiveQueries");
1012
- // If any ReactiveFields depended on ReactiveQueryFields, go ahead and calc those now
1041
+ // If any ReactiveFields depended on AsyncReactiveFields, go ahead and calc those now
1013
1042
  await this.#rm.recalcPendingReactables("reactables");
1014
1043
  });
1015
1044
  // Advance `now` so that our triggers don't think our UPDATEs are forgetting to self-bump
@@ -1022,7 +1051,7 @@ class EntityManager {
1022
1051
  // Recreate `entityTodos` against the only-the-just-changed entities
1023
1052
  entityTodos = (0, Todo_1.createTodos)(entitiesToFlush);
1024
1053
  joinRowTodos = (0, Todo_1.combineJoinRows)(this.#joinRows);
1025
- await runValidation(entityTodos, joinRowTodos);
1054
+ await runValidation(entityTodos, joinRowTodos, true);
1026
1055
  this.#rm.throwIfAnySuppressedTypeErrors();
1027
1056
  }
1028
1057
  else {
@@ -1044,6 +1073,11 @@ class EntityManager {
1044
1073
  if (e.isNewEntity && !e.isDeletedEntity)
1045
1074
  this.#entitiesById.set(e.idTagged, e);
1046
1075
  (0, BaseEntity_1.getInstanceData)(e).resetAfterFlushed();
1076
+ // Reset AsyncQueryProperties since DB state may have changed
1077
+ for (const rel of Object.values((0, BaseEntity_1.getInstanceData)(e).relations)) {
1078
+ if (rel instanceof AsyncProperty_1.AsyncPropertyImpl)
1079
+ rel.resetAfterFlush();
1080
+ }
1047
1081
  }
1048
1082
  // Update the joinRows refs to reflect the new state
1049
1083
  for (const joinRow of Object.values(joinRowTodos)) {
@@ -1251,7 +1285,7 @@ class EntityManager {
1251
1285
  (0, fields_1.setField)(entity, field.fieldName, entity[field.fieldName]);
1252
1286
  }));
1253
1287
  // `.load()` recalculated the immediate relations, go ahead and recalc any downstream reactables.
1254
- // We'll still defer ReactiveQueryFields to the em.flush loop.
1288
+ // We'll still defer AsyncReactiveFields to the em.flush loop.
1255
1289
  await this.#rm.recalcPendingReactables("reactables");
1256
1290
  }
1257
1291
  beforeBegin(fn) {
@@ -1859,6 +1893,19 @@ function beforeUpdate(ctx, todos) {
1859
1893
  function afterValidation(ctx, todos) {
1860
1894
  return runHookOnTodos(ctx, "afterValidation", todos, ["inserts", "updates"]);
1861
1895
  }
1896
+ /** Collects changed entities from flush todos, i.e. m2m endpoint entities. */
1897
+ function entitiesFromTodos(entityTodos, joinRowTodos) {
1898
+ const entities = new Set();
1899
+ for (const todo of Object.values(entityTodos)) {
1900
+ [...todo.inserts, ...todo.updates, ...todo.deletes].forEach((entity) => entities.add(entity));
1901
+ }
1902
+ for (const todo of Object.values(joinRowTodos)) {
1903
+ [...todo.newRows, ...todo.deletedRows].forEach((row) => {
1904
+ Object.values(row.columns).forEach((entity) => entities.add(entity));
1905
+ });
1906
+ }
1907
+ return [...entities];
1908
+ }
1862
1909
  function beforeCommit(ctx, entities) {
1863
1910
  return runHook(ctx, "beforeCommit", [...entities]);
1864
1911
  }
@@ -1986,7 +2033,7 @@ function getCascadeDeleteRelations(entity) {
1986
2033
  function isCustomRelation(r) {
1987
2034
  return r instanceof index_1.CustomCollection || r instanceof index_1.CustomReference || r instanceof relations_1.ReactiveReferenceImpl;
1988
2035
  }
1989
- function maybeBumpUpdatedAt(todos, now) {
2036
+ function maybeBumpUpdatedAt(rm, todos, now) {
1990
2037
  for (const todo of Object.values(todos)) {
1991
2038
  const { updatedAt } = todo.metadata.timestampFields ?? {};
1992
2039
  if (updatedAt) {
@@ -1999,6 +2046,7 @@ function maybeBumpUpdatedAt(todos, now) {
1999
2046
  orm.originalData[updatedAt] = (0, fields_1.getField)(e, updatedAt);
2000
2047
  const serde = todo.metadata.fields[updatedAt].serde;
2001
2048
  orm.data[updatedAt] = serde.mapFromNow(now);
2049
+ rm.queueDownstreamReactables(e, updatedAt);
2002
2050
  }
2003
2051
  }
2004
2052
  }
@@ -2074,6 +2122,10 @@ function findPendingFlushEntities(entities, hooksInvoked, pendingFlush, pendingH
2074
2122
  }
2075
2123
  }
2076
2124
  }
2125
+ /** Returns true if the caller explicitly asked `find` to use SQL pagination. */
2126
+ function hasPaginationSettings(options) {
2127
+ return "limit" in options || "offset" in options;
2128
+ }
2077
2129
  /** An error we throw to get knex to `ROLLBACK`, but then catch. */
2078
2130
  class InMemoryRollbackError extends Error {
2079
2131
  }