joist-core 2.1.0 → 2.2.0-next.10
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/BaseEntity.js +1 -1
- package/build/BaseEntity.js.map +1 -1
- package/build/ConditionBuilder.d.ts +1 -22
- package/build/ConditionBuilder.d.ts.map +1 -1
- package/build/ConditionBuilder.js +6 -72
- package/build/ConditionBuilder.js.map +1 -1
- package/build/EntityFilter.d.ts +4 -2
- package/build/EntityFilter.d.ts.map +1 -1
- package/build/EntityGraphQLFilter.d.ts +2 -0
- package/build/EntityGraphQLFilter.d.ts.map +1 -1
- package/build/EntityGraphQLFilter.js.map +1 -1
- package/build/EntityManager.d.ts +27 -39
- package/build/EntityManager.d.ts.map +1 -1
- package/build/EntityManager.js +243 -122
- package/build/EntityManager.js.map +1 -1
- package/build/EntityMetadata.d.ts +19 -1
- package/build/EntityMetadata.d.ts.map +1 -1
- package/build/EntityMetadata.js.map +1 -1
- package/build/IndexManager.d.ts +2 -2
- package/build/IndexManager.d.ts.map +1 -1
- package/build/IndexManager.js +22 -16
- package/build/IndexManager.js.map +1 -1
- package/build/InstanceData.d.ts +21 -3
- package/build/InstanceData.d.ts.map +1 -1
- package/build/InstanceData.js +70 -10
- package/build/InstanceData.js.map +1 -1
- package/build/IsLoadedCache.d.ts +1 -1
- package/build/IsLoadedCache.d.ts.map +1 -1
- package/build/IsLoadedCache.js +37 -26
- package/build/IsLoadedCache.js.map +1 -1
- package/build/JoinRows.d.ts +1 -1
- package/build/JoinRows.js +6 -1
- package/build/JoinRows.js.map +1 -1
- package/build/PluginManager.d.ts +12 -0
- package/build/PluginManager.d.ts.map +1 -1
- package/build/PluginManager.js +18 -2
- package/build/PluginManager.js.map +1 -1
- package/build/QueryParser.collectionJoins.d.ts +27 -0
- package/build/QueryParser.collectionJoins.d.ts.map +1 -0
- package/build/QueryParser.collectionJoins.js +466 -0
- package/build/QueryParser.collectionJoins.js.map +1 -0
- package/build/QueryParser.collectionJoins.test.d.ts +2 -0
- package/build/QueryParser.collectionJoins.test.d.ts.map +1 -0
- package/build/QueryParser.collectionJoins.test.js +772 -0
- package/build/QueryParser.collectionJoins.test.js.map +1 -0
- package/build/QueryParser.d.ts +71 -11
- package/build/QueryParser.d.ts.map +1 -1
- package/build/QueryParser.js +39 -33
- package/build/QueryParser.js.map +1 -1
- package/build/QueryParser.pruning.d.ts +4 -2
- package/build/QueryParser.pruning.d.ts.map +1 -1
- package/build/QueryParser.pruning.js +87 -10
- package/build/QueryParser.pruning.js.map +1 -1
- package/build/QueryParser.pruning.test.d.ts +2 -0
- package/build/QueryParser.pruning.test.d.ts.map +1 -0
- package/build/QueryParser.pruning.test.js +106 -0
- package/build/QueryParser.pruning.test.js.map +1 -0
- package/build/QueryVisitor.d.ts.map +1 -1
- package/build/QueryVisitor.js +22 -0
- package/build/QueryVisitor.js.map +1 -1
- package/build/ReactionsManager.d.ts +2 -1
- package/build/ReactionsManager.d.ts.map +1 -1
- package/build/ReactionsManager.js +55 -51
- package/build/ReactionsManager.js.map +1 -1
- package/build/batchloaders/BatchLoader.d.ts.map +1 -1
- package/build/batchloaders/BatchLoader.js +9 -1
- package/build/batchloaders/BatchLoader.js.map +1 -1
- package/build/batchloaders/manyToManyBatchLoader.js +3 -1
- package/build/batchloaders/manyToManyBatchLoader.js.map +1 -1
- package/build/batchloaders/populateBatchLoader.d.ts.map +1 -1
- package/build/batchloaders/populateBatchLoader.js +2 -1
- package/build/batchloaders/populateBatchLoader.js.map +1 -1
- package/build/batchloaders/recursiveM2mBatchLoader.d.ts.map +1 -1
- package/build/batchloaders/recursiveM2mBatchLoader.js +3 -1
- package/build/batchloaders/recursiveM2mBatchLoader.js.map +1 -1
- package/build/changes.d.ts.map +1 -1
- package/build/changes.js +1 -4
- package/build/changes.js.map +1 -1
- package/build/config.d.ts.map +1 -1
- package/build/config.js +18 -10
- package/build/config.js.map +1 -1
- package/build/configure.d.ts +3 -3
- package/build/configure.d.ts.map +1 -1
- package/build/configure.js +66 -2
- package/build/configure.js.map +1 -1
- package/build/dataloaders/fastWhereFilterHash.d.ts +15 -0
- package/build/dataloaders/fastWhereFilterHash.d.ts.map +1 -0
- package/build/dataloaders/fastWhereFilterHash.js +164 -0
- package/build/dataloaders/fastWhereFilterHash.js.map +1 -0
- package/build/dataloaders/fastWhereFilterHash.test.d.ts +2 -0
- package/build/dataloaders/fastWhereFilterHash.test.d.ts.map +1 -0
- package/build/dataloaders/fastWhereFilterHash.test.js +59 -0
- package/build/dataloaders/fastWhereFilterHash.test.js.map +1 -0
- package/build/dataloaders/findCountDataLoader.d.ts +1 -2
- package/build/dataloaders/findCountDataLoader.d.ts.map +1 -1
- package/build/dataloaders/findCountDataLoader.js +16 -13
- package/build/dataloaders/findCountDataLoader.js.map +1 -1
- package/build/dataloaders/findDataLoader.d.ts +7 -3
- package/build/dataloaders/findDataLoader.d.ts.map +1 -1
- package/build/dataloaders/findDataLoader.js +105 -91
- package/build/dataloaders/findDataLoader.js.map +1 -1
- package/build/dataloaders/findIdsDataLoader.d.ts +1 -2
- package/build/dataloaders/findIdsDataLoader.d.ts.map +1 -1
- package/build/dataloaders/findIdsDataLoader.js +16 -15
- package/build/dataloaders/findIdsDataLoader.js.map +1 -1
- package/build/dataloaders/findOrCreateDataLoader.d.ts.map +1 -1
- package/build/dataloaders/findOrCreateDataLoader.js +7 -2
- package/build/dataloaders/findOrCreateDataLoader.js.map +1 -1
- package/build/dataloaders/findPaginatedDataLoader.d.ts +7 -0
- package/build/dataloaders/findPaginatedDataLoader.d.ts.map +1 -0
- package/build/dataloaders/findPaginatedDataLoader.js +79 -0
- package/build/dataloaders/findPaginatedDataLoader.js.map +1 -0
- package/build/defaults.d.ts.map +1 -1
- package/build/defaults.js +49 -42
- package/build/defaults.js.map +1 -1
- package/build/drivers/EntityWriter.js +13 -7
- package/build/drivers/EntityWriter.js.map +1 -1
- package/build/drivers/buildRawQuery.d.ts +6 -4
- package/build/drivers/buildRawQuery.d.ts.map +1 -1
- package/build/drivers/buildRawQuery.js +11 -6
- package/build/drivers/buildRawQuery.js.map +1 -1
- package/build/drivers/buildUtils.d.ts +7 -2
- package/build/drivers/buildUtils.d.ts.map +1 -1
- package/build/drivers/buildUtils.js +14 -5
- package/build/drivers/buildUtils.js.map +1 -1
- package/build/fields.d.ts.map +1 -1
- package/build/fields.js +31 -12
- package/build/fields.js.map +1 -1
- package/build/index.d.ts +2 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +7 -5
- package/build/index.js.map +1 -1
- package/build/json.d.ts +3 -3
- package/build/json.d.ts.map +1 -1
- package/build/json.js +4 -4
- package/build/json.js.map +1 -1
- package/build/keys.d.ts.map +1 -1
- package/build/keys.js +8 -6
- package/build/keys.js.map +1 -1
- package/build/loadHints.d.ts +13 -4
- package/build/loadHints.d.ts.map +1 -1
- package/build/loadHints.js +63 -9
- package/build/loadHints.js.map +1 -1
- package/build/loadLens.d.ts +11 -0
- package/build/loadLens.d.ts.map +1 -1
- package/build/loadLens.js +25 -8
- package/build/loadLens.js.map +1 -1
- package/build/normalizeHints.d.ts +7 -0
- package/build/normalizeHints.d.ts.map +1 -1
- package/build/normalizeHints.js +40 -2
- package/build/normalizeHints.js.map +1 -1
- package/build/preloading/JsonAggregatePreloader.js +6 -2
- package/build/preloading/JsonAggregatePreloader.js.map +1 -1
- package/build/reactiveHints.d.ts +24 -7
- package/build/reactiveHints.d.ts.map +1 -1
- package/build/reactiveHints.js +45 -28
- package/build/reactiveHints.js.map +1 -1
- package/build/relations/AbstractRelationImpl.d.ts +7 -2
- package/build/relations/AbstractRelationImpl.d.ts.map +1 -1
- package/build/relations/AbstractRelationImpl.js.map +1 -1
- package/build/relations/AsyncProperty.d.ts +36 -0
- package/build/relations/AsyncProperty.d.ts.map +1 -0
- package/build/relations/AsyncProperty.js +80 -0
- package/build/relations/AsyncProperty.js.map +1 -0
- package/build/relations/{ReactiveQueryField.d.ts → AsyncReactiveField.d.ts} +10 -10
- package/build/relations/AsyncReactiveField.d.ts.map +1 -0
- package/build/relations/{ReactiveQueryField.js → AsyncReactiveField.js} +19 -19
- package/build/relations/{ReactiveQueryField.js.map → AsyncReactiveField.js.map} +1 -1
- package/build/relations/ReactiveField.d.ts +7 -9
- package/build/relations/ReactiveField.d.ts.map +1 -1
- package/build/relations/ReactiveField.js +5 -10
- package/build/relations/ReactiveField.js.map +1 -1
- package/build/relations/ReactiveGetter.d.ts +5 -5
- package/build/relations/ReactiveGetter.d.ts.map +1 -1
- package/build/relations/ReactiveGetter.js +3 -3
- package/build/relations/ReactiveGetter.js.map +1 -1
- package/build/relations/ReactiveReference.d.ts +2 -2
- package/build/relations/ReactiveReference.d.ts.map +1 -1
- package/build/relations/ReactiveReference.js +100 -36
- package/build/relations/ReactiveReference.js.map +1 -1
- package/build/relations/hasOneThrough.d.ts.map +1 -1
- package/build/relations/hasOneThrough.js +6 -4
- package/build/relations/hasOneThrough.js.map +1 -1
- package/build/relations/{hasAsyncProperty.d.ts → hasProperty.d.ts} +12 -12
- package/build/relations/hasProperty.d.ts.map +1 -0
- package/build/relations/{hasAsyncProperty.js → hasProperty.js} +20 -20
- package/build/relations/hasProperty.js.map +1 -0
- package/build/relations/index.d.ts +3 -2
- package/build/relations/index.d.ts.map +1 -1
- package/build/relations/index.js +16 -11
- package/build/relations/index.js.map +1 -1
- package/build/resurrection.d.ts +10 -0
- package/build/resurrection.d.ts.map +1 -0
- package/build/resurrection.js +93 -0
- package/build/resurrection.js.map +1 -0
- package/build/rules.js +3 -3
- package/build/trusted.d.ts +1 -1
- package/build/trusted.d.ts.map +1 -1
- package/build/trusted.js +1 -1
- package/build/trusted.js.map +1 -1
- package/build/upsert.d.ts.map +1 -1
- package/build/upsert.js +26 -10
- package/build/upsert.js.map +1 -1
- package/build/utils.d.ts +2 -0
- package/build/utils.d.ts.map +1 -1
- package/build/utils.js +12 -0
- package/build/utils.js.map +1 -1
- package/build/withLoaded.js +5 -5
- package/build/withLoaded.js.map +1 -1
- package/package.json +10 -12
- package/build/caches.d.ts +0 -6
- package/build/caches.d.ts.map +0 -1
- package/build/caches.js +0 -42
- package/build/caches.js.map +0 -1
- package/build/relations/ReactiveQueryField.d.ts.map +0 -1
- package/build/relations/hasAsyncProperty.d.ts.map +0 -1
- package/build/relations/hasAsyncProperty.js.map +0 -1
package/build/EntityManager.js
CHANGED
|
@@ -29,7 +29,6 @@ const IndexManager_1 = require("./IndexManager");
|
|
|
29
29
|
// We alias `Entity => EntityW` to denote "Entity wide" i.e. the non-narrowed Entity
|
|
30
30
|
const loadBatchLoader_1 = require("./batchloaders/loadBatchLoader");
|
|
31
31
|
const populateBatchLoader_1 = require("./batchloaders/populateBatchLoader");
|
|
32
|
-
const caches_1 = require("./caches");
|
|
33
32
|
const config_1 = require("./config");
|
|
34
33
|
const configure_1 = require("./configure");
|
|
35
34
|
const findByUniqueDataLoader_1 = require("./dataloaders/findByUniqueDataLoader");
|
|
@@ -37,17 +36,20 @@ const findCountDataLoader_1 = require("./dataloaders/findCountDataLoader");
|
|
|
37
36
|
const findDataLoader_1 = require("./dataloaders/findDataLoader");
|
|
38
37
|
const findIdsDataLoader_1 = require("./dataloaders/findIdsDataLoader");
|
|
39
38
|
const findOrCreateDataLoader_1 = require("./dataloaders/findOrCreateDataLoader");
|
|
39
|
+
const findPaginatedDataLoader_1 = require("./dataloaders/findPaginatedDataLoader");
|
|
40
40
|
const Entity_1 = require("./Entity");
|
|
41
41
|
const FlushLock_1 = require("./FlushLock");
|
|
42
42
|
const index_1 = require("./index");
|
|
43
43
|
const IsLoadedCache_1 = require("./IsLoadedCache");
|
|
44
44
|
const JoinRows_1 = require("./JoinRows");
|
|
45
|
+
const loadHints_1 = require("./loadHints");
|
|
45
46
|
const newEntity_1 = require("./newEntity");
|
|
46
47
|
const newTestInstance_1 = require("./newTestInstance");
|
|
47
48
|
const PluginManager_1 = require("./PluginManager");
|
|
48
49
|
const ReactionsManager_1 = require("./ReactionsManager");
|
|
49
50
|
const reactiveHints_1 = require("./reactiveHints");
|
|
50
51
|
const relations_1 = require("./relations");
|
|
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");
|
|
@@ -86,6 +88,8 @@ class EntityManager {
|
|
|
86
88
|
txn;
|
|
87
89
|
entityLimit = defaultEntityLimit;
|
|
88
90
|
#entitiesArray = [];
|
|
91
|
+
// Incrementally track dirty entities so we don't have to scan `#entityArray` during flush
|
|
92
|
+
#maybePendingFlushEntities = new Set();
|
|
89
93
|
// Indexes the currently loaded entities by their tagged ids and `toTaggedString` ids (i.e. `a#`). This fixes
|
|
90
94
|
// real-world performance issues where `findExistingInstance` scanning `#entities` was an `O(n^2)`.
|
|
91
95
|
#entitiesById = new Map();
|
|
@@ -153,6 +157,12 @@ class EntityManager {
|
|
|
153
157
|
indexManager: this.#indexManager,
|
|
154
158
|
isLoadedCache: this.#isLoadedCache,
|
|
155
159
|
pluginManager,
|
|
160
|
+
markMaybePending(entity) {
|
|
161
|
+
em.#maybePendingFlushEntities.add(entity);
|
|
162
|
+
},
|
|
163
|
+
unmarkMaybePending(entity) {
|
|
164
|
+
em.#maybePendingFlushEntities.delete(entity);
|
|
165
|
+
},
|
|
156
166
|
isMerging(entity) {
|
|
157
167
|
return em.#merging?.has(entity) ?? false;
|
|
158
168
|
},
|
|
@@ -246,9 +256,9 @@ class EntityManager {
|
|
|
246
256
|
async find(type, where, options) {
|
|
247
257
|
const { populate, ...rest } = options || {};
|
|
248
258
|
const settings = { where, ...rest };
|
|
249
|
-
const result = await (
|
|
250
|
-
.
|
|
251
|
-
.catch(function find(err) {
|
|
259
|
+
const result = await (hasPaginationSettings(rest)
|
|
260
|
+
? (0, findPaginatedDataLoader_1.findPaginatedDataLoader)(this, type, settings, populate)
|
|
261
|
+
: (0, findDataLoader_1.findDataLoader)(this, type, settings, populate)).catch(function find(err) {
|
|
252
262
|
throw appendStack(err, new Error());
|
|
253
263
|
});
|
|
254
264
|
if (populate) {
|
|
@@ -256,30 +266,52 @@ class EntityManager {
|
|
|
256
266
|
}
|
|
257
267
|
return result;
|
|
258
268
|
}
|
|
259
|
-
|
|
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
|
-
}
|
|
269
|
+
/** Runs the post-parse find pipeline: plugins mutate the logical AST, then Joist optimizes/prunes before SQL. */
|
|
271
270
|
async executeFind(meta, operation, parsed, settings) {
|
|
271
|
+
const { checkLimit, findSettings } = this.prepareFind(meta, operation, parsed, settings);
|
|
272
|
+
return this.executePreparedFind(meta, operation, parsed, findSettings, checkLimit);
|
|
273
|
+
}
|
|
274
|
+
/** Executes a query that has already had find hooks and optimizations applied. */
|
|
275
|
+
async executePreparedFind(meta, operation, parsed, findSettings, checkLimit) {
|
|
272
276
|
const { pluginManager } = getEmInternalApi(this);
|
|
273
|
-
|
|
274
|
-
|
|
277
|
+
const rows = await this.driver.executeFind(this, parsed, findSettings);
|
|
278
|
+
// Check by default unless explicitly disabled or the caller removed the LIMIT via `limit: undefined`
|
|
279
|
+
const shouldCheck = checkLimit ?? !("limit" in findSettings && findSettings.limit === undefined);
|
|
280
|
+
if (shouldCheck && rows.length >= this.entityLimit) {
|
|
281
|
+
throw new Error(`Query returned more than ${this.entityLimit} entityLimit rows`);
|
|
282
|
+
}
|
|
275
283
|
pluginManager.afterFind(meta, operation, rows);
|
|
276
284
|
return rows;
|
|
277
285
|
}
|
|
278
|
-
|
|
279
|
-
|
|
286
|
+
/**
|
|
287
|
+
* Runs pre-SQL find hooks and optimizations against a parsed query.
|
|
288
|
+
*
|
|
289
|
+
* This allows plugins to see "pre-batched" / "logical" query ASTs, instead of our
|
|
290
|
+
* more complicated `_find` batched queries. The flow would be:
|
|
291
|
+
*
|
|
292
|
+
* - A loader calls `prepareFind(originalQuery)`
|
|
293
|
+
* - Plugins inspect/modify the query as/if needed
|
|
294
|
+
* - The loader crafts a new, more complicated query that embeds the originalQuery
|
|
295
|
+
* - The loader calls `executePreparedFind` with the 2nd query
|
|
296
|
+
*/
|
|
297
|
+
prepareFind(meta, operation, parsed, settings) {
|
|
298
|
+
const { checkLimit, ...findSettings } = settings;
|
|
299
|
+
const { pluginManager } = getEmInternalApi(this);
|
|
300
|
+
// Plugins may mutate the settings object, so return the post-hook version that loaders must reuse.
|
|
301
|
+
pluginManager.beforeFind(meta, operation, parsed, findSettings);
|
|
302
|
+
(0, index_1.optimizeCollectionJoins)(parsed, settings);
|
|
303
|
+
return { checkLimit, findSettings };
|
|
280
304
|
}
|
|
281
|
-
async
|
|
282
|
-
|
|
305
|
+
async findGql(type, where, options) {
|
|
306
|
+
if (!options) {
|
|
307
|
+
return this.find(type, where);
|
|
308
|
+
}
|
|
309
|
+
const normalized = { ...options };
|
|
310
|
+
if ("limit" in normalized)
|
|
311
|
+
normalized.limit = normalized.limit ?? undefined;
|
|
312
|
+
if ("offset" in normalized)
|
|
313
|
+
normalized.offset = normalized.offset ?? undefined;
|
|
314
|
+
return this.find(type, where, normalized);
|
|
283
315
|
}
|
|
284
316
|
async findOne(type, where, options) {
|
|
285
317
|
const list = await this.find(type, where, options);
|
|
@@ -337,9 +369,7 @@ class EntityManager {
|
|
|
337
369
|
*/
|
|
338
370
|
async findCount(type, where, options = {}) {
|
|
339
371
|
const settings = { where, ...options };
|
|
340
|
-
let count = await (0, findCountDataLoader_1.findCountDataLoader)(this, type, settings)
|
|
341
|
-
.load(settings)
|
|
342
|
-
.catch(function findCount(err) {
|
|
372
|
+
let count = await (0, findCountDataLoader_1.findCountDataLoader)(this, type, settings).catch(function findCount(err) {
|
|
343
373
|
throw appendStack(err, new Error());
|
|
344
374
|
});
|
|
345
375
|
// If the user is do "count all", we can adjust the number up/down based on
|
|
@@ -371,9 +401,7 @@ class EntityManager {
|
|
|
371
401
|
*/
|
|
372
402
|
async findIds(type, where, options = {}) {
|
|
373
403
|
const settings = { where, ...options };
|
|
374
|
-
return (0, findIdsDataLoader_1.findIdsDataLoader)(this, type, settings)
|
|
375
|
-
.load(settings)
|
|
376
|
-
.catch(function findIds(err) {
|
|
404
|
+
return (0, findIdsDataLoader_1.findIdsDataLoader)(this, type, settings).catch(function findIds(err) {
|
|
377
405
|
throw appendStack(err, new Error());
|
|
378
406
|
});
|
|
379
407
|
}
|
|
@@ -662,48 +690,86 @@ class EntityManager {
|
|
|
662
690
|
}
|
|
663
691
|
async loadAll(type, _ids, hint) {
|
|
664
692
|
const meta = (0, index_1.getMetadata)(type);
|
|
665
|
-
|
|
666
|
-
const
|
|
667
|
-
|
|
693
|
+
// Use pre-allocated arrays/for loops instead of `.filter`s since this can be a hot spot
|
|
694
|
+
const ids = new Array(_ids.length);
|
|
695
|
+
const entities = new Array(_ids.length);
|
|
696
|
+
let idsToLoad;
|
|
697
|
+
let positionsToLoad;
|
|
698
|
+
for (let i = 0; i < _ids.length; i++) {
|
|
699
|
+
const id = (0, index_1.tagId)(meta, _ids[i]);
|
|
700
|
+
ids[i] = id;
|
|
701
|
+
const entity = this.findExistingInstance(id);
|
|
702
|
+
if (entity) {
|
|
703
|
+
entities[i] = entity;
|
|
704
|
+
}
|
|
705
|
+
else {
|
|
706
|
+
(idsToLoad ??= []).push(id);
|
|
707
|
+
(positionsToLoad ??= []).push(i);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
if (idsToLoad && idsToLoad.length > 0) {
|
|
668
711
|
await (0, loadBatchLoader_1.loadBatchLoader)(this, meta)
|
|
669
712
|
.loadAll(idsToLoad.map((id) => ({ taggedId: id, hint })))
|
|
670
713
|
.catch(function loadAll(err) {
|
|
671
714
|
throw appendStack(err, new Error());
|
|
672
715
|
});
|
|
716
|
+
for (const i of positionsToLoad) {
|
|
717
|
+
entities[i] = this.findExistingInstance(ids[i]);
|
|
718
|
+
}
|
|
673
719
|
}
|
|
674
|
-
|
|
675
|
-
for (
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
720
|
+
let idsNotFound;
|
|
721
|
+
for (let i = 0; i < entities.length; i++) {
|
|
722
|
+
if (entities[i] === undefined) {
|
|
723
|
+
(idsNotFound ??= []).push(ids[i]);
|
|
724
|
+
}
|
|
679
725
|
}
|
|
680
|
-
if (
|
|
681
|
-
const idsNotFound = ids.filter((_, i) => entities[i] === undefined);
|
|
726
|
+
if (idsNotFound) {
|
|
682
727
|
throw new NotFoundError(`${idsNotFound.join(",")} were not found`);
|
|
683
728
|
}
|
|
729
|
+
const loadedEntities = entities;
|
|
684
730
|
if (hint) {
|
|
685
|
-
await this.populate(
|
|
731
|
+
await this.populate(loadedEntities, hint);
|
|
686
732
|
}
|
|
687
733
|
if (meta.inheritanceType === "sti" && meta.baseType) {
|
|
688
|
-
const wrongType =
|
|
734
|
+
const wrongType = loadedEntities.filter((e) => !(e instanceof meta.cstr));
|
|
689
735
|
if (wrongType.length > 0) {
|
|
690
736
|
throw new Error(`${wrongType.join(", ")} were not of type ${meta.cstr.name}`);
|
|
691
737
|
}
|
|
692
738
|
}
|
|
693
|
-
return
|
|
739
|
+
return loadedEntities;
|
|
694
740
|
}
|
|
695
741
|
async loadAllIfExists(type, _ids, hint) {
|
|
696
742
|
const meta = (0, index_1.getMetadata)(type);
|
|
697
|
-
|
|
698
|
-
const
|
|
699
|
-
|
|
743
|
+
// Use pre-allocated arrays/for loops instead of `.filter`s since this can be a hot spot
|
|
744
|
+
const ids = new Array(_ids.length);
|
|
745
|
+
const entities = [];
|
|
746
|
+
let idsToLoad;
|
|
747
|
+
// Ensure the ids are tagged, and find any not-yet-loaded
|
|
748
|
+
for (let i = 0; i < _ids.length; i++) {
|
|
749
|
+
const id = (0, index_1.tagId)(meta, _ids[i]);
|
|
750
|
+
ids[i] = id;
|
|
751
|
+
const entity = this.findExistingInstance(id);
|
|
752
|
+
if (entity) {
|
|
753
|
+
entities.push(entity);
|
|
754
|
+
}
|
|
755
|
+
else {
|
|
756
|
+
(idsToLoad ??= []).push(id);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
if (idsToLoad && idsToLoad.length > 0) {
|
|
700
760
|
await (0, loadBatchLoader_1.loadBatchLoader)(this, meta)
|
|
701
761
|
.loadAll(idsToLoad.map((id) => ({ taggedId: id, hint })))
|
|
702
762
|
.catch(function loadAllIfExists(err) {
|
|
703
763
|
throw appendStack(err, new Error());
|
|
704
764
|
});
|
|
765
|
+
// Now that everything is loaded, recalc `entities`
|
|
766
|
+
entities.length = 0;
|
|
767
|
+
for (const id of ids) {
|
|
768
|
+
const entity = this.findExistingInstance(id);
|
|
769
|
+
if (entity)
|
|
770
|
+
entities.push(entity);
|
|
771
|
+
}
|
|
705
772
|
}
|
|
706
|
-
const entities = ids.map((id) => this.findExistingInstance(id)).filter(Boolean);
|
|
707
773
|
if (hint) {
|
|
708
774
|
await this.populate(entities, hint);
|
|
709
775
|
}
|
|
@@ -754,6 +820,12 @@ class EntityManager {
|
|
|
754
820
|
if (list.length === 0) {
|
|
755
821
|
return !fn ? entityOrList : fn(entityOrList);
|
|
756
822
|
}
|
|
823
|
+
// Avoid building a HintTree/batchloader for persisted entities when everything is already loaded; in the phase 9
|
|
824
|
+
// benchmark this moved already-loaded populates from ~1.10ms to ~0.89ms, and nested already-loaded populates from
|
|
825
|
+
// ~2.39ms to ~1.94ms.
|
|
826
|
+
if (!opts.forceReload && list.every((entity) => !entity.isNewEntity && (0, loadHints_1.isLoadedForPopulate)(entity, hintOpt))) {
|
|
827
|
+
return fn ? fn(entityOrList) : entityOrList;
|
|
828
|
+
}
|
|
757
829
|
const meta = (0, index_1.getMetadata)(list[0]);
|
|
758
830
|
if (this.#preloader) {
|
|
759
831
|
// If we can preload, prevent promise deadlocking by one large-batch preload populate (which can't have
|
|
@@ -802,7 +874,7 @@ class EntityManager {
|
|
|
802
874
|
delete(entityOrArray) {
|
|
803
875
|
for (const entity of (0, utils_1.toArray)(entityOrArray)) {
|
|
804
876
|
// Early return if already deleted.
|
|
805
|
-
const alreadyMarked = (0, BaseEntity_1.getInstanceData)(entity).markDeleted(
|
|
877
|
+
const alreadyMarked = (0, BaseEntity_1.getInstanceData)(entity).markDeleted();
|
|
806
878
|
if (!alreadyMarked)
|
|
807
879
|
continue;
|
|
808
880
|
// Any derived fields that read this entity will need recalc-d
|
|
@@ -878,11 +950,11 @@ class EntityManager {
|
|
|
878
950
|
const createdThenDeleted = new Set();
|
|
879
951
|
// We'll only invoke hooks once/entity (the 1st time that entity goes through runHooksOnPendingEntities)
|
|
880
952
|
const hooksInvoked = new Set();
|
|
881
|
-
// Make sure two
|
|
953
|
+
// Make sure two AsyncReactiveFields don't ping-pong each other forever
|
|
882
954
|
let hookLoops = 0;
|
|
883
955
|
let now = getNow();
|
|
884
956
|
const suppressedDefaultTypeErrors = [];
|
|
885
|
-
// Make a lambda that we can invoke multiple times, if we loop for
|
|
957
|
+
// Make a lambda that we can invoke multiple times, if we loop for AsyncReactiveFields
|
|
886
958
|
const runHooksOnPendingEntities = async () => {
|
|
887
959
|
if (hookLoops++ >= 10)
|
|
888
960
|
throw new Error("runHooksOnPendingEntities has ran 10 iterations, aborting");
|
|
@@ -898,19 +970,19 @@ class EntityManager {
|
|
|
898
970
|
const pendingHooks = new Set();
|
|
899
971
|
// Subset of pendingFlush entities that had hooks invoked in a prior `runHooksOnPendingEntities`
|
|
900
972
|
const alreadyRanHooks = new Set();
|
|
901
|
-
findPendingFlushEntities(this
|
|
902
|
-
// If we're re-looping for
|
|
973
|
+
findPendingFlushEntities(this.#maybePendingFlushEntities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks);
|
|
974
|
+
// If we're re-looping for AsyncReactiveField, make sure to bump updatedAt
|
|
903
975
|
// each time, so that for an INSERT-then-UPDATE the triggers don't think the
|
|
904
976
|
// UPDATE forgot to self-bump updatedAt, and then "helpfully" bump it for us.
|
|
905
977
|
if (alreadyRanHooks.size > 0) {
|
|
906
|
-
maybeBumpUpdatedAt((0, Todo_1.createTodos)([...alreadyRanHooks]), now);
|
|
978
|
+
maybeBumpUpdatedAt(this.#rm, (0, Todo_1.createTodos)([...alreadyRanHooks]), now);
|
|
907
979
|
}
|
|
908
980
|
// Run hooks in a series of loops until things "settle down"
|
|
909
981
|
while (pendingHooks.size > 0) {
|
|
910
982
|
await this.#fl.allowWrites(async () => {
|
|
911
983
|
let todos = (0, Todo_1.createTodos)([...pendingHooks]);
|
|
912
984
|
await (0, defaults_1.setAsyncDefaults)(suppressedDefaultTypeErrors, this.ctx, Todo_1.Todo.groupInsertsByTypeAndSubType(todos));
|
|
913
|
-
maybeBumpUpdatedAt(todos, now);
|
|
985
|
+
maybeBumpUpdatedAt(this.#rm, todos, now);
|
|
914
986
|
// Run our hooks
|
|
915
987
|
for (const group of maybeSetupHookOrdering(todos)) {
|
|
916
988
|
await beforeCreate(this.ctx, group);
|
|
@@ -937,7 +1009,7 @@ class EntityManager {
|
|
|
937
1009
|
hooksInvoked.add(e);
|
|
938
1010
|
pendingHooks.clear();
|
|
939
1011
|
// See if the hooks mutated any new, not-yet-hooksInvoked entities
|
|
940
|
-
findPendingFlushEntities(this
|
|
1012
|
+
findPendingFlushEntities(this.#maybePendingFlushEntities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks);
|
|
941
1013
|
// The final run of recalcPendingReactables could have left us with pending type errors and no entities in
|
|
942
1014
|
// pendingHooks. If so, we need to re-run recalcPendingTypeErrors to get those errors to transition into
|
|
943
1015
|
// suppressed errors so that we will fail after simpleValidation.
|
|
@@ -955,24 +1027,30 @@ class EntityManager {
|
|
|
955
1027
|
return !createThenDelete;
|
|
956
1028
|
});
|
|
957
1029
|
};
|
|
958
|
-
const
|
|
1030
|
+
const { pluginManager } = getEmInternalApi(this);
|
|
1031
|
+
const runValidation = async (entityTodos, joinRowTodos, validate) => {
|
|
1032
|
+
const changedEntities = entitiesFromTodos(entityTodos, joinRowTodos);
|
|
959
1033
|
try {
|
|
960
1034
|
this.#isValidating = true;
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1035
|
+
await pluginManager.beforeValidate(changedEntities);
|
|
1036
|
+
if (validate) {
|
|
1037
|
+
// Run simple rules first b/c it includes not-null/required rules, so that then when we run
|
|
1038
|
+
// `validateReactiveRules` next, the app's lambdas won't see fundamentally invalid entities & NPE.
|
|
1039
|
+
await validateSimpleRules(entityTodos);
|
|
1040
|
+
// After we've let any "author is not set" simple rules fail before prematurely throwing
|
|
1041
|
+
// the "of course that caused an NPE" `TypeError`s, if all the authors *were* valid/set,
|
|
1042
|
+
// and we still have TypeErrors (from derived valeus), they were real, unrelated errors
|
|
1043
|
+
// that the user should see.
|
|
1044
|
+
if (suppressedDefaultTypeErrors.length > 0)
|
|
1045
|
+
throw suppressedDefaultTypeErrors[0];
|
|
1046
|
+
await validateReactiveRules(this, this.#rm.logger, entityTodos, joinRowTodos);
|
|
1047
|
+
await afterValidation(this.ctx, entityTodos);
|
|
1048
|
+
}
|
|
1049
|
+
await pluginManager.afterValidate(changedEntities);
|
|
971
1050
|
}
|
|
972
1051
|
finally {
|
|
973
1052
|
this.#isValidating = false;
|
|
974
1053
|
}
|
|
975
|
-
await afterValidation(this.ctx, entityTodos);
|
|
976
1054
|
};
|
|
977
1055
|
// Run hooks (in iterative loops if hooks mutate new entities) on pending entities
|
|
978
1056
|
let entitiesToFlush = await runHooksOnPendingEntities();
|
|
@@ -982,13 +1060,10 @@ class EntityManager {
|
|
|
982
1060
|
// the full set of entities that will be INSERT/UPDATE/DELETE-d in the database.
|
|
983
1061
|
let entityTodos = (0, Todo_1.createTodos)(entitiesToFlush);
|
|
984
1062
|
let joinRowTodos = (0, Todo_1.combineJoinRows)(this.#joinRows);
|
|
985
|
-
|
|
986
|
-
await runValidation(entityTodos, joinRowTodos);
|
|
987
|
-
}
|
|
1063
|
+
await runValidation(entityTodos, joinRowTodos, !skipValidation);
|
|
988
1064
|
this.#rm.throwIfAnySuppressedTypeErrors();
|
|
989
1065
|
if (suppressedDefaultTypeErrors.length > 0)
|
|
990
1066
|
throw suppressedDefaultTypeErrors[0];
|
|
991
|
-
const { pluginManager } = getEmInternalApi(this);
|
|
992
1067
|
if (Object.keys(entityTodos).length > 0 || Object.keys(joinRowTodos).length > 0) {
|
|
993
1068
|
// The driver will handle the right thing if we're already in an existing transaction.
|
|
994
1069
|
await this.driver.transaction(this, async () => {
|
|
@@ -1009,7 +1084,7 @@ class EntityManager {
|
|
|
1009
1084
|
// Actually do the recalc
|
|
1010
1085
|
await this.#fl.allowWrites(async () => {
|
|
1011
1086
|
await this.#rm.recalcPendingReactables("reactiveQueries");
|
|
1012
|
-
// If any ReactiveFields depended on
|
|
1087
|
+
// If any ReactiveFields depended on AsyncReactiveFields, go ahead and calc those now
|
|
1013
1088
|
await this.#rm.recalcPendingReactables("reactables");
|
|
1014
1089
|
});
|
|
1015
1090
|
// Advance `now` so that our triggers don't think our UPDATEs are forgetting to self-bump
|
|
@@ -1022,7 +1097,7 @@ class EntityManager {
|
|
|
1022
1097
|
// Recreate `entityTodos` against the only-the-just-changed entities
|
|
1023
1098
|
entityTodos = (0, Todo_1.createTodos)(entitiesToFlush);
|
|
1024
1099
|
joinRowTodos = (0, Todo_1.combineJoinRows)(this.#joinRows);
|
|
1025
|
-
await runValidation(entityTodos, joinRowTodos);
|
|
1100
|
+
await runValidation(entityTodos, joinRowTodos, true);
|
|
1026
1101
|
this.#rm.throwIfAnySuppressedTypeErrors();
|
|
1027
1102
|
}
|
|
1028
1103
|
else {
|
|
@@ -1044,6 +1119,11 @@ class EntityManager {
|
|
|
1044
1119
|
if (e.isNewEntity && !e.isDeletedEntity)
|
|
1045
1120
|
this.#entitiesById.set(e.idTagged, e);
|
|
1046
1121
|
(0, BaseEntity_1.getInstanceData)(e).resetAfterFlushed();
|
|
1122
|
+
// Reset AsyncQueryProperties since DB state may have changed
|
|
1123
|
+
for (const rel of Object.values((0, BaseEntity_1.getInstanceData)(e).relations)) {
|
|
1124
|
+
if (rel instanceof AsyncProperty_1.AsyncPropertyImpl)
|
|
1125
|
+
rel.resetAfterFlush();
|
|
1126
|
+
}
|
|
1047
1127
|
}
|
|
1048
1128
|
// Update the joinRows refs to reflect the new state
|
|
1049
1129
|
for (const joinRow of Object.values(joinRowTodos)) {
|
|
@@ -1196,10 +1276,13 @@ class EntityManager {
|
|
|
1196
1276
|
*/
|
|
1197
1277
|
hydrate(type, rows, options) {
|
|
1198
1278
|
const maybeBaseMeta = (0, index_1.getMetadata)(type);
|
|
1279
|
+
const taggedIdPrefix = `${maybeBaseMeta.tagName}:`;
|
|
1280
|
+
const overwriteExisting = options?.overwriteExisting === true;
|
|
1199
1281
|
let i = 0;
|
|
1200
1282
|
const entities = new Array(rows.length);
|
|
1201
1283
|
for (const row of rows) {
|
|
1202
|
-
const
|
|
1284
|
+
const id = row["id"];
|
|
1285
|
+
const taggedId = id === undefined || id === null ? (0, utils_1.fail)("No id column was available") : `${taggedIdPrefix}${id}`;
|
|
1203
1286
|
// See if this is already in our UoW
|
|
1204
1287
|
let entity = this.findExistingInstance(taggedId);
|
|
1205
1288
|
if (!entity) {
|
|
@@ -1208,23 +1291,36 @@ class EntityManager {
|
|
|
1208
1291
|
// Pass id as a hint that we're in hydrate mode
|
|
1209
1292
|
entity = (0, newEntity_1.newEntity)(this, (0, index_1.asConcreteCstr)(meta.cstr), false);
|
|
1210
1293
|
(0, BaseEntity_1.getInstanceData)(entity).row = row;
|
|
1211
|
-
this.#doRegister(entity, taggedId);
|
|
1294
|
+
this.#doRegister(entity, taggedId, meta, true);
|
|
1212
1295
|
}
|
|
1213
|
-
else if (
|
|
1296
|
+
else if (overwriteExisting) {
|
|
1214
1297
|
// Usually if the entity already exists, we don't write over it, but in this case we assume that
|
|
1215
1298
|
// `EntityManager.refresh` is telling us to explicitly load the latest data.
|
|
1216
1299
|
// First swap out the old row with the new row
|
|
1217
|
-
(0, BaseEntity_1.getInstanceData)(entity)
|
|
1300
|
+
const instanceData = (0, BaseEntity_1.getInstanceData)(entity);
|
|
1301
|
+
instanceData.row = row;
|
|
1218
1302
|
// And then only refresh the data keys that have already been serde-d from rows
|
|
1219
1303
|
// (this keeps us from deserializing data out of rows that we don't need).
|
|
1220
|
-
const { data
|
|
1221
|
-
const
|
|
1222
|
-
|
|
1223
|
-
const
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1304
|
+
const { data } = instanceData;
|
|
1305
|
+
const dataKeys = Object.keys(data);
|
|
1306
|
+
if (dataKeys.length > 0) {
|
|
1307
|
+
const allFields = (0, index_1.getMetadata)(entity).allFields;
|
|
1308
|
+
const changedFields = entity.changes.fieldsWithoutRelations;
|
|
1309
|
+
if (changedFields.length === 0) {
|
|
1310
|
+
for (const fieldName of dataKeys) {
|
|
1311
|
+
const serde = allFields[fieldName].serde ?? (0, utils_1.fail)(`Missing serde for ${fieldName}`);
|
|
1312
|
+
serde.setOnEntity(data, row);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
else {
|
|
1316
|
+
for (const fieldName of dataKeys) {
|
|
1317
|
+
const serde = allFields[fieldName].serde ?? (0, utils_1.fail)(`Missing serde for ${fieldName}`);
|
|
1318
|
+
serde.setOnEntity(data, row);
|
|
1319
|
+
// Make the field look not-dirty
|
|
1320
|
+
if (changedFields.includes(fieldName)) {
|
|
1321
|
+
instanceData.markFieldClean(fieldName);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1228
1324
|
}
|
|
1229
1325
|
}
|
|
1230
1326
|
}
|
|
@@ -1233,8 +1329,9 @@ class EntityManager {
|
|
|
1233
1329
|
return entities;
|
|
1234
1330
|
}
|
|
1235
1331
|
touch(entityOrEntities) {
|
|
1236
|
-
for (const entity of (0, utils_1.toArray)(entityOrEntities))
|
|
1237
|
-
(0, BaseEntity_1.getInstanceData)(entity).
|
|
1332
|
+
for (const entity of (0, utils_1.toArray)(entityOrEntities)) {
|
|
1333
|
+
(0, BaseEntity_1.getInstanceData)(entity).markTouched();
|
|
1334
|
+
}
|
|
1238
1335
|
}
|
|
1239
1336
|
async recalc(entityOrEntities) {
|
|
1240
1337
|
// Look for async reactive fields
|
|
@@ -1251,7 +1348,7 @@ class EntityManager {
|
|
|
1251
1348
|
(0, fields_1.setField)(entity, field.fieldName, entity[field.fieldName]);
|
|
1252
1349
|
}));
|
|
1253
1350
|
// `.load()` recalculated the immediate relations, go ahead and recalc any downstream reactables.
|
|
1254
|
-
// We'll still defer
|
|
1351
|
+
// We'll still defer AsyncReactiveFields to the em.flush loop.
|
|
1255
1352
|
await this.#rm.recalcPendingReactables("reactables");
|
|
1256
1353
|
}
|
|
1257
1354
|
beforeBegin(fn) {
|
|
@@ -1311,9 +1408,16 @@ class EntityManager {
|
|
|
1311
1408
|
// Run the beforeDelete hook before we unhook the entity
|
|
1312
1409
|
const todos = (0, Todo_1.createTodos)(entities);
|
|
1313
1410
|
await beforeDelete(this.ctx, todos);
|
|
1314
|
-
// For all relations, unhook the entity from the other side
|
|
1315
|
-
//
|
|
1316
|
-
|
|
1411
|
+
// For all relations, unhook the entity from the other side; this append path is optimized for
|
|
1412
|
+
// large deletes with ~100k relations by avoiding `flatMap` intermediates and `concat` copies.
|
|
1413
|
+
for (const entity of entities) {
|
|
1414
|
+
const relations = (0, index_1.getRelations)(entity);
|
|
1415
|
+
const start = relationsToCleanup.length;
|
|
1416
|
+
relationsToCleanup.length += relations.length;
|
|
1417
|
+
for (let i = 0; i < relations.length; i++) {
|
|
1418
|
+
relationsToCleanup[start + i] = relations[i];
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1317
1421
|
entities = this.#pendingDeletes;
|
|
1318
1422
|
this.#pendingDeletes = [];
|
|
1319
1423
|
}
|
|
@@ -1369,7 +1473,7 @@ class EntityManager {
|
|
|
1369
1473
|
return entities.filter((e) => e instanceof cstr && !e.isDeletedEntity);
|
|
1370
1474
|
}
|
|
1371
1475
|
if (this.#indexManager.shouldIndexType(entities.length)) {
|
|
1372
|
-
this.#indexManager.enableIndexingForType(meta, entities);
|
|
1476
|
+
this.#indexManager.enableIndexingForType(meta, entities, where);
|
|
1373
1477
|
return (this.#indexManager
|
|
1374
1478
|
.findMatching(meta, where)
|
|
1375
1479
|
// Still filter by `instanceof cstr` to handle subtyping
|
|
@@ -1471,7 +1575,8 @@ class EntityManager {
|
|
|
1471
1575
|
continue;
|
|
1472
1576
|
const { originalData: oldOriginalData, data: oldData } = oldInstanceData;
|
|
1473
1577
|
const newEntity = mapEntity(oldEntity);
|
|
1474
|
-
const
|
|
1578
|
+
const newInstanceData = newEntity.__data;
|
|
1579
|
+
const { data: newData } = newInstanceData;
|
|
1475
1580
|
// for new entities, anything in `data` is changed and should be copied across. for existing entities, we
|
|
1476
1581
|
// only care about changed fields, which are enumerated by originalData
|
|
1477
1582
|
const maybeEntity = (value) => ((0, Entity_1.isEntity)(value) ? mapEntity(value) : value);
|
|
@@ -1479,7 +1584,7 @@ class EntityManager {
|
|
|
1479
1584
|
for (const field of fields) {
|
|
1480
1585
|
// copy over originalData so .changes is consistent across ems
|
|
1481
1586
|
if (field in oldOriginalData)
|
|
1482
|
-
|
|
1587
|
+
newInstanceData.markFieldDirty(field, maybeEntity(oldOriginalData[field]));
|
|
1483
1588
|
newData[field] = maybeEntity(oldData[field]);
|
|
1484
1589
|
}
|
|
1485
1590
|
}
|
|
@@ -1489,7 +1594,7 @@ class EntityManager {
|
|
|
1489
1594
|
const newEntity = mapEntity(oldEntity);
|
|
1490
1595
|
if (oldEntity.isDeletedEntity) {
|
|
1491
1596
|
// If the old entity was deleted, that should be persisted in the new em
|
|
1492
|
-
newEntity.__data.markDeleted(
|
|
1597
|
+
newEntity.__data.markDeleted();
|
|
1493
1598
|
// deleted entities will fail if you try to `get` their relations, so skip them since they should be cleared
|
|
1494
1599
|
// out regardless
|
|
1495
1600
|
continue;
|
|
@@ -1634,11 +1739,11 @@ class EntityManager {
|
|
|
1634
1739
|
}
|
|
1635
1740
|
}
|
|
1636
1741
|
/** Registers a newly-instantiated entity with our EntityManager; only called by #doCreate and hydrate. */
|
|
1637
|
-
#doRegister(entity, id) {
|
|
1742
|
+
#doRegister(entity, id, meta, skipDuplicateCheck = false) {
|
|
1638
1743
|
// Keep our indexes up to date...
|
|
1639
1744
|
const maybeId = id ?? entity.idTaggedMaybe;
|
|
1640
1745
|
if (maybeId) {
|
|
1641
|
-
if (this.findExistingInstance(maybeId) !== undefined) {
|
|
1746
|
+
if (!skipDuplicateCheck && this.findExistingInstance(maybeId) !== undefined) {
|
|
1642
1747
|
throw new Error(`Entity ${entity} has a duplicate instance already loaded`);
|
|
1643
1748
|
}
|
|
1644
1749
|
this.#entitiesById.set(maybeId, entity);
|
|
@@ -1648,7 +1753,7 @@ class EntityManager {
|
|
|
1648
1753
|
this.#entitiesById.set(entity.toTaggedString(), entity);
|
|
1649
1754
|
}
|
|
1650
1755
|
this.#entitiesArray.push(entity);
|
|
1651
|
-
|
|
1756
|
+
meta ??= (0, index_1.getMetadata)(entity);
|
|
1652
1757
|
const set = this.#entitiesByTag.get(meta.tagName) ?? [];
|
|
1653
1758
|
if (set.length === 0)
|
|
1654
1759
|
this.#entitiesByTag.set(meta.tagName, set);
|
|
@@ -1747,14 +1852,14 @@ async function validateReactiveRules(em, logger, todos, joinRowTodos) {
|
|
|
1747
1852
|
const p1 = Object.values(todos).flatMap((todo) => {
|
|
1748
1853
|
const entities = [...todo.inserts, ...todo.updates, ...todo.deletes];
|
|
1749
1854
|
// Find each statically-declared reactive rule for the given entity type
|
|
1750
|
-
const rules =
|
|
1855
|
+
const rules = todo.metadata.reactiveRules;
|
|
1751
1856
|
return rules.map((rule) => {
|
|
1752
1857
|
// Of all changed entities of this type, how many specifically trigger this rule?
|
|
1753
1858
|
const triggered = entities.filter((e) => {
|
|
1754
1859
|
// If the rule is for a different subtype, skip it
|
|
1755
1860
|
if (!(e instanceof rule.source))
|
|
1756
1861
|
return false;
|
|
1757
|
-
// Any new-or-deleted entity fires every rule (
|
|
1862
|
+
// Any new-or-deleted entity fires every rule (reactiveRules has already filtered out read-only)
|
|
1758
1863
|
if (e.isNewEntity || e.isDeletedEntity)
|
|
1759
1864
|
return true;
|
|
1760
1865
|
// Otherwise see if the changed fields overlaps with the rule's fields
|
|
@@ -1772,15 +1877,15 @@ async function validateReactiveRules(em, logger, todos, joinRowTodos) {
|
|
|
1772
1877
|
const p2 = Object.values(joinRowTodos).flatMap((todo) => {
|
|
1773
1878
|
const entities = [...todo.newRows, ...todo.deletedRows].flatMap((jr) => Object.values(jr.columns));
|
|
1774
1879
|
// Do the first side
|
|
1775
|
-
const p1 =
|
|
1776
|
-
.filter((rule) => rule.fields.includes(todo.m2m.fieldName))
|
|
1880
|
+
const p1 = todo.m2m.meta
|
|
1881
|
+
.reactiveRules.filter((rule) => rule.fields.includes(todo.m2m.fieldName))
|
|
1777
1882
|
.map((rule) => {
|
|
1778
1883
|
const triggered = entities.filter((e) => e instanceof todo.m2m.meta.cstr);
|
|
1779
1884
|
return followAndQueue(triggered, rule);
|
|
1780
1885
|
});
|
|
1781
1886
|
// And the second side
|
|
1782
|
-
const p2 =
|
|
1783
|
-
.filter((rule) => rule.fields.includes(todo.m2m.otherFieldName))
|
|
1887
|
+
const p2 = todo.m2m.otherMeta
|
|
1888
|
+
.reactiveRules.filter((rule) => rule.fields.includes(todo.m2m.otherFieldName))
|
|
1784
1889
|
.map((rule) => {
|
|
1785
1890
|
const triggered = entities.filter((e) => e instanceof todo.m2m.otherMeta.cstr);
|
|
1786
1891
|
return followAndQueue(triggered, rule);
|
|
@@ -1859,6 +1964,19 @@ function beforeUpdate(ctx, todos) {
|
|
|
1859
1964
|
function afterValidation(ctx, todos) {
|
|
1860
1965
|
return runHookOnTodos(ctx, "afterValidation", todos, ["inserts", "updates"]);
|
|
1861
1966
|
}
|
|
1967
|
+
/** Collects changed entities from flush todos, i.e. m2m endpoint entities. */
|
|
1968
|
+
function entitiesFromTodos(entityTodos, joinRowTodos) {
|
|
1969
|
+
const entities = new Set();
|
|
1970
|
+
for (const todo of Object.values(entityTodos)) {
|
|
1971
|
+
[...todo.inserts, ...todo.updates, ...todo.deletes].forEach((entity) => entities.add(entity));
|
|
1972
|
+
}
|
|
1973
|
+
for (const todo of Object.values(joinRowTodos)) {
|
|
1974
|
+
[...todo.newRows, ...todo.deletedRows].forEach((row) => {
|
|
1975
|
+
Object.values(row.columns).forEach((entity) => entities.add(entity));
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
return [...entities];
|
|
1979
|
+
}
|
|
1862
1980
|
function beforeCommit(ctx, entities) {
|
|
1863
1981
|
return runHook(ctx, "beforeCommit", [...entities]);
|
|
1864
1982
|
}
|
|
@@ -1986,7 +2104,7 @@ function getCascadeDeleteRelations(entity) {
|
|
|
1986
2104
|
function isCustomRelation(r) {
|
|
1987
2105
|
return r instanceof index_1.CustomCollection || r instanceof index_1.CustomReference || r instanceof relations_1.ReactiveReferenceImpl;
|
|
1988
2106
|
}
|
|
1989
|
-
function maybeBumpUpdatedAt(todos, now) {
|
|
2107
|
+
function maybeBumpUpdatedAt(rm, todos, now) {
|
|
1990
2108
|
for (const todo of Object.values(todos)) {
|
|
1991
2109
|
const { updatedAt } = todo.metadata.timestampFields ?? {};
|
|
1992
2110
|
if (updatedAt) {
|
|
@@ -1996,9 +2114,10 @@ function maybeBumpUpdatedAt(todos, now) {
|
|
|
1996
2114
|
// it has changed. This is technically true, but this will break the oplock SQL generation,
|
|
1997
2115
|
// so force the field to be dirty.
|
|
1998
2116
|
const orm = (0, BaseEntity_1.getInstanceData)(e);
|
|
1999
|
-
orm.
|
|
2117
|
+
orm.markFieldDirty(updatedAt, (0, fields_1.getField)(e, updatedAt));
|
|
2000
2118
|
const serde = todo.metadata.fields[updatedAt].serde;
|
|
2001
2119
|
orm.data[updatedAt] = serde.mapFromNow(now);
|
|
2120
|
+
rm.queueDownstreamReactables(e, updatedAt);
|
|
2002
2121
|
}
|
|
2003
2122
|
}
|
|
2004
2123
|
}
|
|
@@ -2032,17 +2151,13 @@ function findConcreteMeta(maybeBaseMeta, row) {
|
|
|
2032
2151
|
throw new Error(`${maybeBaseMeta.type} ${(0, index_1.tagId)(maybeBaseMeta, row.id)} must be instantiated via a subtype`);
|
|
2033
2152
|
}
|
|
2034
2153
|
// Look for the CTI __class from the driver telling us which subtype to instantiate
|
|
2035
|
-
return maybeBaseMeta.
|
|
2154
|
+
return maybeBaseMeta.subTypesByType.get(row.__class) ?? maybeBaseMeta;
|
|
2036
2155
|
}
|
|
2037
2156
|
else if (maybeBaseMeta.inheritanceType === "sti") {
|
|
2038
2157
|
// Look for the STI discriminator value
|
|
2039
2158
|
const baseMeta = (0, index_1.getBaseMeta)(maybeBaseMeta);
|
|
2040
|
-
const
|
|
2041
|
-
|
|
2042
|
-
throw new Error("Discriminator field must be an enum");
|
|
2043
|
-
const columnName = field.serde.columns[0].columnName;
|
|
2044
|
-
const value = row[columnName];
|
|
2045
|
-
return baseMeta.subTypes.find((st) => st.stiDiscriminatorValue === value) ?? baseMeta;
|
|
2159
|
+
const value = row[baseMeta.stiDiscriminatorColumnName];
|
|
2160
|
+
return baseMeta.subTypesByStiValue.get(value) ?? baseMeta;
|
|
2046
2161
|
}
|
|
2047
2162
|
else {
|
|
2048
2163
|
throw new Error("Unknown inheritance type");
|
|
@@ -2051,7 +2166,7 @@ function findConcreteMeta(maybeBaseMeta, row) {
|
|
|
2051
2166
|
/** Sets the `Animal.type` enum to the right subtype value. */
|
|
2052
2167
|
function setStiDiscriminatorValue(baseMeta, entity) {
|
|
2053
2168
|
const typeName = entity.constructor.name;
|
|
2054
|
-
const st = baseMeta.
|
|
2169
|
+
const st = baseMeta.subTypesByType.get(typeName);
|
|
2055
2170
|
if (st) {
|
|
2056
2171
|
const field = baseMeta.fields[baseMeta.stiDiscriminatorField];
|
|
2057
2172
|
const code = field.enumDetailType.findById(st.stiDiscriminatorValue).code;
|
|
@@ -2061,19 +2176,25 @@ function setStiDiscriminatorValue(baseMeta, entity) {
|
|
|
2061
2176
|
entity[baseMeta.stiDiscriminatorField] = undefined;
|
|
2062
2177
|
}
|
|
2063
2178
|
}
|
|
2064
|
-
function findPendingFlushEntities(
|
|
2065
|
-
for (const e of
|
|
2066
|
-
if ((0, BaseEntity_1.getInstanceData)(e).pendingOperation
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
}
|
|
2070
|
-
else {
|
|
2071
|
-
alreadyRanHooks.add(e);
|
|
2072
|
-
}
|
|
2073
|
-
pendingFlush.add(e);
|
|
2179
|
+
function findPendingFlushEntities(maybePendingFlushEntities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks) {
|
|
2180
|
+
for (const e of maybePendingFlushEntities) {
|
|
2181
|
+
if ((0, BaseEntity_1.getInstanceData)(e).pendingOperation === "none") {
|
|
2182
|
+
maybePendingFlushEntities.delete(e);
|
|
2183
|
+
continue;
|
|
2074
2184
|
}
|
|
2185
|
+
if (!hooksInvoked.has(e)) {
|
|
2186
|
+
pendingHooks.add(e);
|
|
2187
|
+
}
|
|
2188
|
+
else {
|
|
2189
|
+
alreadyRanHooks.add(e);
|
|
2190
|
+
}
|
|
2191
|
+
pendingFlush.add(e);
|
|
2075
2192
|
}
|
|
2076
2193
|
}
|
|
2194
|
+
/** Returns true if the caller explicitly asked `find` to use SQL pagination. */
|
|
2195
|
+
function hasPaginationSettings(options) {
|
|
2196
|
+
return "limit" in options || "offset" in options;
|
|
2197
|
+
}
|
|
2077
2198
|
/** An error we throw to get knex to `ROLLBACK`, but then catch. */
|
|
2078
2199
|
class InMemoryRollbackError extends Error {
|
|
2079
2200
|
}
|