@expo/entity 0.19.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/build/EntityMutationInfo.d.ts +26 -0
  2. package/build/EntityMutationInfo.js +10 -0
  3. package/build/EntityMutationInfo.js.map +1 -0
  4. package/build/EntityMutationTriggerConfiguration.d.ts +3 -3
  5. package/build/EntityMutationValidator.d.ts +2 -2
  6. package/build/EntityMutationValidator.js.map +1 -1
  7. package/build/EntityMutator.d.ts +4 -15
  8. package/build/EntityMutator.js +45 -41
  9. package/build/EntityMutator.js.map +1 -1
  10. package/build/EntityQueryContext.d.ts +13 -0
  11. package/build/EntityQueryContext.js +18 -0
  12. package/build/EntityQueryContext.js.map +1 -1
  13. package/build/__tests__/EntityEdges-test.js +99 -3
  14. package/build/__tests__/EntityEdges-test.js.map +1 -1
  15. package/build/__tests__/EntityMutator-MutationCacheConsistency-test.d.ts +1 -0
  16. package/build/__tests__/EntityMutator-MutationCacheConsistency-test.js +81 -0
  17. package/build/__tests__/EntityMutator-MutationCacheConsistency-test.js.map +1 -0
  18. package/build/__tests__/EntityMutator-test.js +9 -7
  19. package/build/__tests__/EntityMutator-test.js.map +1 -1
  20. package/build/index.d.ts +1 -0
  21. package/build/index.js +1 -0
  22. package/build/index.js.map +1 -1
  23. package/package.json +1 -1
  24. package/src/EntityMutationInfo.ts +47 -0
  25. package/src/EntityMutationTriggerConfiguration.ts +3 -3
  26. package/src/EntityMutationValidator.ts +8 -2
  27. package/src/EntityMutator.ts +81 -68
  28. package/src/EntityQueryContext.ts +20 -0
  29. package/src/__tests__/EntityEdges-test.ts +163 -9
  30. package/src/__tests__/EntityMutator-MutationCacheConsistency-test.ts +105 -0
  31. package/src/__tests__/EntityMutator-test.ts +24 -6
  32. package/src/index.ts +1 -0
@@ -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 executeMutationTriggersOrValidatorsAsync(
114
- triggersOrValidators:
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: EntityMutationInfo<TFields, TID, TViewerContext, TEntity, TSelectedFields>
120
+ mutationInfo: EntityTriggerMutationInfo<TFields, TID, TViewerContext, TEntity, TSelectedFields>
121
121
  ): Promise<void> {
122
- if (!triggersOrValidators) {
122
+ if (!triggers) {
123
123
  return;
124
124
  }
125
125
  await Promise.all(
126
- triggersOrValidators.map((triggerOrValidator) =>
127
- triggerOrValidator.executeAsync(this.viewerContext, queryContext, entity, mutationInfo)
126
+ triggers.map((trigger) =>
127
+ trigger.executeAsync(this.viewerContext, queryContext, entity, mutationInfo)
128
128
  )
129
129
  );
130
130
  }
131
131
 
132
- protected async executeNonTransactionalMutationTriggersOrValidatorsAsync(
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: EntityMutationInfo<TFields, TID, TViewerContext, TEntity, TSelectedFields>
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.executeMutationTriggersOrValidatorsAsync(
231
+ await this.executeMutationValidatorsAsync(
232
232
  this.mutationValidators,
233
233
  queryContext,
234
234
  temporaryEntityForPrivacyCheck,
235
235
  { type: EntityMutationType.CREATE }
236
236
  );
237
- await this.executeMutationTriggersOrValidatorsAsync(
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.executeMutationTriggersOrValidatorsAsync(
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.appendPostCommitCallback(
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.executeMutationTriggersOrValidatorsAsync(
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.executeMutationTriggersOrValidatorsAsync(
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.executeNonTransactionalMutationTriggersOrValidatorsAsync.bind(
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.executeMutationTriggersOrValidatorsAsync(
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.executeMutationTriggersOrValidatorsAsync(
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.executeMutationTriggersOrValidatorsAsync(
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.appendPostCommitCallback(
448
+ queryContext.appendPostCommitInvalidationCallback(
449
449
  entityLoader.invalidateFieldsAsync.bind(
450
450
  entityLoader,
451
451
  this.originalEntity.getAllDatabaseFields()
452
452
  )
453
453
  );
454
- queryContext.appendPostCommitCallback(
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.executeMutationTriggersOrValidatorsAsync(
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.executeMutationTriggersOrValidatorsAsync(
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.executeNonTransactionalMutationTriggersOrValidatorsAsync.bind(
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> = new Set(),
580
- skipDatabaseDeletion: boolean = false
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.executeMutationTriggersOrValidatorsAsync(
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.executeMutationTriggersOrValidatorsAsync(
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.appendPostCommitCallback(
640
+ queryContext.appendPostCommitInvalidationCallback(
637
641
  entityLoader.invalidateFieldsAsync.bind(entityLoader, this.entity.getAllDatabaseFields())
638
642
  );
639
643
 
640
- await this.executeMutationTriggersOrValidatorsAsync(
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.executeMutationTriggersOrValidatorsAsync(
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.executeNonTransactionalMutationTriggersOrValidatorsAsync.bind(
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
  );
@@ -43,13 +43,33 @@ export class EntityNonTransactionalQueryContext extends EntityQueryContext {
43
43
  }
44
44
 
45
45
  export class EntityTransactionalQueryContext extends EntityQueryContext {
46
+ private readonly postCommitInvalidationCallbacks: PostCommitCallback[] = [];
46
47
  private readonly postCommitCallbacks: PostCommitCallback[] = [];
47
48
 
49
+ /**
50
+ * Schedule a post-commit cache invalidation callback. These are run before normal
51
+ * post-commit callbacks in order to have cache consistency in normal post-commit callbacks.
52
+ *
53
+ * @param callback - callback to schedule
54
+ */
55
+ public appendPostCommitInvalidationCallback(callback: PostCommitCallback): void {
56
+ this.postCommitInvalidationCallbacks.push(callback);
57
+ }
58
+
59
+ /**
60
+ * Schedule a post-commit callback. These will be run after the transaction has
61
+ * been committed.
62
+ * @param callback - callback to schedule
63
+ */
48
64
  public appendPostCommitCallback(callback: PostCommitCallback): void {
49
65
  this.postCommitCallbacks.push(callback);
50
66
  }
51
67
 
52
68
  public async runPostCommitCallbacksAsync(): Promise<void> {
69
+ const invalidationCallbacks = [...this.postCommitInvalidationCallbacks];
70
+ this.postCommitInvalidationCallbacks.length = 0;
71
+ await Promise.all(invalidationCallbacks.map((callback) => callback()));
72
+
53
73
  const callbacks = [...this.postCommitCallbacks];
54
74
  this.postCommitCallbacks.length = 0;
55
75
  await Promise.all(callbacks.map((callback) => callback()));
@@ -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 } = makeEntityClasses(
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 } = makeEntityClasses(
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 } = makeEntityClasses(
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
  });