joist-core 2.0.3 → 2.1.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 (147) hide show
  1. package/build/AliasAssigner.d.ts +3 -0
  2. package/build/AliasAssigner.d.ts.map +1 -1
  3. package/build/AliasAssigner.js +12 -3
  4. package/build/AliasAssigner.js.map +1 -1
  5. package/build/EntityGraphQLFilter.d.ts.map +1 -1
  6. package/build/EntityGraphQLFilter.js +1 -2
  7. package/build/EntityGraphQLFilter.js.map +1 -1
  8. package/build/EntityManager.d.ts +25 -10
  9. package/build/EntityManager.d.ts.map +1 -1
  10. package/build/EntityManager.js +242 -159
  11. package/build/EntityManager.js.map +1 -1
  12. package/build/IndexManager.d.ts +1 -0
  13. package/build/IndexManager.d.ts.map +1 -1
  14. package/build/IndexManager.js +6 -3
  15. package/build/IndexManager.js.map +1 -1
  16. package/build/JoinRows.d.ts.map +1 -1
  17. package/build/JoinRows.js +1 -2
  18. package/build/JoinRows.js.map +1 -1
  19. package/build/PendingChanges.d.ts +26 -0
  20. package/build/PendingChanges.d.ts.map +1 -0
  21. package/build/PendingChanges.js +3 -0
  22. package/build/PendingChanges.js.map +1 -0
  23. package/build/ReactionsManager.d.ts +1 -1
  24. package/build/ReactionsManager.d.ts.map +1 -1
  25. package/build/ReactionsManager.js +8 -5
  26. package/build/ReactionsManager.js.map +1 -1
  27. package/build/batchloaders/BatchLoader.d.ts +14 -0
  28. package/build/batchloaders/BatchLoader.d.ts.map +1 -0
  29. package/build/batchloaders/BatchLoader.js +49 -0
  30. package/build/batchloaders/BatchLoader.js.map +1 -0
  31. package/build/batchloaders/loadBatchLoader.d.ts +17 -0
  32. package/build/batchloaders/loadBatchLoader.d.ts.map +1 -0
  33. package/build/batchloaders/loadBatchLoader.js +55 -0
  34. package/build/batchloaders/loadBatchLoader.js.map +1 -0
  35. package/build/batchloaders/manyToManyBatchLoader.d.ts +7 -0
  36. package/build/batchloaders/manyToManyBatchLoader.d.ts.map +1 -0
  37. package/build/batchloaders/manyToManyBatchLoader.js +57 -0
  38. package/build/batchloaders/manyToManyBatchLoader.js.map +1 -0
  39. package/build/batchloaders/oneToManyBatchLoader.d.ts +7 -0
  40. package/build/batchloaders/oneToManyBatchLoader.d.ts.map +1 -0
  41. package/build/{dataloaders/oneToManyDataLoader.js → batchloaders/oneToManyBatchLoader.js} +9 -18
  42. package/build/batchloaders/oneToManyBatchLoader.js.map +1 -0
  43. package/build/batchloaders/oneToOneBatchLoader.d.ts +7 -0
  44. package/build/batchloaders/oneToOneBatchLoader.d.ts.map +1 -0
  45. package/build/{dataloaders/oneToOneDataLoader.js → batchloaders/oneToOneBatchLoader.js} +10 -16
  46. package/build/batchloaders/oneToOneBatchLoader.js.map +1 -0
  47. package/build/{dataloaders/populateDataLoader.d.ts → batchloaders/populateBatchLoader.d.ts} +5 -5
  48. package/build/batchloaders/populateBatchLoader.d.ts.map +1 -0
  49. package/build/{dataloaders/populateDataLoader.js → batchloaders/populateBatchLoader.js} +97 -54
  50. package/build/batchloaders/populateBatchLoader.js.map +1 -0
  51. package/build/batchloaders/recursiveChildrenBatchLoader.d.ts +7 -0
  52. package/build/batchloaders/recursiveChildrenBatchLoader.d.ts.map +1 -0
  53. package/build/{dataloaders/recursiveChildrenDataLoader.js → batchloaders/recursiveChildrenBatchLoader.js} +4 -6
  54. package/build/batchloaders/recursiveChildrenBatchLoader.js.map +1 -0
  55. package/build/batchloaders/recursiveM2mBatchLoader.d.ts +7 -0
  56. package/build/batchloaders/recursiveM2mBatchLoader.d.ts.map +1 -0
  57. package/build/batchloaders/recursiveM2mBatchLoader.js +84 -0
  58. package/build/batchloaders/recursiveM2mBatchLoader.js.map +1 -0
  59. package/build/batchloaders/recursiveParentsBatchLoader.d.ts +7 -0
  60. package/build/batchloaders/recursiveParentsBatchLoader.d.ts.map +1 -0
  61. package/build/{dataloaders/recursiveParentsDataLoader.js → batchloaders/recursiveParentsBatchLoader.js} +4 -5
  62. package/build/batchloaders/recursiveParentsBatchLoader.js.map +1 -0
  63. package/build/changes.js +2 -2
  64. package/build/changes.js.map +1 -1
  65. package/build/config.d.ts +13 -0
  66. package/build/config.d.ts.map +1 -1
  67. package/build/config.js +26 -1
  68. package/build/config.js.map +1 -1
  69. package/build/dataloaders/findIdsDataLoader.d.ts.map +1 -1
  70. package/build/dataloaders/findIdsDataLoader.js +7 -5
  71. package/build/dataloaders/findIdsDataLoader.js.map +1 -1
  72. package/build/dataloaders/findOrCreateDataLoader.d.ts.map +1 -1
  73. package/build/dataloaders/findOrCreateDataLoader.js +3 -2
  74. package/build/dataloaders/findOrCreateDataLoader.js.map +1 -1
  75. package/build/index.d.ts +2 -1
  76. package/build/index.d.ts.map +1 -1
  77. package/build/index.js +4 -2
  78. package/build/index.js.map +1 -1
  79. package/build/logging/ReactionLogger.d.ts +8 -4
  80. package/build/logging/ReactionLogger.d.ts.map +1 -1
  81. package/build/logging/ReactionLogger.js +11 -4
  82. package/build/logging/ReactionLogger.js.map +1 -1
  83. package/build/newTestInstance.d.ts +15 -0
  84. package/build/newTestInstance.d.ts.map +1 -1
  85. package/build/newTestInstance.js +49 -6
  86. package/build/newTestInstance.js.map +1 -1
  87. package/build/preloading/JsonAggregatePreloader.js +29 -24
  88. package/build/preloading/JsonAggregatePreloader.js.map +1 -1
  89. package/build/reactiveHints.d.ts.map +1 -1
  90. package/build/reactiveHints.js +25 -0
  91. package/build/reactiveHints.js.map +1 -1
  92. package/build/relations/Collection.d.ts +6 -1
  93. package/build/relations/Collection.d.ts.map +1 -1
  94. package/build/relations/Collection.js.map +1 -1
  95. package/build/relations/ManyToManyCollection.d.ts +8 -1
  96. package/build/relations/ManyToManyCollection.d.ts.map +1 -1
  97. package/build/relations/ManyToManyCollection.js +338 -130
  98. package/build/relations/ManyToManyCollection.js.map +1 -1
  99. package/build/relations/ManyToOneReference.d.ts +2 -6
  100. package/build/relations/ManyToOneReference.d.ts.map +1 -1
  101. package/build/relations/ManyToOneReference.js +115 -78
  102. package/build/relations/ManyToOneReference.js.map +1 -1
  103. package/build/relations/OneToManyCollection.d.ts +8 -4
  104. package/build/relations/OneToManyCollection.d.ts.map +1 -1
  105. package/build/relations/OneToManyCollection.js +415 -173
  106. package/build/relations/OneToManyCollection.js.map +1 -1
  107. package/build/relations/OneToOneReference.d.ts.map +1 -1
  108. package/build/relations/OneToOneReference.js +12 -7
  109. package/build/relations/OneToOneReference.js.map +1 -1
  110. package/build/relations/ReactiveManyToMany.js +4 -4
  111. package/build/relations/ReactiveManyToMany.js.map +1 -1
  112. package/build/relations/ReactiveManyToManyOtherSide.js +3 -3
  113. package/build/relations/ReactiveManyToManyOtherSide.js.map +1 -1
  114. package/build/relations/ReadOnlyCollection.d.ts.map +1 -1
  115. package/build/relations/ReadOnlyCollection.js +2 -1
  116. package/build/relations/ReadOnlyCollection.js.map +1 -1
  117. package/build/relations/RecursiveCollection.d.ts +29 -1
  118. package/build/relations/RecursiveCollection.d.ts.map +1 -1
  119. package/build/relations/RecursiveCollection.js +160 -9
  120. package/build/relations/RecursiveCollection.js.map +1 -1
  121. package/build/relations/index.d.ts +1 -1
  122. package/build/relations/index.d.ts.map +1 -1
  123. package/build/relations/index.js +2 -1
  124. package/build/relations/index.js.map +1 -1
  125. package/package.json +2 -2
  126. package/build/dataloaders/loadDataLoader.d.ts +0 -11
  127. package/build/dataloaders/loadDataLoader.d.ts.map +0 -1
  128. package/build/dataloaders/loadDataLoader.js +0 -54
  129. package/build/dataloaders/loadDataLoader.js.map +0 -1
  130. package/build/dataloaders/manyToManyDataLoader.d.ts +0 -8
  131. package/build/dataloaders/manyToManyDataLoader.d.ts.map +0 -1
  132. package/build/dataloaders/manyToManyDataLoader.js +0 -74
  133. package/build/dataloaders/manyToManyDataLoader.js.map +0 -1
  134. package/build/dataloaders/oneToManyDataLoader.d.ts +0 -7
  135. package/build/dataloaders/oneToManyDataLoader.d.ts.map +0 -1
  136. package/build/dataloaders/oneToManyDataLoader.js.map +0 -1
  137. package/build/dataloaders/oneToOneDataLoader.d.ts +0 -7
  138. package/build/dataloaders/oneToOneDataLoader.d.ts.map +0 -1
  139. package/build/dataloaders/oneToOneDataLoader.js.map +0 -1
  140. package/build/dataloaders/populateDataLoader.d.ts.map +0 -1
  141. package/build/dataloaders/populateDataLoader.js.map +0 -1
  142. package/build/dataloaders/recursiveChildrenDataLoader.d.ts +0 -7
  143. package/build/dataloaders/recursiveChildrenDataLoader.d.ts.map +0 -1
  144. package/build/dataloaders/recursiveChildrenDataLoader.js.map +0 -1
  145. package/build/dataloaders/recursiveParentsDataLoader.d.ts +0 -7
  146. package/build/dataloaders/recursiveParentsDataLoader.d.ts.map +0 -1
  147. package/build/dataloaders/recursiveParentsDataLoader.js.map +0 -1
