@expo/entity 0.19.0 → 0.23.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/EntityCacheAdapter.d.ts +2 -9
- package/build/EntityCacheAdapter.js.map +1 -1
- package/build/EntityMutationInfo.d.ts +26 -0
- package/build/EntityMutationInfo.js +10 -0
- package/build/EntityMutationInfo.js.map +1 -0
- package/build/EntityMutationTriggerConfiguration.d.ts +3 -3
- package/build/EntityMutationValidator.d.ts +2 -2
- package/build/EntityMutationValidator.js.map +1 -1
- package/build/EntityMutator.d.ts +4 -15
- package/build/EntityMutator.js +45 -41
- package/build/EntityMutator.js.map +1 -1
- package/build/EntityQueryContext.d.ts +24 -0
- package/build/EntityQueryContext.js +43 -0
- package/build/EntityQueryContext.js.map +1 -1
- package/build/EntityQueryContextProvider.js +1 -0
- package/build/EntityQueryContextProvider.js.map +1 -1
- package/build/__tests__/EntityEdges-test.js +99 -3
- package/build/__tests__/EntityEdges-test.js.map +1 -1
- package/build/__tests__/EntityMutator-MutationCacheConsistency-test.d.ts +1 -0
- package/build/__tests__/EntityMutator-MutationCacheConsistency-test.js +81 -0
- package/build/__tests__/EntityMutator-MutationCacheConsistency-test.js.map +1 -0
- package/build/__tests__/EntityMutator-test.js +9 -7
- package/build/__tests__/EntityMutator-test.js.map +1 -1
- package/build/__tests__/EntityQueryContext-test.d.ts +1 -0
- package/build/__tests__/EntityQueryContext-test.js +56 -0
- package/build/__tests__/EntityQueryContext-test.js.map +1 -0
- package/build/index.d.ts +1 -0
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/internal/ReadThroughEntityCache.d.ts +2 -3
- package/build/internal/ReadThroughEntityCache.js +2 -6
- package/build/internal/ReadThroughEntityCache.js.map +1 -1
- package/build/internal/__tests__/ReadThroughEntityCache-test.js +0 -32
- package/build/internal/__tests__/ReadThroughEntityCache-test.js.map +1 -1
- package/build/utils/testing/StubCacheAdapter.d.ts +6 -9
- package/build/utils/testing/StubCacheAdapter.js +0 -6
- package/build/utils/testing/StubCacheAdapter.js.map +1 -1
- package/package.json +1 -1
- package/src/EntityCacheAdapter.ts +2 -10
- package/src/EntityMutationInfo.ts +47 -0
- package/src/EntityMutationTriggerConfiguration.ts +3 -3
- package/src/EntityMutationValidator.ts +8 -2
- package/src/EntityMutator.ts +81 -68
- package/src/EntityQueryContext.ts +54 -0
- package/src/EntityQueryContextProvider.ts +1 -0
- package/src/__tests__/EntityEdges-test.ts +163 -9
- package/src/__tests__/EntityMutator-MutationCacheConsistency-test.ts +105 -0
- package/src/__tests__/EntityMutator-test.ts +24 -6
- package/src/__tests__/EntityQueryContext-test.ts +82 -0
- package/src/index.ts +1 -0
- package/src/internal/ReadThroughEntityCache.ts +6 -28
- package/src/internal/__tests__/ReadThroughEntityCache-test.ts +0 -44
- package/src/utils/testing/StubCacheAdapter.ts +11 -17
package/src/EntityMutator.ts
CHANGED
|
@@ -7,6 +7,12 @@ import EntityConfiguration from './EntityConfiguration';
|
|
|
7
7
|
import EntityDatabaseAdapter from './EntityDatabaseAdapter';
|
|
8
8
|
import { EntityEdgeDeletionBehavior } from './EntityFieldDefinition';
|
|
9
9
|
import EntityLoaderFactory from './EntityLoaderFactory';
|
|
10
|
+
import {
|
|
11
|
+
EntityValidatorMutationInfo,
|
|
12
|
+
EntityMutationType,
|
|
13
|
+
EntityTriggerMutationInfo,
|
|
14
|
+
EntityMutationTriggerDeleteCascadeInfo,
|
|
15
|
+
} from './EntityMutationInfo';
|
|
10
16
|
import EntityMutationTriggerConfiguration, {
|
|
11
17
|
EntityMutationTrigger,
|
|
12
18
|
EntityNonTransactionalMutationTrigger,
|
|
@@ -21,30 +27,6 @@ import { timeAndLogMutationEventAsync } from './metrics/EntityMetricsUtils';
|
|
|
21
27
|
import IEntityMetricsAdapter, { EntityMetricsMutationType } from './metrics/IEntityMetricsAdapter';
|
|
22
28
|
import { mapMapAsync } from './utils/collections/maps';
|
|
23
29
|
|
|
24
|
-
export enum EntityMutationType {
|
|
25
|
-
CREATE,
|
|
26
|
-
UPDATE,
|
|
27
|
-
DELETE,
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export type EntityMutationInfo<
|
|
31
|
-
TFields,
|
|
32
|
-
TID extends NonNullable<TFields[TSelectedFields]>,
|
|
33
|
-
TViewerContext extends ViewerContext,
|
|
34
|
-
TEntity extends Entity<TFields, TID, TViewerContext, TSelectedFields>,
|
|
35
|
-
TSelectedFields extends keyof TFields = keyof TFields
|
|
36
|
-
> =
|
|
37
|
-
| {
|
|
38
|
-
type: EntityMutationType.CREATE;
|
|
39
|
-
}
|
|
40
|
-
| {
|
|
41
|
-
type: EntityMutationType.UPDATE;
|
|
42
|
-
previousValue: TEntity;
|
|
43
|
-
}
|
|
44
|
-
| {
|
|
45
|
-
type: EntityMutationType.DELETE;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
30
|
abstract class BaseMutator<
|
|
49
31
|
TFields,
|
|
50
32
|
TID extends NonNullable<TFields[TSelectedFields]>,
|
|
@@ -110,26 +92,44 @@ abstract class BaseMutator<
|
|
|
110
92
|
}
|
|
111
93
|
}
|
|
112
94
|
|
|
113
|
-
protected async
|
|
114
|
-
|
|
95
|
+
protected async executeMutationValidatorsAsync(
|
|
96
|
+
validators: EntityMutationValidator<TFields, TID, TViewerContext, TEntity, TSelectedFields>[],
|
|
97
|
+
queryContext: EntityTransactionalQueryContext,
|
|
98
|
+
entity: TEntity,
|
|
99
|
+
mutationInfo: EntityValidatorMutationInfo<
|
|
100
|
+
TFields,
|
|
101
|
+
TID,
|
|
102
|
+
TViewerContext,
|
|
103
|
+
TEntity,
|
|
104
|
+
TSelectedFields
|
|
105
|
+
>
|
|
106
|
+
): Promise<void> {
|
|
107
|
+
await Promise.all(
|
|
108
|
+
validators.map((validator) =>
|
|
109
|
+
validator.executeAsync(this.viewerContext, queryContext, entity, mutationInfo)
|
|
110
|
+
)
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
protected async executeMutationTriggersAsync(
|
|
115
|
+
triggers:
|
|
115
116
|
| EntityMutationTrigger<TFields, TID, TViewerContext, TEntity, TSelectedFields>[]
|
|
116
|
-
| EntityMutationValidator<TFields, TID, TViewerContext, TEntity, TSelectedFields>[]
|
|
117
117
|
| undefined,
|
|
118
118
|
queryContext: EntityTransactionalQueryContext,
|
|
119
119
|
entity: TEntity,
|
|
120
|
-
mutationInfo:
|
|
120
|
+
mutationInfo: EntityTriggerMutationInfo<TFields, TID, TViewerContext, TEntity, TSelectedFields>
|
|
121
121
|
): Promise<void> {
|
|
122
|
-
if (!
|
|
122
|
+
if (!triggers) {
|
|
123
123
|
return;
|
|
124
124
|
}
|
|
125
125
|
await Promise.all(
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
triggers.map((trigger) =>
|
|
127
|
+
trigger.executeAsync(this.viewerContext, queryContext, entity, mutationInfo)
|
|
128
128
|
)
|
|
129
129
|
);
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
protected async
|
|
132
|
+
protected async executeNonTransactionalMutationTriggersAsync(
|
|
133
133
|
triggers:
|
|
134
134
|
| EntityNonTransactionalMutationTrigger<
|
|
135
135
|
TFields,
|
|
@@ -140,7 +140,7 @@ abstract class BaseMutator<
|
|
|
140
140
|
>[]
|
|
141
141
|
| undefined,
|
|
142
142
|
entity: TEntity,
|
|
143
|
-
mutationInfo:
|
|
143
|
+
mutationInfo: EntityTriggerMutationInfo<TFields, TID, TViewerContext, TEntity, TSelectedFields>
|
|
144
144
|
): Promise<void> {
|
|
145
145
|
if (!triggers) {
|
|
146
146
|
return;
|
|
@@ -228,19 +228,19 @@ export class CreateMutator<
|
|
|
228
228
|
return authorizeCreateResult;
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
-
await this.
|
|
231
|
+
await this.executeMutationValidatorsAsync(
|
|
232
232
|
this.mutationValidators,
|
|
233
233
|
queryContext,
|
|
234
234
|
temporaryEntityForPrivacyCheck,
|
|
235
235
|
{ type: EntityMutationType.CREATE }
|
|
236
236
|
);
|
|
237
|
-
await this.
|
|
237
|
+
await this.executeMutationTriggersAsync(
|
|
238
238
|
this.mutationTriggers.beforeAll,
|
|
239
239
|
queryContext,
|
|
240
240
|
temporaryEntityForPrivacyCheck,
|
|
241
241
|
{ type: EntityMutationType.CREATE }
|
|
242
242
|
);
|
|
243
|
-
await this.
|
|
243
|
+
await this.executeMutationTriggersAsync(
|
|
244
244
|
this.mutationTriggers.beforeCreate,
|
|
245
245
|
queryContext,
|
|
246
246
|
temporaryEntityForPrivacyCheck,
|
|
@@ -250,7 +250,7 @@ export class CreateMutator<
|
|
|
250
250
|
const insertResult = await this.databaseAdapter.insertAsync(queryContext, this.fieldsForEntity);
|
|
251
251
|
|
|
252
252
|
const entityLoader = this.entityLoaderFactory.forLoad(this.viewerContext, queryContext);
|
|
253
|
-
queryContext.
|
|
253
|
+
queryContext.appendPostCommitInvalidationCallback(
|
|
254
254
|
entityLoader.invalidateFieldsAsync.bind(entityLoader, insertResult)
|
|
255
255
|
);
|
|
256
256
|
|
|
@@ -259,13 +259,13 @@ export class CreateMutator<
|
|
|
259
259
|
.enforcing()
|
|
260
260
|
.loadByIDAsync(unauthorizedEntityAfterInsert.getID());
|
|
261
261
|
|
|
262
|
-
await this.
|
|
262
|
+
await this.executeMutationTriggersAsync(
|
|
263
263
|
this.mutationTriggers.afterCreate,
|
|
264
264
|
queryContext,
|
|
265
265
|
newEntity,
|
|
266
266
|
{ type: EntityMutationType.CREATE }
|
|
267
267
|
);
|
|
268
|
-
await this.
|
|
268
|
+
await this.executeMutationTriggersAsync(
|
|
269
269
|
this.mutationTriggers.afterAll,
|
|
270
270
|
queryContext,
|
|
271
271
|
newEntity,
|
|
@@ -273,7 +273,7 @@ export class CreateMutator<
|
|
|
273
273
|
);
|
|
274
274
|
|
|
275
275
|
queryContext.appendPostCommitCallback(
|
|
276
|
-
this.
|
|
276
|
+
this.executeNonTransactionalMutationTriggersAsync.bind(
|
|
277
277
|
this,
|
|
278
278
|
this.mutationTriggers.afterCommit,
|
|
279
279
|
newEntity,
|
|
@@ -417,19 +417,19 @@ export class UpdateMutator<
|
|
|
417
417
|
return authorizeUpdateResult;
|
|
418
418
|
}
|
|
419
419
|
|
|
420
|
-
await this.
|
|
420
|
+
await this.executeMutationValidatorsAsync(
|
|
421
421
|
this.mutationValidators,
|
|
422
422
|
queryContext,
|
|
423
423
|
entityAboutToBeUpdated,
|
|
424
424
|
{ type: EntityMutationType.UPDATE, previousValue: this.originalEntity }
|
|
425
425
|
);
|
|
426
|
-
await this.
|
|
426
|
+
await this.executeMutationTriggersAsync(
|
|
427
427
|
this.mutationTriggers.beforeAll,
|
|
428
428
|
queryContext,
|
|
429
429
|
entityAboutToBeUpdated,
|
|
430
430
|
{ type: EntityMutationType.UPDATE, previousValue: this.originalEntity }
|
|
431
431
|
);
|
|
432
|
-
await this.
|
|
432
|
+
await this.executeMutationTriggersAsync(
|
|
433
433
|
this.mutationTriggers.beforeUpdate,
|
|
434
434
|
queryContext,
|
|
435
435
|
entityAboutToBeUpdated,
|
|
@@ -445,13 +445,13 @@ export class UpdateMutator<
|
|
|
445
445
|
|
|
446
446
|
const entityLoader = this.entityLoaderFactory.forLoad(this.viewerContext, queryContext);
|
|
447
447
|
|
|
448
|
-
queryContext.
|
|
448
|
+
queryContext.appendPostCommitInvalidationCallback(
|
|
449
449
|
entityLoader.invalidateFieldsAsync.bind(
|
|
450
450
|
entityLoader,
|
|
451
451
|
this.originalEntity.getAllDatabaseFields()
|
|
452
452
|
)
|
|
453
453
|
);
|
|
454
|
-
queryContext.
|
|
454
|
+
queryContext.appendPostCommitInvalidationCallback(
|
|
455
455
|
entityLoader.invalidateFieldsAsync.bind(entityLoader, this.fieldsForEntity)
|
|
456
456
|
);
|
|
457
457
|
|
|
@@ -460,13 +460,13 @@ export class UpdateMutator<
|
|
|
460
460
|
.enforcing()
|
|
461
461
|
.loadByIDAsync(unauthorizedEntityAfterUpdate.getID());
|
|
462
462
|
|
|
463
|
-
await this.
|
|
463
|
+
await this.executeMutationTriggersAsync(
|
|
464
464
|
this.mutationTriggers.afterUpdate,
|
|
465
465
|
queryContext,
|
|
466
466
|
updatedEntity,
|
|
467
467
|
{ type: EntityMutationType.UPDATE, previousValue: this.originalEntity }
|
|
468
468
|
);
|
|
469
|
-
await this.
|
|
469
|
+
await this.executeMutationTriggersAsync(
|
|
470
470
|
this.mutationTriggers.afterAll,
|
|
471
471
|
queryContext,
|
|
472
472
|
updatedEntity,
|
|
@@ -474,7 +474,7 @@ export class UpdateMutator<
|
|
|
474
474
|
);
|
|
475
475
|
|
|
476
476
|
queryContext.appendPostCommitCallback(
|
|
477
|
-
this.
|
|
477
|
+
this.executeNonTransactionalMutationTriggersAsync.bind(
|
|
478
478
|
this,
|
|
479
479
|
this.mutationTriggers.afterCommit,
|
|
480
480
|
updatedEntity,
|
|
@@ -565,7 +565,7 @@ export class DeleteMutator<
|
|
|
565
565
|
this.metricsAdapter,
|
|
566
566
|
EntityMetricsMutationType.DELETE,
|
|
567
567
|
this.entityClass.name
|
|
568
|
-
)(this.deleteInTransactionAsync());
|
|
568
|
+
)(this.deleteInTransactionAsync(new Set(), false, null));
|
|
569
569
|
}
|
|
570
570
|
|
|
571
571
|
/**
|
|
@@ -576,14 +576,16 @@ export class DeleteMutator<
|
|
|
576
576
|
}
|
|
577
577
|
|
|
578
578
|
private async deleteInTransactionAsync(
|
|
579
|
-
processedEntityIdentifiersFromTransitiveDeletions: Set<string
|
|
580
|
-
skipDatabaseDeletion: boolean
|
|
579
|
+
processedEntityIdentifiersFromTransitiveDeletions: Set<string>,
|
|
580
|
+
skipDatabaseDeletion: boolean,
|
|
581
|
+
cascadingDeleteCause: EntityMutationTriggerDeleteCascadeInfo | null
|
|
581
582
|
): Promise<Result<void>> {
|
|
582
583
|
return await this.queryContext.runInTransactionIfNotInTransactionAsync((innerQueryContext) =>
|
|
583
584
|
this.deleteInternalAsync(
|
|
584
585
|
innerQueryContext,
|
|
585
586
|
processedEntityIdentifiersFromTransitiveDeletions,
|
|
586
|
-
skipDatabaseDeletion
|
|
587
|
+
skipDatabaseDeletion,
|
|
588
|
+
cascadingDeleteCause
|
|
587
589
|
)
|
|
588
590
|
);
|
|
589
591
|
}
|
|
@@ -591,7 +593,8 @@ export class DeleteMutator<
|
|
|
591
593
|
private async deleteInternalAsync(
|
|
592
594
|
queryContext: EntityTransactionalQueryContext,
|
|
593
595
|
processedEntityIdentifiersFromTransitiveDeletions: Set<string>,
|
|
594
|
-
skipDatabaseDeletion: boolean
|
|
596
|
+
skipDatabaseDeletion: boolean,
|
|
597
|
+
cascadingDeleteCause: EntityMutationTriggerDeleteCascadeInfo | null
|
|
595
598
|
): Promise<Result<void>> {
|
|
596
599
|
const authorizeDeleteResult = await asyncResult(
|
|
597
600
|
this.privacyPolicy.authorizeDeleteAsync(
|
|
@@ -608,20 +611,21 @@ export class DeleteMutator<
|
|
|
608
611
|
await this.processEntityDeletionForInboundEdgesAsync(
|
|
609
612
|
this.entity,
|
|
610
613
|
queryContext,
|
|
611
|
-
processedEntityIdentifiersFromTransitiveDeletions
|
|
614
|
+
processedEntityIdentifiersFromTransitiveDeletions,
|
|
615
|
+
cascadingDeleteCause
|
|
612
616
|
);
|
|
613
617
|
|
|
614
|
-
await this.
|
|
618
|
+
await this.executeMutationTriggersAsync(
|
|
615
619
|
this.mutationTriggers.beforeAll,
|
|
616
620
|
queryContext,
|
|
617
621
|
this.entity,
|
|
618
|
-
{ type: EntityMutationType.DELETE }
|
|
622
|
+
{ type: EntityMutationType.DELETE, cascadingDeleteCause }
|
|
619
623
|
);
|
|
620
|
-
await this.
|
|
624
|
+
await this.executeMutationTriggersAsync(
|
|
621
625
|
this.mutationTriggers.beforeDelete,
|
|
622
626
|
queryContext,
|
|
623
627
|
this.entity,
|
|
624
|
-
{ type: EntityMutationType.DELETE }
|
|
628
|
+
{ type: EntityMutationType.DELETE, cascadingDeleteCause }
|
|
625
629
|
);
|
|
626
630
|
|
|
627
631
|
if (!skipDatabaseDeletion) {
|
|
@@ -633,29 +637,29 @@ export class DeleteMutator<
|
|
|
633
637
|
}
|
|
634
638
|
|
|
635
639
|
const entityLoader = this.entityLoaderFactory.forLoad(this.viewerContext, queryContext);
|
|
636
|
-
queryContext.
|
|
640
|
+
queryContext.appendPostCommitInvalidationCallback(
|
|
637
641
|
entityLoader.invalidateFieldsAsync.bind(entityLoader, this.entity.getAllDatabaseFields())
|
|
638
642
|
);
|
|
639
643
|
|
|
640
|
-
await this.
|
|
644
|
+
await this.executeMutationTriggersAsync(
|
|
641
645
|
this.mutationTriggers.afterDelete,
|
|
642
646
|
queryContext,
|
|
643
647
|
this.entity,
|
|
644
|
-
{ type: EntityMutationType.DELETE }
|
|
648
|
+
{ type: EntityMutationType.DELETE, cascadingDeleteCause }
|
|
645
649
|
);
|
|
646
|
-
await this.
|
|
650
|
+
await this.executeMutationTriggersAsync(
|
|
647
651
|
this.mutationTriggers.afterAll,
|
|
648
652
|
queryContext,
|
|
649
653
|
this.entity,
|
|
650
|
-
{ type: EntityMutationType.DELETE }
|
|
654
|
+
{ type: EntityMutationType.DELETE, cascadingDeleteCause }
|
|
651
655
|
);
|
|
652
656
|
|
|
653
657
|
queryContext.appendPostCommitCallback(
|
|
654
|
-
this.
|
|
658
|
+
this.executeNonTransactionalMutationTriggersAsync.bind(
|
|
655
659
|
this,
|
|
656
660
|
this.mutationTriggers.afterCommit,
|
|
657
661
|
this.entity,
|
|
658
|
-
{ type: EntityMutationType.DELETE }
|
|
662
|
+
{ type: EntityMutationType.DELETE, cascadingDeleteCause }
|
|
659
663
|
)
|
|
660
664
|
);
|
|
661
665
|
|
|
@@ -679,7 +683,8 @@ export class DeleteMutator<
|
|
|
679
683
|
private async processEntityDeletionForInboundEdgesAsync(
|
|
680
684
|
entity: TEntity,
|
|
681
685
|
queryContext: EntityTransactionalQueryContext,
|
|
682
|
-
processedEntityIdentifiers: Set<string
|
|
686
|
+
processedEntityIdentifiers: Set<string>,
|
|
687
|
+
cascadingDeleteCause: EntityMutationTriggerDeleteCascadeInfo | null
|
|
683
688
|
): Promise<void> {
|
|
684
689
|
// prevent infinite reference cycles by keeping track of entities already processed
|
|
685
690
|
if (processedEntityIdentifiers.has(entity.getUniqueIdentifier())) {
|
|
@@ -752,7 +757,11 @@ export class DeleteMutator<
|
|
|
752
757
|
.forDelete(inboundReferenceEntity, queryContext)
|
|
753
758
|
.deleteInTransactionAsync(
|
|
754
759
|
processedEntityIdentifiers,
|
|
755
|
-
/* skipDatabaseDeletion */ true // deletion is handled by DB
|
|
760
|
+
/* skipDatabaseDeletion */ true, // deletion is handled by DB
|
|
761
|
+
{
|
|
762
|
+
entity,
|
|
763
|
+
cascadingDeleteCause,
|
|
764
|
+
}
|
|
756
765
|
)
|
|
757
766
|
)
|
|
758
767
|
);
|
|
@@ -776,7 +785,11 @@ export class DeleteMutator<
|
|
|
776
785
|
.forDelete(inboundReferenceEntity, queryContext)
|
|
777
786
|
.deleteInTransactionAsync(
|
|
778
787
|
processedEntityIdentifiers,
|
|
779
|
-
/* skipDatabaseDeletion */ false
|
|
788
|
+
/* skipDatabaseDeletion */ false,
|
|
789
|
+
{
|
|
790
|
+
entity,
|
|
791
|
+
cascadingDeleteCause,
|
|
792
|
+
}
|
|
780
793
|
)
|
|
781
794
|
)
|
|
782
795
|
);
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
import assert from 'assert';
|
|
2
|
+
|
|
1
3
|
import EntityQueryContextProvider from './EntityQueryContextProvider';
|
|
2
4
|
|
|
3
5
|
export type PostCommitCallback = (...args: any) => Promise<any>;
|
|
6
|
+
export type PreCommitCallback = (
|
|
7
|
+
queryContext: EntityTransactionalQueryContext,
|
|
8
|
+
...args: any
|
|
9
|
+
) => Promise<any>;
|
|
4
10
|
|
|
5
11
|
/**
|
|
6
12
|
* Entity framework representation of transactional and non-transactional database
|
|
@@ -43,13 +49,61 @@ export class EntityNonTransactionalQueryContext extends EntityQueryContext {
|
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
export class EntityTransactionalQueryContext extends EntityQueryContext {
|
|
52
|
+
private readonly postCommitInvalidationCallbacks: PostCommitCallback[] = [];
|
|
46
53
|
private readonly postCommitCallbacks: PostCommitCallback[] = [];
|
|
47
54
|
|
|
55
|
+
private readonly preCommitCallbacks: { callback: PreCommitCallback; order: number }[] = [];
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Schedule a pre-commit callback. These will be run within the transaction right before it is
|
|
59
|
+
* committed, and will be run in the order specified. Ordering of callbacks scheduled with the
|
|
60
|
+
* same value for the order parameter is undefined within that ordering group.
|
|
61
|
+
* @param callback - callback to schedule
|
|
62
|
+
* @param order - order in which this should be run relative to other scheduled pre-commit callbacks,
|
|
63
|
+
* with higher numbers running later than lower numbers.
|
|
64
|
+
*/
|
|
65
|
+
public appendPreCommitCallback(callback: PreCommitCallback, order: number): void {
|
|
66
|
+
assert(
|
|
67
|
+
order >= Number.MIN_SAFE_INTEGER && order <= Number.MAX_SAFE_INTEGER,
|
|
68
|
+
`Invalid order specified: ${order}`
|
|
69
|
+
);
|
|
70
|
+
this.preCommitCallbacks.push({ callback, order });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Schedule a post-commit cache invalidation callback. These are run before normal
|
|
75
|
+
* post-commit callbacks in order to have cache consistency in normal post-commit callbacks.
|
|
76
|
+
* @param callback - callback to schedule
|
|
77
|
+
*/
|
|
78
|
+
public appendPostCommitInvalidationCallback(callback: PostCommitCallback): void {
|
|
79
|
+
this.postCommitInvalidationCallbacks.push(callback);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Schedule a post-commit callback. These will be run after the transaction has
|
|
84
|
+
* been committed.
|
|
85
|
+
* @param callback - callback to schedule
|
|
86
|
+
*/
|
|
48
87
|
public appendPostCommitCallback(callback: PostCommitCallback): void {
|
|
49
88
|
this.postCommitCallbacks.push(callback);
|
|
50
89
|
}
|
|
51
90
|
|
|
91
|
+
public async runPreCommitCallbacksAsync(): Promise<void> {
|
|
92
|
+
const callbacks = [...this.preCommitCallbacks]
|
|
93
|
+
.sort((a, b) => a.order - b.order)
|
|
94
|
+
.map((c) => c.callback);
|
|
95
|
+
this.preCommitCallbacks.length = 0;
|
|
96
|
+
|
|
97
|
+
for (const callback of callbacks) {
|
|
98
|
+
await callback(this);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
52
102
|
public async runPostCommitCallbacksAsync(): Promise<void> {
|
|
103
|
+
const invalidationCallbacks = [...this.postCommitInvalidationCallbacks];
|
|
104
|
+
this.postCommitInvalidationCallbacks.length = 0;
|
|
105
|
+
await Promise.all(invalidationCallbacks.map((callback) => callback()));
|
|
106
|
+
|
|
53
107
|
const callbacks = [...this.postCommitCallbacks];
|
|
54
108
|
this.postCommitCallbacks.length = 0;
|
|
55
109
|
await Promise.all(callbacks.map((callback) => callback()));
|
|
@@ -38,6 +38,7 @@ export default abstract class EntityQueryContextProvider {
|
|
|
38
38
|
>()(async (queryInterface) => {
|
|
39
39
|
const queryContext = new EntityTransactionalQueryContext(queryInterface);
|
|
40
40
|
const result = await transactionScope(queryContext);
|
|
41
|
+
await queryContext.runPreCommitCallbacksAsync();
|
|
41
42
|
return [result, queryContext];
|
|
42
43
|
});
|
|
43
44
|
await queryContext.runPostCommitCallbacksAsync();
|
|
@@ -3,7 +3,10 @@ import { EntityCompanionDefinition } from '../EntityCompanionProvider';
|
|
|
3
3
|
import EntityConfiguration from '../EntityConfiguration';
|
|
4
4
|
import { EntityEdgeDeletionBehavior } from '../EntityFieldDefinition';
|
|
5
5
|
import { UUIDField } from '../EntityFields';
|
|
6
|
+
import { EntityTriggerMutationInfo, EntityMutationType } from '../EntityMutationInfo';
|
|
7
|
+
import { EntityMutationTrigger } from '../EntityMutationTriggerConfiguration';
|
|
6
8
|
import EntityPrivacyPolicy from '../EntityPrivacyPolicy';
|
|
9
|
+
import { EntityTransactionalQueryContext } from '../EntityQueryContext';
|
|
7
10
|
import { CacheStatus } from '../internal/ReadThroughEntityCache';
|
|
8
11
|
import AlwaysAllowPrivacyPolicyRule from '../rules/AlwaysAllowPrivacyPolicyRule';
|
|
9
12
|
import TestViewerContext from '../testfixtures/TestViewerContext';
|
|
@@ -47,6 +50,125 @@ class TestEntityPrivacyPolicy extends EntityPrivacyPolicy<
|
|
|
47
50
|
|
|
48
51
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
49
52
|
const makeEntityClasses = (edgeDeletionBehavior: EntityEdgeDeletionBehavior) => {
|
|
53
|
+
const triggerExecutionCounts = {
|
|
54
|
+
parent: 0,
|
|
55
|
+
child: 0,
|
|
56
|
+
grandchild: 0,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
class ParentCheckInfoTrigger extends EntityMutationTrigger<
|
|
60
|
+
ParentFields,
|
|
61
|
+
string,
|
|
62
|
+
TestViewerContext,
|
|
63
|
+
ParentEntity
|
|
64
|
+
> {
|
|
65
|
+
async executeAsync(
|
|
66
|
+
_viewerContext: TestViewerContext,
|
|
67
|
+
_queryContext: EntityTransactionalQueryContext,
|
|
68
|
+
_entity: ParentEntity,
|
|
69
|
+
mutationInfo: EntityTriggerMutationInfo<ParentFields, string, TestViewerContext, ParentEntity>
|
|
70
|
+
): Promise<void> {
|
|
71
|
+
if (mutationInfo.type !== EntityMutationType.DELETE) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (mutationInfo.cascadingDeleteCause !== null) {
|
|
76
|
+
throw new Error('Parent entity should not have casade delete cause');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
triggerExecutionCounts.parent++;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
class ChildCheckInfoTrigger extends EntityMutationTrigger<
|
|
84
|
+
ChildFields,
|
|
85
|
+
string,
|
|
86
|
+
TestViewerContext,
|
|
87
|
+
ChildEntity
|
|
88
|
+
> {
|
|
89
|
+
async executeAsync(
|
|
90
|
+
_viewerContext: TestViewerContext,
|
|
91
|
+
_queryContext: EntityTransactionalQueryContext,
|
|
92
|
+
_entity: ChildEntity,
|
|
93
|
+
mutationInfo: EntityTriggerMutationInfo<ChildFields, string, TestViewerContext, ChildEntity>
|
|
94
|
+
): Promise<void> {
|
|
95
|
+
if (mutationInfo.type !== EntityMutationType.DELETE) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (mutationInfo.cascadingDeleteCause === null) {
|
|
100
|
+
throw new Error('Child entity should have casade delete cause');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const cascadingDeleteCauseEntity = mutationInfo.cascadingDeleteCause.entity;
|
|
104
|
+
if (!(cascadingDeleteCauseEntity instanceof ParentEntity)) {
|
|
105
|
+
throw new Error('Child entity should have casade delete cause entity of type ParentEntity');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const secondLevelCascadingDeleteCause =
|
|
109
|
+
mutationInfo.cascadingDeleteCause.cascadingDeleteCause;
|
|
110
|
+
if (secondLevelCascadingDeleteCause) {
|
|
111
|
+
throw new Error('Child entity should not have two-level casade delete cause');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
triggerExecutionCounts.child++;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
class GrandChildCheckInfoTrigger extends EntityMutationTrigger<
|
|
119
|
+
GrandChildFields,
|
|
120
|
+
string,
|
|
121
|
+
TestViewerContext,
|
|
122
|
+
GrandChildEntity
|
|
123
|
+
> {
|
|
124
|
+
async executeAsync(
|
|
125
|
+
_viewerContext: TestViewerContext,
|
|
126
|
+
_queryContext: EntityTransactionalQueryContext,
|
|
127
|
+
_entity: GrandChildEntity,
|
|
128
|
+
mutationInfo: EntityTriggerMutationInfo<
|
|
129
|
+
GrandChildFields,
|
|
130
|
+
string,
|
|
131
|
+
TestViewerContext,
|
|
132
|
+
GrandChildEntity
|
|
133
|
+
>
|
|
134
|
+
): Promise<void> {
|
|
135
|
+
if (mutationInfo.type !== EntityMutationType.DELETE) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (mutationInfo.cascadingDeleteCause === null) {
|
|
140
|
+
throw new Error('GrandChild entity should have cascade delete cause');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const cascadingDeleteCauseEntity = mutationInfo.cascadingDeleteCause.entity;
|
|
144
|
+
if (!(cascadingDeleteCauseEntity instanceof ChildEntity)) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
'GrandChild entity should have cascade delete cause entity of type ChildEntity'
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const secondLevelCascadingDeleteCause =
|
|
151
|
+
mutationInfo.cascadingDeleteCause.cascadingDeleteCause;
|
|
152
|
+
if (!secondLevelCascadingDeleteCause) {
|
|
153
|
+
throw new Error('GrandChild entity should have two-level casade delete cause');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const secondLevelCascadingDeleteCauseEntity = secondLevelCascadingDeleteCause.entity;
|
|
157
|
+
if (!(secondLevelCascadingDeleteCauseEntity instanceof ParentEntity)) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
'GrandChild entity should have second level casade delete cause entity of type ParentEntity'
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const thirdLevelCascadingDeleteCause = secondLevelCascadingDeleteCause.cascadingDeleteCause;
|
|
164
|
+
if (thirdLevelCascadingDeleteCause) {
|
|
165
|
+
throw new Error('GrandChild entity should not have three-level casade delete cause');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
triggerExecutionCounts.grandchild++;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
50
172
|
class ParentEntity extends Entity<ParentFields, string, TestViewerContext> {
|
|
51
173
|
static getCompanionDefinition(): EntityCompanionDefinition<
|
|
52
174
|
ParentFields,
|
|
@@ -144,33 +266,45 @@ const makeEntityClasses = (edgeDeletionBehavior: EntityEdgeDeletionBehavior) =>
|
|
|
144
266
|
entityClass: ParentEntity,
|
|
145
267
|
entityConfiguration: parentEntityConfiguration,
|
|
146
268
|
privacyPolicyClass: TestEntityPrivacyPolicy,
|
|
269
|
+
mutationTriggers: () => ({
|
|
270
|
+
beforeDelete: [new ParentCheckInfoTrigger()],
|
|
271
|
+
afterDelete: [new ParentCheckInfoTrigger()],
|
|
272
|
+
}),
|
|
147
273
|
});
|
|
148
274
|
|
|
149
275
|
const childEntityCompanion = new EntityCompanionDefinition({
|
|
150
276
|
entityClass: ChildEntity,
|
|
151
277
|
entityConfiguration: childEntityConfiguration,
|
|
152
278
|
privacyPolicyClass: TestEntityPrivacyPolicy,
|
|
279
|
+
mutationTriggers: () => ({
|
|
280
|
+
beforeDelete: [new ChildCheckInfoTrigger()],
|
|
281
|
+
afterDelete: [new ChildCheckInfoTrigger()],
|
|
282
|
+
}),
|
|
153
283
|
});
|
|
154
284
|
|
|
155
285
|
const grandChildEntityCompanion = new EntityCompanionDefinition({
|
|
156
286
|
entityClass: GrandChildEntity,
|
|
157
287
|
entityConfiguration: grandChildEntityConfiguration,
|
|
158
288
|
privacyPolicyClass: TestEntityPrivacyPolicy,
|
|
289
|
+
mutationTriggers: () => ({
|
|
290
|
+
beforeDelete: [new GrandChildCheckInfoTrigger()],
|
|
291
|
+
afterDelete: [new GrandChildCheckInfoTrigger()],
|
|
292
|
+
}),
|
|
159
293
|
});
|
|
160
294
|
|
|
161
295
|
return {
|
|
162
296
|
ParentEntity,
|
|
163
297
|
ChildEntity,
|
|
164
298
|
GrandChildEntity,
|
|
299
|
+
triggerExecutionCounts,
|
|
165
300
|
};
|
|
166
301
|
};
|
|
167
302
|
|
|
168
303
|
describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
|
|
169
304
|
describe('EntityEdgeDeletionBehavior.CASCADE_DELETE', () => {
|
|
170
305
|
it('deletes', async () => {
|
|
171
|
-
const { ParentEntity, ChildEntity, GrandChildEntity } =
|
|
172
|
-
EntityEdgeDeletionBehavior.CASCADE_DELETE
|
|
173
|
-
);
|
|
306
|
+
const { ParentEntity, ChildEntity, GrandChildEntity, triggerExecutionCounts } =
|
|
307
|
+
makeEntityClasses(EntityEdgeDeletionBehavior.CASCADE_DELETE);
|
|
174
308
|
const companionProvider = createUnitTestEntityCompanionProvider();
|
|
175
309
|
const viewerContext = new TestViewerContext(companionProvider);
|
|
176
310
|
|
|
@@ -203,14 +337,20 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
|
|
|
203
337
|
await expect(
|
|
204
338
|
GrandChildEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(grandchild.getID())
|
|
205
339
|
).resolves.toBeNull();
|
|
340
|
+
|
|
341
|
+
// two calls for each trigger, one beforeDelete, one afterDelete
|
|
342
|
+
expect(triggerExecutionCounts).toMatchObject({
|
|
343
|
+
parent: 2,
|
|
344
|
+
child: 2,
|
|
345
|
+
grandchild: 2,
|
|
346
|
+
});
|
|
206
347
|
});
|
|
207
348
|
});
|
|
208
349
|
|
|
209
350
|
describe('EntityEdgeDeletionBehavior.SET_NULL', () => {
|
|
210
351
|
it('sets null', async () => {
|
|
211
|
-
const { ParentEntity, ChildEntity, GrandChildEntity } =
|
|
212
|
-
EntityEdgeDeletionBehavior.SET_NULL
|
|
213
|
-
);
|
|
352
|
+
const { ParentEntity, ChildEntity, GrandChildEntity, triggerExecutionCounts } =
|
|
353
|
+
makeEntityClasses(EntityEdgeDeletionBehavior.SET_NULL);
|
|
214
354
|
|
|
215
355
|
const companionProvider = createUnitTestEntityCompanionProvider();
|
|
216
356
|
const viewerContext = new TestViewerContext(companionProvider);
|
|
@@ -248,14 +388,21 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
|
|
|
248
388
|
.enforcing()
|
|
249
389
|
.loadByIDAsync(grandchild.getID());
|
|
250
390
|
expect(loadedGrandchild.getField('parent_id')).toEqual(loadedChild.getID());
|
|
391
|
+
|
|
392
|
+
// two calls for only parent trigger, one beforeDelete, one afterDelete
|
|
393
|
+
// when using set null the descendants aren't deleted
|
|
394
|
+
expect(triggerExecutionCounts).toMatchObject({
|
|
395
|
+
parent: 2,
|
|
396
|
+
child: 0,
|
|
397
|
+
grandchild: 0,
|
|
398
|
+
});
|
|
251
399
|
});
|
|
252
400
|
});
|
|
253
401
|
|
|
254
402
|
describe('EntityEdgeDeletionBehavior.CASCADE_DELETE_INVALIDATE_CACHE', () => {
|
|
255
403
|
it('invalidates the cache', async () => {
|
|
256
|
-
const { ParentEntity, ChildEntity, GrandChildEntity } =
|
|
257
|
-
EntityEdgeDeletionBehavior.CASCADE_DELETE_INVALIDATE_CACHE
|
|
258
|
-
);
|
|
404
|
+
const { ParentEntity, ChildEntity, GrandChildEntity, triggerExecutionCounts } =
|
|
405
|
+
makeEntityClasses(EntityEdgeDeletionBehavior.CASCADE_DELETE_INVALIDATE_CACHE);
|
|
259
406
|
|
|
260
407
|
const companionProvider = createUnitTestEntityCompanionProvider();
|
|
261
408
|
const viewerContext = new TestViewerContext(companionProvider);
|
|
@@ -319,6 +466,13 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
|
|
|
319
466
|
await expect(
|
|
320
467
|
GrandChildEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(grandchild.getID())
|
|
321
468
|
).resolves.not.toBeNull();
|
|
469
|
+
|
|
470
|
+
// two calls for each trigger, one beforeDelete, one afterDelete
|
|
471
|
+
expect(triggerExecutionCounts).toMatchObject({
|
|
472
|
+
parent: 2,
|
|
473
|
+
child: 2,
|
|
474
|
+
grandchild: 2,
|
|
475
|
+
});
|
|
322
476
|
});
|
|
323
477
|
});
|
|
324
478
|
});
|