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.
- package/build/AliasAssigner.d.ts +3 -0
- package/build/AliasAssigner.d.ts.map +1 -1
- package/build/AliasAssigner.js +12 -3
- package/build/AliasAssigner.js.map +1 -1
- package/build/EntityGraphQLFilter.d.ts.map +1 -1
- package/build/EntityGraphQLFilter.js +1 -2
- package/build/EntityGraphQLFilter.js.map +1 -1
- package/build/EntityManager.d.ts +25 -10
- package/build/EntityManager.d.ts.map +1 -1
- package/build/EntityManager.js +242 -159
- package/build/EntityManager.js.map +1 -1
- package/build/IndexManager.d.ts +1 -0
- package/build/IndexManager.d.ts.map +1 -1
- package/build/IndexManager.js +6 -3
- package/build/IndexManager.js.map +1 -1
- package/build/JoinRows.d.ts.map +1 -1
- package/build/JoinRows.js +1 -2
- package/build/JoinRows.js.map +1 -1
- package/build/PendingChanges.d.ts +26 -0
- package/build/PendingChanges.d.ts.map +1 -0
- package/build/PendingChanges.js +3 -0
- package/build/PendingChanges.js.map +1 -0
- package/build/ReactionsManager.d.ts +1 -1
- package/build/ReactionsManager.d.ts.map +1 -1
- package/build/ReactionsManager.js +8 -5
- package/build/ReactionsManager.js.map +1 -1
- package/build/batchloaders/BatchLoader.d.ts +14 -0
- package/build/batchloaders/BatchLoader.d.ts.map +1 -0
- package/build/batchloaders/BatchLoader.js +49 -0
- package/build/batchloaders/BatchLoader.js.map +1 -0
- package/build/batchloaders/loadBatchLoader.d.ts +17 -0
- package/build/batchloaders/loadBatchLoader.d.ts.map +1 -0
- package/build/batchloaders/loadBatchLoader.js +55 -0
- package/build/batchloaders/loadBatchLoader.js.map +1 -0
- package/build/batchloaders/manyToManyBatchLoader.d.ts +7 -0
- package/build/batchloaders/manyToManyBatchLoader.d.ts.map +1 -0
- package/build/batchloaders/manyToManyBatchLoader.js +57 -0
- package/build/batchloaders/manyToManyBatchLoader.js.map +1 -0
- package/build/batchloaders/oneToManyBatchLoader.d.ts +7 -0
- package/build/batchloaders/oneToManyBatchLoader.d.ts.map +1 -0
- package/build/{dataloaders/oneToManyDataLoader.js → batchloaders/oneToManyBatchLoader.js} +9 -18
- package/build/batchloaders/oneToManyBatchLoader.js.map +1 -0
- package/build/batchloaders/oneToOneBatchLoader.d.ts +7 -0
- package/build/batchloaders/oneToOneBatchLoader.d.ts.map +1 -0
- package/build/{dataloaders/oneToOneDataLoader.js → batchloaders/oneToOneBatchLoader.js} +10 -16
- package/build/batchloaders/oneToOneBatchLoader.js.map +1 -0
- package/build/{dataloaders/populateDataLoader.d.ts → batchloaders/populateBatchLoader.d.ts} +5 -5
- package/build/batchloaders/populateBatchLoader.d.ts.map +1 -0
- package/build/{dataloaders/populateDataLoader.js → batchloaders/populateBatchLoader.js} +97 -54
- package/build/batchloaders/populateBatchLoader.js.map +1 -0
- package/build/batchloaders/recursiveChildrenBatchLoader.d.ts +7 -0
- package/build/batchloaders/recursiveChildrenBatchLoader.d.ts.map +1 -0
- package/build/{dataloaders/recursiveChildrenDataLoader.js → batchloaders/recursiveChildrenBatchLoader.js} +4 -6
- package/build/batchloaders/recursiveChildrenBatchLoader.js.map +1 -0
- package/build/batchloaders/recursiveM2mBatchLoader.d.ts +7 -0
- package/build/batchloaders/recursiveM2mBatchLoader.d.ts.map +1 -0
- package/build/batchloaders/recursiveM2mBatchLoader.js +84 -0
- package/build/batchloaders/recursiveM2mBatchLoader.js.map +1 -0
- package/build/batchloaders/recursiveParentsBatchLoader.d.ts +7 -0
- package/build/batchloaders/recursiveParentsBatchLoader.d.ts.map +1 -0
- package/build/{dataloaders/recursiveParentsDataLoader.js → batchloaders/recursiveParentsBatchLoader.js} +4 -5
- package/build/batchloaders/recursiveParentsBatchLoader.js.map +1 -0
- package/build/changes.js +2 -2
- package/build/changes.js.map +1 -1
- package/build/config.d.ts +13 -0
- package/build/config.d.ts.map +1 -1
- package/build/config.js +26 -1
- package/build/config.js.map +1 -1
- package/build/dataloaders/findIdsDataLoader.d.ts.map +1 -1
- package/build/dataloaders/findIdsDataLoader.js +7 -5
- package/build/dataloaders/findIdsDataLoader.js.map +1 -1
- package/build/dataloaders/findOrCreateDataLoader.d.ts.map +1 -1
- package/build/dataloaders/findOrCreateDataLoader.js +3 -2
- package/build/dataloaders/findOrCreateDataLoader.js.map +1 -1
- package/build/index.d.ts +2 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +4 -2
- package/build/index.js.map +1 -1
- package/build/logging/ReactionLogger.d.ts +8 -4
- package/build/logging/ReactionLogger.d.ts.map +1 -1
- package/build/logging/ReactionLogger.js +11 -4
- package/build/logging/ReactionLogger.js.map +1 -1
- package/build/newTestInstance.d.ts +15 -0
- package/build/newTestInstance.d.ts.map +1 -1
- package/build/newTestInstance.js +49 -6
- package/build/newTestInstance.js.map +1 -1
- package/build/preloading/JsonAggregatePreloader.js +29 -24
- package/build/preloading/JsonAggregatePreloader.js.map +1 -1
- package/build/reactiveHints.d.ts.map +1 -1
- package/build/reactiveHints.js +25 -0
- package/build/reactiveHints.js.map +1 -1
- package/build/relations/Collection.d.ts +6 -1
- package/build/relations/Collection.d.ts.map +1 -1
- package/build/relations/Collection.js.map +1 -1
- package/build/relations/ManyToManyCollection.d.ts +8 -1
- package/build/relations/ManyToManyCollection.d.ts.map +1 -1
- package/build/relations/ManyToManyCollection.js +338 -130
- package/build/relations/ManyToManyCollection.js.map +1 -1
- package/build/relations/ManyToOneReference.d.ts +2 -6
- package/build/relations/ManyToOneReference.d.ts.map +1 -1
- package/build/relations/ManyToOneReference.js +115 -78
- package/build/relations/ManyToOneReference.js.map +1 -1
- package/build/relations/OneToManyCollection.d.ts +8 -4
- package/build/relations/OneToManyCollection.d.ts.map +1 -1
- package/build/relations/OneToManyCollection.js +415 -173
- package/build/relations/OneToManyCollection.js.map +1 -1
- package/build/relations/OneToOneReference.d.ts.map +1 -1
- package/build/relations/OneToOneReference.js +12 -7
- package/build/relations/OneToOneReference.js.map +1 -1
- package/build/relations/ReactiveManyToMany.js +4 -4
- package/build/relations/ReactiveManyToMany.js.map +1 -1
- package/build/relations/ReactiveManyToManyOtherSide.js +3 -3
- package/build/relations/ReactiveManyToManyOtherSide.js.map +1 -1
- package/build/relations/ReadOnlyCollection.d.ts.map +1 -1
- package/build/relations/ReadOnlyCollection.js +2 -1
- package/build/relations/ReadOnlyCollection.js.map +1 -1
- package/build/relations/RecursiveCollection.d.ts +29 -1
- package/build/relations/RecursiveCollection.d.ts.map +1 -1
- package/build/relations/RecursiveCollection.js +160 -9
- package/build/relations/RecursiveCollection.js.map +1 -1
- package/build/relations/index.d.ts +1 -1
- package/build/relations/index.d.ts.map +1 -1
- package/build/relations/index.js +2 -1
- package/build/relations/index.js.map +1 -1
- package/package.json +2 -2
- package/build/dataloaders/loadDataLoader.d.ts +0 -11
- package/build/dataloaders/loadDataLoader.d.ts.map +0 -1
- package/build/dataloaders/loadDataLoader.js +0 -54
- package/build/dataloaders/loadDataLoader.js.map +0 -1
- package/build/dataloaders/manyToManyDataLoader.d.ts +0 -8
- package/build/dataloaders/manyToManyDataLoader.d.ts.map +0 -1
- package/build/dataloaders/manyToManyDataLoader.js +0 -74
- package/build/dataloaders/manyToManyDataLoader.js.map +0 -1
- package/build/dataloaders/oneToManyDataLoader.d.ts +0 -7
- package/build/dataloaders/oneToManyDataLoader.d.ts.map +0 -1
- package/build/dataloaders/oneToManyDataLoader.js.map +0 -1
- package/build/dataloaders/oneToOneDataLoader.d.ts +0 -7
- package/build/dataloaders/oneToOneDataLoader.d.ts.map +0 -1
- package/build/dataloaders/oneToOneDataLoader.js.map +0 -1
- package/build/dataloaders/populateDataLoader.d.ts.map +0 -1
- package/build/dataloaders/populateDataLoader.js.map +0 -1
- package/build/dataloaders/recursiveChildrenDataLoader.d.ts +0 -7
- package/build/dataloaders/recursiveChildrenDataLoader.d.ts.map +0 -1
- package/build/dataloaders/recursiveChildrenDataLoader.js.map +0 -1
- package/build/dataloaders/recursiveParentsDataLoader.d.ts +0 -7
- package/build/dataloaders/recursiveParentsDataLoader.d.ts.map +0 -1
- package/build/dataloaders/recursiveParentsDataLoader.js.map +0 -1
package/build/EntityManager.js
CHANGED
|
@@ -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
|
-
#
|
|
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
|
-
|
|
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
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
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(`${
|
|
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
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
}
|
|
634
|
-
const
|
|
635
|
-
|
|
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
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
}
|
|
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,
|
|
717
|
-
await
|
|
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,
|
|
723
|
-
await
|
|
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,
|
|
730
|
-
await
|
|
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
|
|
1061
|
-
|
|
1062
|
-
|
|
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,
|
|
1068
|
-
.load({
|
|
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
|
|
1074
|
-
//
|
|
1075
|
-
const [custom, builtin] = (0, utils_1.partition)(
|
|
1076
|
-
.filter((e) =>
|
|
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
|
-
|
|
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
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
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
|
}
|