@@ -22,10 +22,13 @@ exports.appendStack = appendStack;
22
22
  exports.createRowFromEntityData = createRowFromEntityData;
23
23
  const dataloader_1 = __importDefault(require("dataloader"));
24
24
  const BaseEntity_1 = require("./BaseEntity");
25
+ const BatchLoader_1 = require("./batchloaders/BatchLoader");
25
26
  const defaults_1 = require("./defaults");
26
27
  const fields_1 = require("./fields");
27
28
  const IndexManager_1 = require("./IndexManager");
28
29
  // We alias `Entity => EntityW` to denote "Entity wide" i.e. the non-narrowed Entity
30
+ const loadBatchLoader_1 = require("./batchloaders/loadBatchLoader");
31
+ const populateBatchLoader_1 = require("./batchloaders/populateBatchLoader");
29
32
  const caches_1 = require("./caches");
30
33
  const config_1 = require("./config");
31
34
  const configure_1 = require("./configure");
@@ -34,8 +37,6 @@ const findCountDataLoader_1 = require("./dataloaders/findCountDataLoader");
34
37
  const findDataLoader_1 = require("./dataloaders/findDataLoader");
35
38
  const findIdsDataLoader_1 = require("./dataloaders/findIdsDataLoader");
36
39
  const findOrCreateDataLoader_1 = require("./dataloaders/findOrCreateDataLoader");
37
- const loadDataLoader_1 = require("./dataloaders/loadDataLoader");
38
- const populateDataLoader_1 = require("./dataloaders/populateDataLoader");
39
40
  const Entity_1 = require("./Entity");
40
41
  const FlushLock_1 = require("./FlushLock");
41
42
  const index_1 = require("./index");
@@ -48,6 +49,7 @@ const ReactionsManager_1 = require("./ReactionsManager");
48
49
  const reactiveHints_1 = require("./reactiveHints");
49
50
  const relations_1 = require("./relations");
50
51
  const hasAsyncMethod_1 = require("./relations/hasAsyncMethod");
52
+ const RecursiveCollection_1 = require("./relations/RecursiveCollection");
51
53
  const Todo_1 = require("./Todo");
52
54
  const trusted_1 = require("./trusted");
53
55
  const upsert_1 = require("./upsert");
@@ -91,7 +93,7 @@ class EntityManager {
91
93
  // Provides field-based indexing for entity types with >1000 entities to optimize findWithNewOrChanged
92
94
  #indexManager = new IndexManager_1.IndexManager();
93
95
  #isValidating = false;
94
- #pendingChildren = new Map();
96
+ #pendingPercolate = new Map();
95
97
  #preloadedRelations = new Map();
96
98
  /**
97
99
  * Tracks cascade deletes.
@@ -103,6 +105,7 @@ class EntityManager {
103
105
  */
104
106
  #pendingDeletes = [];
105
107
  #dataloaders = {};
108
+ #batchLoaders = {};
106
109
  #joinRows = {};
107
110
  /** Stores any `source -> downstream` reactions to recalc during `em.flush`. */
108
111
  #rm = new ReactionsManager_1.ReactionsManager(this);
@@ -142,8 +145,9 @@ class EntityManager {
142
145
  const em = this;
143
146
  this.__api = {
144
147
  preloader: this.#preloader,
145
- pendingChildren: this.#pendingChildren,
148
+ pendingPercolate: this.#pendingPercolate,
146
149
  mutatedCollections: new Set(),
150
+ pendingLoads: new Set(),
147
151
  hooks: this.#hooks,
148
152
  rm: this.#rm,
149
153
  indexManager: this.#indexManager,
@@ -181,6 +185,7 @@ class EntityManager {
181
185
  },
182
186
  clearDataloaders() {
183
187
  em.#dataloaders = {};
188
+ em.#batchLoaders = {};
184
189
  },
185
190
  clearPreloadedRelations() {
186
191
  em.#preloadedRelations = new Map();
@@ -194,6 +199,38 @@ class EntityManager {
194
199
  get entities() {
195
200
  return [...this.#entitiesArray];
196
201
  }
202
+ /** Returns a list of all pending creates, updates, deletes, and m2m changes that would be flushed. */
203
+ get pendingChanges() {
204
+ const changes = [];
205
+ for (const entity of this.#entitiesArray) {
206
+ const op = (0, BaseEntity_1.getInstanceData)(entity).pendingOperation;
207
+ switch (op) {
208
+ case "insert":
209
+ changes.push({ kind: "create", entity });
210
+ break;
211
+ case "update":
212
+ changes.push({ kind: "update", entity });
213
+ break;
214
+ case "delete":
215
+ changes.push({ kind: "delete", entity });
216
+ break;
217
+ }
218
+ }
219
+ for (const joinRows of Object.values(this.#joinRows)) {
220
+ const todo = joinRows.toTodo();
221
+ if (todo) {
222
+ for (const row of todo.newRows) {
223
+ const entities = Object.values(row.columns);
224
+ changes.push({ kind: "m2m", op: "add", joinTableName: todo.m2m.joinTableName, entities });
225
+ }
226
+ for (const row of todo.deletedRows) {
227
+ const entities = Object.values(row.columns);
228
+ changes.push({ kind: "m2m", op: "remove", joinTableName: todo.m2m.joinTableName, entities });
229
+ }
230
+ }
231
+ }
232
+ return changes;
233
+ }
197
234
  /** Returns a read-only list of the currently-loaded entities of `type`. */
198
235
  getEntities(type) {
199
236
  const meta = (0, index_1.getMetadata)(type);
@@ -602,15 +639,18 @@ class EntityManager {
602
639
  id = id || (0, utils_1.fail)(`Invalid ${typeOrId.name} id: ${id}`);
603
640
  }
604
641
  const meta = (0, index_1.getMetadata)(type);
605
- const tagged = (0, index_1.toTaggedId)(meta, id);
606
- const entity = this.findExistingInstance(tagged) ||
607
- (await (0, loadDataLoader_1.loadDataLoader)(this, meta)
608
- .load({ entity: tagged, hint })
642
+ const taggedId = (0, index_1.toTaggedId)(meta, id);
643
+ let entity = this.findExistingInstance(taggedId);
644
+ if (!entity) {
645
+ await (0, loadBatchLoader_1.loadBatchLoader)(this, meta)
646
+ .load({ taggedId, hint })
609
647
  .catch(function load(err) {
610
648
  throw appendStack(err, new Error());
611
- }));
649
+ });
650
+ entity = this.findExistingInstance(taggedId);
651
+ }
612
652
  if (!entity) {
613
- throw new NotFoundError(`${tagged} was not found`);
653
+ throw new NotFoundError(`${taggedId} was not found`);
614
654
  }
615
655
  if (hint) {
616
656
  await this.populate(entity, hint);
@@ -623,16 +663,22 @@ class EntityManager {
623
663
  async loadAll(type, _ids, hint) {
624
664
  const meta = (0, index_1.getMetadata)(type);
625
665
  const ids = _ids.map((id) => (0, index_1.tagId)(meta, id));
626
- const entities = await Promise.all(ids.map((id) => {
627
- return (this.findExistingInstance(id) ||
628
- (0, loadDataLoader_1.loadDataLoader)(this, meta)
629
- .load({ entity: id, hint })
630
- .catch(function loadAll(err) {
631
- throw appendStack(err, new Error());
632
- }));
633
- }));
634
- const idsNotFound = ids.filter((_, i) => entities[i] === undefined);
635
- if (idsNotFound.length > 0) {
666
+ const idsToLoad = ids.filter((id) => !this.findExistingInstance(id));
667
+ if (idsToLoad.length > 0) {
668
+ await (0, loadBatchLoader_1.loadBatchLoader)(this, meta)
669
+ .loadAll(idsToLoad.map((id) => ({ taggedId: id, hint })))
670
+ .catch(function loadAll(err) {
671
+ throw appendStack(err, new Error());
672
+ });
673
+ }
674
+ const entities = [];
675
+ for (const id of ids) {
676
+ const entity = this.findExistingInstance(id);
677
+ if (entity)
678
+ entities.push(entity);
679
+ }
680
+ if (entities.length !== ids.length) {
681
+ const idsNotFound = ids.filter((_, i) => entities[i] === undefined);
636
682
  throw new NotFoundError(`${idsNotFound.join(",")} were not found`);
637
683
  }
638
684
  if (hint) {
@@ -649,14 +695,15 @@ class EntityManager {
649
695
  async loadAllIfExists(type, _ids, hint) {
650
696
  const meta = (0, index_1.getMetadata)(type);
651
697
  const ids = _ids.map((id) => (0, index_1.tagId)(meta, id));
652
- const entities = (await Promise.all(ids.map((id) => {
653
- return (this.findExistingInstance(id) ||
654
- (0, loadDataLoader_1.loadDataLoader)(this, meta)
655
- .load({ entity: id, hint })
656
- .catch(function loadAllIfExists(err) {
657
- throw appendStack(err, new Error());
658
- }));
659
- }))).filter(Boolean);
698
+ const idsToLoad = ids.filter((id) => !this.findExistingInstance(id));
699
+ if (idsToLoad.length > 0) {
700
+ await (0, loadBatchLoader_1.loadBatchLoader)(this, meta)
701
+ .loadAll(idsToLoad.map((id) => ({ taggedId: id, hint })))
702
+ .catch(function loadAllIfExists(err) {
703
+ throw appendStack(err, new Error());
704
+ });
705
+ }
706
+ const entities = ids.map((id) => this.findExistingInstance(id)).filter(Boolean);
660
707
  if (hint) {
661
708
  await this.populate(entities, hint);
662
709
  }
@@ -713,23 +760,23 @@ class EntityManager {
713
760
  // intra dependencies), then a 2nd small-batch non-preload populate.
714
761
  const [preload, non] = this.#preloader.partitionHint(meta, hintOpt);
715
762
  if (preload) {
716
- const loader = (0, populateDataLoader_1.populateDataLoader)(this, meta, preload, "preload", opts);
717
- await Promise.all(list.map((entity) => loader.load({ entity, hint: preload }).catch(function populate(err) {
763
+ const loader = (0, populateBatchLoader_1.populateBatchLoader)(this, meta, preload, "preload", opts);
764
+ await loader.loadAll(list.map((entity) => ({ entity, hint: preload }))).catch(function populate(err) {
718
765
  throw appendStack(err, new Error());
719
- })));
766
+ });
720
767
  }
721
768
  if (non) {
722
- const loader = (0, populateDataLoader_1.populateDataLoader)(this, meta, non, "intermixed", opts);
723
- await Promise.all(list.map((entity) => loader.load({ entity, hint: non }).catch(function populate(err) {
769
+ const loader = (0, populateBatchLoader_1.populateBatchLoader)(this, meta, non, "intermixed", opts);
770
+ await loader.loadAll(list.map((entity) => ({ entity, hint: non }))).catch(function populate(err) {
724
771
  throw appendStack(err, new Error());
725
- })));
772
+ });
726
773
  }
727
774
  }
728
775
  else {
729
- const loader = (0, populateDataLoader_1.populateDataLoader)(this, meta, hintOpt, "intermixed", opts);
730
- await Promise.all(list.map((entity) => loader.load({ entity, hint: hintOpt }).catch(function populate(err) {
776
+ const loader = (0, populateBatchLoader_1.populateBatchLoader)(this, meta, hintOpt, "intermixed", opts);
777
+ await loader.loadAll(list.map((entity) => ({ entity, hint: hintOpt }))).catch(function populate(err) {
731
778
  throw appendStack(err, new Error());
732
- })));
779
+ });
733
780
  }
734
781
  return fn ? fn(entityOrList) : entityOrList;
735
782
  }
@@ -817,110 +864,116 @@ class EntityManager {
817
864
  throw new ReadOnlyError();
818
865
  const { skipValidation = false } = flushOptions;
819
866
  this.#fl.startLock();
820
- await this.#fl.allowWrites(async () => {
821
- // Cascade deletes now that we're async (i.e. to keep `em.delete` synchronous).
822
- // Also do this before calling `recalcPendingReactables` to avoid recalculating
823
- // fields on entities that will be deleted (and probably have unset/invalid FKs
824
- // that would NPE their logic anyway).
825
- await this.flushDeletes();
826
- // Recalc before we run hooks, so the hooks will see the latest calculated values.
827
- await this.#rm.recalcPendingReactables("reactables");
828
- });
829
- const createdThenDeleted = new Set();
830
- // We'll only invoke hooks once/entity (the 1st time that entity goes through runHooksOnPendingEntities)
831
- const hooksInvoked = new Set();
832
- // Make sure two ReactiveQueryFields don't ping-pong each other forever
833
- let hookLoops = 0;
834
- let now = getNow();
835
- const suppressedDefaultTypeErrors = [];
836
- // Make a lambda that we can invoke multiple times, if we loop for ReactiveQueryFields
837
- const runHooksOnPendingEntities = async () => {
838
- if (hookLoops++ >= 10)
839
- throw new Error("runHooksOnPendingEntities has ran 10 iterations, aborting");
840
- // Any dirty entities we find, even if we skipped firing their hooks on this loop
841
- const pendingFlush = new Set();
842
- // Subset of pendingFlush entities that we will run hooks on
843
- const pendingHooks = new Set();
844
- // Subset of pendingFlush entities that had hooks invoked in a prior `runHooksOnPendingEntities`
845
- const alreadyRanHooks = new Set();
846
- findPendingFlushEntities(this.entities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks);
847
- // If we're re-looping for ReactiveQueryField, make sure to bump updatedAt
848
- // each time, so that for an INSERT-then-UPDATE the triggers don't think the
849
- // UPDATE forgot to self-bump updatedAt, and then "helpfully" bump it for us.
850
- if (alreadyRanHooks.size > 0) {
851
- maybeBumpUpdatedAt((0, Todo_1.createTodos)([...alreadyRanHooks]), now);
852
- }
853
- // Run hooks in a series of loops until things "settle down"
854
- while (pendingHooks.size > 0) {
855
- await this.#fl.allowWrites(async () => {
856
- // Run our hooks
857
- let todos = (0, Todo_1.createTodos)([...pendingHooks]);
858
- await (0, defaults_1.setAsyncDefaults)(suppressedDefaultTypeErrors, this.ctx, Todo_1.Todo.groupInsertsByTypeAndSubType(todos));
859
- maybeBumpUpdatedAt(todos, now);
860
- for (const group of maybeSetupHookOrdering(todos)) {
861
- await beforeCreate(this.ctx, group);
862
- await beforeUpdate(this.ctx, group);
863
- await beforeFlush(this.ctx, group);
864
- }
865
- // Call `setField` just to get the column marked as dirty if needed.
866
- // This can come after the hooks, b/c if the hooks read any of these
867
- // fields, they'd be via the synchronous getter and would not be stale.
868
- recalcSynchronousDerivedFields(todos);
869
- // The hooks could have deleted this-loop or prior-loop entities, so re-cascade again.
870
- await this.flushDeletes();
871
- // The hooks could have changed fields, so recalc again.
872
- await this.#rm.recalcPendingReactables("reactables");
873
- // We may have reactables that failed earlier, but will succeed now that hooks have been run and cascade
874
- // deletes have been processed
875
- if (this.#rm.hasPendingTypeErrors) {
876
- await this.#rm.recalcPendingTypeErrors();
877
- // We need to re-run reactables again if we dirtied something while retrying type errors
878
- if (this.#rm.needsRecalc("reactables"))
879
- await this.#rm.recalcPendingReactables("reactables");
880
- }
881
- for (const e of pendingHooks)
882
- hooksInvoked.add(e);
883
- pendingHooks.clear();
884
- // See if the hooks mutated any new, not-yet-hooksInvoked entities
885
- findPendingFlushEntities(this.entities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks);
886
- // The final run of recalcPendingReactables could have left us with pending type errors and no entities in
887
- // pendingHooks. If so, we need to re-run recalcPendingTypeErrors to get those errors to transition into
888
- // suppressed errors so that we will fail after simpleValidation.
889
- if (pendingHooks.size === 0 && this.#rm.hasPendingTypeErrors)
890
- await this.#rm.recalcPendingTypeErrors();
891
- });
892
- }
893
- // We might have invoked hooks that immediately deleted a new entity (weird but allowed);
894
- // if so, filter it out so that we don't flush it, but keep track for later fixing up
895
- // it's `#orm.deleted` field.
896
- return [...pendingFlush].filter((e) => {
897
- const createThenDelete = e.isDeletedEntity && e.isNewEntity;
898
- if (createThenDelete)
899
- createdThenDeleted.add(e);
900
- return !createThenDelete;
901
- });
902
- };
903
- const runValidation = async (entityTodos, joinRowTodos) => {
904
- try {
905
- this.#isValidating = true;
906
- // Run simple rules first b/c it includes not-null/required rules, so that then when we run
907
- // `validateReactiveRules` next, the app's lambdas won't see fundamentally invalid entities & NPE.
908
- await validateSimpleRules(entityTodos);
909
- // After we've let any "author is not set" simple rules fail before prematurely throwing
910
- // the "of course that caused an NPE" `TypeError`s, if all the authors *were* valid/set,
911
- // and we still have TypeErrors (from derived valeus), they were real, unrelated errors
912
- // that the user should see.
913
- if (suppressedDefaultTypeErrors.length > 0)
914
- throw suppressedDefaultTypeErrors[0];
915
- await validateReactiveRules(entityTodos, joinRowTodos);
916
- }
917
- finally {
918
- this.#isValidating = false;
919
- }
920
- await afterValidation(this.ctx, entityTodos);
921
- };
922
867
  const allFlushedEntities = new Set();
923
868
  try {
869
+ await this.#fl.allowWrites(async () => {
870
+ // Cascade deletes now that we're async (i.e. to keep `em.delete` synchronous).
871
+ // Also do this before calling `recalcPendingReactables` to avoid recalculating
872
+ // fields on entities that will be deleted (and probably have unset/invalid FKs
873
+ // that would NPE their logic anyway).
874
+ await this.flushDeletes();
875
+ // Recalc before we run hooks, so the hooks will see the latest calculated values.
876
+ await this.#rm.recalcPendingReactables("reactables");
877
+ });
878
+ const createdThenDeleted = new Set();
879
+ // We'll only invoke hooks once/entity (the 1st time that entity goes through runHooksOnPendingEntities)
880
+ const hooksInvoked = new Set();
881
+ // Make sure two ReactiveQueryFields don't ping-pong each other forever
882
+ let hookLoops = 0;
883
+ let now = getNow();
884
+ const suppressedDefaultTypeErrors = [];
885
+ // Make a lambda that we can invoke multiple times, if we loop for ReactiveQueryFields
886
+ const runHooksOnPendingEntities = async () => {
887
+ if (hookLoops++ >= 10)
888
+ throw new Error("runHooksOnPendingEntities has ran 10 iterations, aborting");
889
+ // Resolve any pending o2m/m2m sets (set() called before load — need to load from DB to diff)
890
+ const pendingLoads = [...getEmInternalApi(this).pendingLoads];
891
+ if (pendingLoads.length > 0) {
892
+ getEmInternalApi(this).pendingLoads.clear();
893
+ await Promise.all(pendingLoads.map((collection) => collection.load()));
894
+ }
895
+ // Any dirty entities we find, even if we skipped firing their hooks on this loop
896
+ const pendingFlush = new Set();
897
+ // Subset of pendingFlush entities that we will run hooks on
898
+ const pendingHooks = new Set();
899
+ // Subset of pendingFlush entities that had hooks invoked in a prior `runHooksOnPendingEntities`
900
+ const alreadyRanHooks = new Set();
901
+ findPendingFlushEntities(this.entities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks);
902
+ // If we're re-looping for ReactiveQueryField, make sure to bump updatedAt
903
+ // each time, so that for an INSERT-then-UPDATE the triggers don't think the
904
+ // UPDATE forgot to self-bump updatedAt, and then "helpfully" bump it for us.
905
+ if (alreadyRanHooks.size > 0) {
906
+ maybeBumpUpdatedAt((0, Todo_1.createTodos)([...alreadyRanHooks]), now);
907
+ }
908
+ // Run hooks in a series of loops until things "settle down"
909
+ while (pendingHooks.size > 0) {
910
+ await this.#fl.allowWrites(async () => {
911
+ let todos = (0, Todo_1.createTodos)([...pendingHooks]);
912
+ await (0, defaults_1.setAsyncDefaults)(suppressedDefaultTypeErrors, this.ctx, Todo_1.Todo.groupInsertsByTypeAndSubType(todos));
913
+ maybeBumpUpdatedAt(todos, now);
914
+ // Run our hooks
915
+ for (const group of maybeSetupHookOrdering(todos)) {
916
+ await beforeCreate(this.ctx, group);
917
+ await beforeUpdate(this.ctx, group);
918
+ await beforeFlush(this.ctx, group);
919
+ }
920
+ // Call `setField` just to get the column marked as dirty if needed.
921
+ // This can come after the hooks, b/c if the hooks read any of these
922
+ // fields, they'd be via the synchronous getter and would not be stale.
923
+ recalcSynchronousDerivedFields(todos);
924
+ // The hooks could have deleted this-loop or prior-loop entities, so re-cascade again.
925
+ await this.flushDeletes();
926
+ // The hooks could have changed fields, so recalc again.
927
+ await this.#rm.recalcPendingReactables("reactables");
928
+ // We may have reactables that failed earlier, but will succeed now that hooks have been run and cascade
929
+ // deletes have been processed
930
+ if (this.#rm.hasPendingTypeErrors) {
931
+ await this.#rm.recalcPendingTypeErrors();
932
+ // We need to re-run reactables again if we dirtied something while retrying type errors
933
+ if (this.#rm.needsRecalc("reactables"))
934
+ await this.#rm.recalcPendingReactables("reactables");
935
+ }
936
+ for (const e of pendingHooks)
937
+ hooksInvoked.add(e);
938
+ pendingHooks.clear();
939
+ // See if the hooks mutated any new, not-yet-hooksInvoked entities
940
+ findPendingFlushEntities(this.entities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks);
941
+ // The final run of recalcPendingReactables could have left us with pending type errors and no entities in
942
+ // pendingHooks. If so, we need to re-run recalcPendingTypeErrors to get those errors to transition into
943
+ // suppressed errors so that we will fail after simpleValidation.
944
+ if (pendingHooks.size === 0 && this.#rm.hasPendingTypeErrors)
945
+ await this.#rm.recalcPendingTypeErrors();
946
+ });
947
+ }
948
+ // We might have invoked hooks that immediately deleted a new entity (weird but allowed);
949
+ // if so, filter it out so that we don't flush it, but keep track for later fixing up
950
+ // it's `#orm.deleted` field.
951
+ return [...pendingFlush].filter((e) => {
952
+ const createThenDelete = e.isDeletedEntity && e.isNewEntity;
953
+ if (createThenDelete)
954
+ createdThenDeleted.add(e);
955
+ return !createThenDelete;
956
+ });
957
+ };
958
+ const runValidation = async (entityTodos, joinRowTodos) => {
959
+ try {
960
+ 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);
971
+ }
972
+ finally {
973
+ this.#isValidating = false;
974
+ }
975
+ await afterValidation(this.ctx, entityTodos);
976
+ };
924
977
  // Run hooks (in iterative loops if hooks mutate new entities) on pending entities
925
978
  let entitiesToFlush = await runHooksOnPendingEntities();
926
979
  for (const e of entitiesToFlush)
@@ -1001,8 +1054,10 @@ class EntityManager {
1001
1054
  o2m.resetAddedRemoved();
1002
1055
  }
1003
1056
  mutatedCollections.clear();
1004
- // Reset the find caches b/c data will have changed in the db
1057
+ // Reset the find/preload caches b/c data will have changed in the db
1005
1058
  this.#dataloaders = {};
1059
+ this.#batchLoaders = {};
1060
+ this.#preloadedRelations = new Map();
1006
1061
  this.#rm.clear();
1007
1062
  }
1008
1063
  // Fixup the `deleted` field on entities that were created then immediately deleted
@@ -1012,6 +1067,19 @@ class EntityManager {
1012
1067
  return [...allFlushedEntities];
1013
1068
  }
1014
1069
  catch (e) {
1070
+ if (e instanceof RecursiveCollection_1.RecursiveCycleError) {
1071
+ const entity = e.entities[0];
1072
+ // Look up a custom cycle message — check both the exact field name and its opposite
1073
+ // direction, since the cycle may be detected from either side (e.g. walking
1074
+ // childrenRecursive during reactive hint reversal when parentsRecursive was configured).
1075
+ for (const meta of (0, index_1.getBaseAndSelfMetas)((0, index_1.getMetadata)(entity))) {
1076
+ const messageFn = meta.config.__data.cycleMessages[e.fieldName] ??
1077
+ meta.config.__data.cycleMessages[entity[e.fieldName]?.otherFieldName];
1078
+ if (messageFn) {
1079
+ throw new index_1.ValidationErrors([{ entity, message: messageFn(entity, e.entities) }]);
1080
+ }
1081
+ }
1082
+ }
1015
1083
  if (e && typeof e === "object" && "constraint" in e && typeof e.constraint === "string") {
1016
1084
  // node-pg errors use `constraint` to indicate the constraint name
1017
1085
  const message = config_1.constraintNameToValidationError[e.constraint];
@@ -1042,6 +1110,7 @@ class EntityManager {
1042
1110
  async refresh(param) {
1043
1111
  this.#isRefreshing = true;
1044
1112
  this.#dataloaders = {};
1113
+ this.#batchLoaders = {};
1045
1114
  this.#preloadedRelations = new Map();
1046
1115
  const deepLoad = param && "deepLoad" in param && param.deepLoad;
1047
1116
  let todo = param === undefined
@@ -1057,23 +1126,22 @@ class EntityManager {
1057
1126
  copy.forEach((e) => done.add(e));
1058
1127
  todo = [];
1059
1128
  // For any non-deleted entity with an id, get its latest data + relations from the database
1060
- const entities = await Promise.all(copy
1061
- .filter((e) => e.idTaggedMaybe && !e.isDeletedEntity)
1062
- .map((entity) => {
1063
- // Pass these as a hint to potentially preload them
1129
+ const entitiesToRefresh = copy.filter((e) => e.idTaggedMaybe && !e.isDeletedEntity);
1130
+ await Promise.all(entitiesToRefresh.map((entity) => {
1131
+ // Pass loaded/all relation names as a hint so the preloader can inject JOINs
1064
1132
  const hint = (0, index_1.getRelationEntries)(entity)
1065
1133
  .filter(([_, r]) => deepLoad || r.isLoaded)
1066
1134
  .map(([k]) => k);
1067
- return (0, loadDataLoader_1.loadDataLoader)(this, (0, index_1.getMetadata)(entity), true)
1068
- .load({ entity: entity.idTagged, hint })
1135
+ return (0, loadBatchLoader_1.loadBatchLoader)(this, (0, index_1.getMetadata)(entity), true)
1136
+ .load({ taggedId: entity.idTagged, hint })
1069
1137
  .catch(function refresh(err) {
1070
1138
  throw appendStack(err, new Error());
1071
1139
  });
1072
1140
  }));
1073
- // Then refresh any loaded relations (the `loadDataLoader.load` only populates the
1074
- // preloader cache, if in use, it doesn't actually get each relation into a loaded state.)
1075
- const [custom, builtin] = (0, utils_1.partition)(entities
1076
- .filter((e) => e && !(0, BaseEntity_1.getInstanceData)(e).isDeletedEntity)
1141
+ // Then refresh any loaded relations (the loadBatchLoader only hydrates the entity
1142
+ // data, it doesn't get each relation into a loaded state.)
1143
+ const [custom, builtin] = (0, utils_1.partition)(entitiesToRefresh
1144
+ .filter((e) => !(0, BaseEntity_1.getInstanceData)(e).isDeletedEntity)
1077
1145
  .flatMap((entity) => (0, index_1.getRelations)(entity).filter((r) => deepLoad || r.isLoaded)), isCustomRelation);
1078
1146
  // Call `.load` on builtin relations first, because if we hit an already-loaded custom relation
1079
1147
  // first, it will call `.get` internally, and might access built-in relations that we've not had a
@@ -1215,6 +1283,16 @@ class EntityManager {
1215
1283
  const loadersForKind = (this.#dataloaders[operation] ??= {});
1216
1284
  return (0, utils_1.getOrSet)(loadersForKind, batchKey, () => new dataloader_1.default(fn, opts));
1217
1285
  }
1286
+ /**
1287
+ * Like `getLoader`, but returns a `BatchLoader` where all callers in the same tick
1288
+ * share a single `Promise<void>` instead of getting per-key promises.
1289
+ *
1290
+ * The batch function should write results via side channels (e.g. `setPreloadedRelation`).
1291
+ */
1292
+ getBatchLoader(operation, batchKey, fn) {
1293
+ const loadersForKind = (this.#batchLoaders[operation] ??= {});
1294
+ return (0, utils_1.getOrSet)(loadersForKind, batchKey, () => new BatchLoader_1.BatchLoader(fn));
1295
+ }
1218
1296
  toString() {
1219
1297
  return "EntityManager";
1220
1298
  }
@@ -1485,6 +1563,7 @@ class EntityManager {
1485
1563
  this.importEntity(source, loadHint);
1486
1564
  }
1487
1565
  else if ("import" in relationOrProp) {
1566
+ // Tell our version of the entity (result) to accept/copy the relation state of the source
1488
1567
  relationOrProp.import(source[fieldName], (e) => this.importEntity(e, subHint, subHint), (entities) => {
1489
1568
  if (entities === undefined)
1490
1569
  return undefined;
@@ -1642,22 +1721,26 @@ exports.TooManyError = TooManyError;
1642
1721
  * from the currently-changed entities back to each rule's originally-defined-in entity,
1643
1722
  * and ensure those entities are added to `todos`.
1644
1723
  */
1645
- async function validateReactiveRules(todos, joinRowTodos) {
1724
+ async function validateReactiveRules(em, logger, todos, joinRowTodos) {
1725
+ logger?.logStartingValidate(em, todos);
1646
1726
  // Use a map of rule -> Set<Entity> so that we only invoke a rule once per entity,
1647
1727
  // even if it was triggered by multiple changed fields.
1648
1728
  const fns = new Map();
1649
1729
  // From the given triggered entities, follow the entity's ReactiveRule back
1650
1730
  // to the reactive rules that need ran, and queue them in the `fn` map
1651
1731
  async function followAndQueue(triggered, rule) {
1652
- (await (0, reactiveHints_1.followReverseHint)(rule.name, triggered, rule.path))
1732
+ if (triggered.length === 0)
1733
+ return;
1734
+ const found = (await (0, reactiveHints_1.followReverseHint)(rule.name, triggered, rule.path))
1653
1735
  .filter((entity) => !entity.isDeletedEntity)
1654
- .filter((e) => e instanceof rule.cstr)
1655
- .forEach((entity) => {
1656
- let entities = fns.get(rule.fn);
1657
- if (!entities) {
1658
- entities = new Set();
1659
- fns.set(rule.fn, entities);
1660
- }
1736
+ .filter((e) => e instanceof rule.cstr);
1737
+ let entities = fns.get(rule.fn);
1738
+ if (!entities) {
1739
+ entities = new Set();
1740
+ fns.set(rule.fn, entities);
1741
+ }
1742
+ logger?.logWalked(triggered, rule, found, "validate");
1743
+ found.forEach((entity) => {
1661
1744
  entities.add(entity);
1662
1745
  });
1663
1746
  }