@expo/entity 0.18.0 → 0.22.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 (85) hide show
  1. package/build/Entity.js +8 -2
  2. package/build/Entity.js.map +1 -1
  3. package/build/EntityCompanion.d.ts +5 -0
  4. package/build/EntityCompanion.js +8 -1
  5. package/build/EntityCompanion.js.map +1 -1
  6. package/build/EntityCompanionProvider.d.ts +1 -1
  7. package/build/EntityCompanionProvider.js.map +1 -1
  8. package/build/EntityLoader.d.ts +3 -1
  9. package/build/EntityLoader.js +5 -4
  10. package/build/EntityLoader.js.map +1 -1
  11. package/build/EntityLoaderFactory.d.ts +3 -1
  12. package/build/EntityLoaderFactory.js +3 -2
  13. package/build/EntityLoaderFactory.js.map +1 -1
  14. package/build/EntityMutationInfo.d.ts +26 -0
  15. package/build/EntityMutationInfo.js +10 -0
  16. package/build/EntityMutationInfo.js.map +1 -0
  17. package/build/EntityMutationTriggerConfiguration.d.ts +3 -3
  18. package/build/EntityMutationValidator.d.ts +2 -2
  19. package/build/EntityMutationValidator.js.map +1 -1
  20. package/build/EntityMutator.d.ts +4 -15
  21. package/build/EntityMutator.js +48 -44
  22. package/build/EntityMutator.js.map +1 -1
  23. package/build/EntityPrivacyPolicy.d.ts +5 -4
  24. package/build/EntityPrivacyPolicy.js +60 -12
  25. package/build/EntityPrivacyPolicy.js.map +1 -1
  26. package/build/EntityQueryContext.d.ts +24 -0
  27. package/build/EntityQueryContext.js +43 -0
  28. package/build/EntityQueryContext.js.map +1 -1
  29. package/build/EntityQueryContextProvider.js +1 -0
  30. package/build/EntityQueryContextProvider.js.map +1 -1
  31. package/build/ViewerScopedEntityCompanion.d.ts +5 -0
  32. package/build/ViewerScopedEntityCompanion.js +6 -0
  33. package/build/ViewerScopedEntityCompanion.js.map +1 -1
  34. package/build/__tests__/EntityEdges-test.js +99 -3
  35. package/build/__tests__/EntityEdges-test.js.map +1 -1
  36. package/build/__tests__/EntityLoader-constructor-test.js +3 -2
  37. package/build/__tests__/EntityLoader-constructor-test.js.map +1 -1
  38. package/build/__tests__/EntityLoader-test.js +19 -11
  39. package/build/__tests__/EntityLoader-test.js.map +1 -1
  40. package/build/__tests__/EntityMutator-MutationCacheConsistency-test.d.ts +1 -0
  41. package/build/__tests__/EntityMutator-MutationCacheConsistency-test.js +81 -0
  42. package/build/__tests__/EntityMutator-MutationCacheConsistency-test.js.map +1 -0
  43. package/build/__tests__/EntityMutator-test.js +16 -14
  44. package/build/__tests__/EntityMutator-test.js.map +1 -1
  45. package/build/__tests__/EntityPrivacyPolicy-test.js +119 -43
  46. package/build/__tests__/EntityPrivacyPolicy-test.js.map +1 -1
  47. package/build/__tests__/EntityQueryContext-test.d.ts +1 -0
  48. package/build/__tests__/EntityQueryContext-test.js +56 -0
  49. package/build/__tests__/EntityQueryContext-test.js.map +1 -0
  50. package/build/__tests__/EntitySecondaryCacheLoader-test.js +1 -1
  51. package/build/__tests__/EntitySecondaryCacheLoader-test.js.map +1 -1
  52. package/build/index.d.ts +1 -0
  53. package/build/index.js +1 -0
  54. package/build/index.js.map +1 -1
  55. package/build/metrics/IEntityMetricsAdapter.d.ts +16 -0
  56. package/build/metrics/IEntityMetricsAdapter.js +6 -1
  57. package/build/metrics/IEntityMetricsAdapter.js.map +1 -1
  58. package/build/metrics/NoOpEntityMetricsAdapter.d.ts +2 -1
  59. package/build/metrics/NoOpEntityMetricsAdapter.js +1 -0
  60. package/build/metrics/NoOpEntityMetricsAdapter.js.map +1 -1
  61. package/package.json +1 -1
  62. package/src/Entity.ts +10 -2
  63. package/src/EntityCompanion.ts +10 -2
  64. package/src/EntityCompanionProvider.ts +1 -1
  65. package/src/EntityLoader.ts +9 -4
  66. package/src/EntityLoaderFactory.ts +5 -2
  67. package/src/EntityMutationInfo.ts +47 -0
  68. package/src/EntityMutationTriggerConfiguration.ts +3 -3
  69. package/src/EntityMutationValidator.ts +8 -2
  70. package/src/EntityMutator.ts +91 -71
  71. package/src/EntityPrivacyPolicy.ts +76 -18
  72. package/src/EntityQueryContext.ts +54 -0
  73. package/src/EntityQueryContextProvider.ts +1 -0
  74. package/src/ViewerScopedEntityCompanion.ts +8 -0
  75. package/src/__tests__/EntityEdges-test.ts +163 -9
  76. package/src/__tests__/EntityLoader-constructor-test.ts +4 -2
  77. package/src/__tests__/EntityLoader-test.ts +39 -11
  78. package/src/__tests__/EntityMutator-MutationCacheConsistency-test.ts +105 -0
  79. package/src/__tests__/EntityMutator-test.ts +38 -13
  80. package/src/__tests__/EntityPrivacyPolicy-test.ts +189 -52
  81. package/src/__tests__/EntityQueryContext-test.ts +82 -0
  82. package/src/__tests__/EntitySecondaryCacheLoader-test.ts +1 -0
  83. package/src/index.ts +1 -0
  84. package/src/metrics/IEntityMetricsAdapter.ts +23 -0
  85. package/src/metrics/NoOpEntityMetricsAdapter.ts +2 -0
@@ -2,6 +2,9 @@ import { EntityQueryContext } from './EntityQueryContext';
2
2
  import ReadonlyEntity from './ReadonlyEntity';
3
3
  import ViewerContext from './ViewerContext';
4
4
  import EntityNotAuthorizedError from './errors/EntityNotAuthorizedError';
5
+ import IEntityMetricsAdapter, {
6
+ EntityMetricsAuthorizationResult,
7
+ } from './metrics/IEntityMetricsAdapter';
5
8
  import PrivacyPolicyRule, { RuleEvaluationResult } from './rules/PrivacyPolicyRule';
6
9
 
7
10
  export enum EntityPrivacyPolicyEvaluationMode {
@@ -124,14 +127,16 @@ export default abstract class EntityPrivacyPolicy<
124
127
  async authorizeCreateAsync(
125
128
  viewerContext: TViewerContext,
126
129
  queryContext: EntityQueryContext,
127
- entity: TEntity
130
+ entity: TEntity,
131
+ metricsAdapter: IEntityMetricsAdapter
128
132
  ): Promise<TEntity> {
129
133
  return await this.authorizeForRulesetAsync(
130
134
  this.createRules,
131
135
  viewerContext,
132
136
  queryContext,
133
137
  entity,
134
- EntityAuthorizationAction.CREATE
138
+ EntityAuthorizationAction.CREATE,
139
+ metricsAdapter
135
140
  );
136
141
  }
137
142
 
@@ -146,14 +151,16 @@ export default abstract class EntityPrivacyPolicy<
146
151
  async authorizeReadAsync(
147
152
  viewerContext: TViewerContext,
148
153
  queryContext: EntityQueryContext,
149
- entity: TEntity
154
+ entity: TEntity,
155
+ metricsAdapter: IEntityMetricsAdapter
150
156
  ): Promise<TEntity> {
151
157
  return await this.authorizeForRulesetAsync(
152
158
  this.readRules,
153
159
  viewerContext,
154
160
  queryContext,
155
161
  entity,
156
- EntityAuthorizationAction.READ
162
+ EntityAuthorizationAction.READ,
163
+ metricsAdapter
157
164
  );
158
165
  }
159
166
 
@@ -168,14 +175,16 @@ export default abstract class EntityPrivacyPolicy<
168
175
  async authorizeUpdateAsync(
169
176
  viewerContext: TViewerContext,
170
177
  queryContext: EntityQueryContext,
171
- entity: TEntity
178
+ entity: TEntity,
179
+ metricsAdapter: IEntityMetricsAdapter
172
180
  ): Promise<TEntity> {
173
181
  return await this.authorizeForRulesetAsync(
174
182
  this.updateRules,
175
183
  viewerContext,
176
184
  queryContext,
177
185
  entity,
178
- EntityAuthorizationAction.UPDATE
186
+ EntityAuthorizationAction.UPDATE,
187
+ metricsAdapter
179
188
  );
180
189
  }
181
190
 
@@ -190,14 +199,16 @@ export default abstract class EntityPrivacyPolicy<
190
199
  async authorizeDeleteAsync(
191
200
  viewerContext: TViewerContext,
192
201
  queryContext: EntityQueryContext,
193
- entity: TEntity
202
+ entity: TEntity,
203
+ metricsAdapter: IEntityMetricsAdapter
194
204
  ): Promise<TEntity> {
195
205
  return await this.authorizeForRulesetAsync(
196
206
  this.deleteRules,
197
207
  viewerContext,
198
208
  queryContext,
199
209
  entity,
200
- EntityAuthorizationAction.DELETE
210
+ EntityAuthorizationAction.DELETE,
211
+ metricsAdapter
201
212
  );
202
213
  }
203
214
 
@@ -206,48 +217,95 @@ export default abstract class EntityPrivacyPolicy<
206
217
  viewerContext: TViewerContext,
207
218
  queryContext: EntityQueryContext,
208
219
  entity: TEntity,
209
- action: EntityAuthorizationAction
220
+ action: EntityAuthorizationAction,
221
+ metricsAdapter: IEntityMetricsAdapter
210
222
  ): Promise<TEntity> {
211
223
  const privacyPolicyEvaluator = this.getPrivacyPolicyEvaluator(viewerContext);
212
224
  switch (privacyPolicyEvaluator.mode) {
213
225
  case EntityPrivacyPolicyEvaluationMode.ENFORCE:
214
- return await this.authorizeForRulesetInnerAsync(
215
- ruleset,
216
- viewerContext,
217
- queryContext,
218
- entity,
219
- action
220
- );
226
+ try {
227
+ const result = await this.authorizeForRulesetInnerAsync(
228
+ ruleset,
229
+ viewerContext,
230
+ queryContext,
231
+ entity,
232
+ action
233
+ );
234
+ metricsAdapter.logAuthorizationEvent({
235
+ entityClassName: entity.constructor.name,
236
+ action,
237
+ evaluationResult: EntityMetricsAuthorizationResult.ALLOW,
238
+ privacyPolicyEvaluationMode: privacyPolicyEvaluator.mode,
239
+ });
240
+ return result;
241
+ } catch (e) {
242
+ if (!(e instanceof EntityNotAuthorizedError)) {
243
+ throw e;
244
+ }
245
+ metricsAdapter.logAuthorizationEvent({
246
+ entityClassName: entity.constructor.name,
247
+ action,
248
+ evaluationResult: EntityMetricsAuthorizationResult.DENY,
249
+ privacyPolicyEvaluationMode: privacyPolicyEvaluator.mode,
250
+ });
251
+ throw e;
252
+ }
221
253
  case EntityPrivacyPolicyEvaluationMode.ENFORCE_AND_LOG:
222
254
  try {
223
- return await this.authorizeForRulesetInnerAsync(
255
+ const result = await this.authorizeForRulesetInnerAsync(
224
256
  ruleset,
225
257
  viewerContext,
226
258
  queryContext,
227
259
  entity,
228
260
  action
229
261
  );
262
+ metricsAdapter.logAuthorizationEvent({
263
+ entityClassName: entity.constructor.name,
264
+ action,
265
+ evaluationResult: EntityMetricsAuthorizationResult.ALLOW,
266
+ privacyPolicyEvaluationMode: privacyPolicyEvaluator.mode,
267
+ });
268
+ return result;
230
269
  } catch (e) {
231
270
  if (!(e instanceof EntityNotAuthorizedError)) {
232
271
  throw e;
233
272
  }
234
273
  privacyPolicyEvaluator.denyHandler(e);
274
+ metricsAdapter.logAuthorizationEvent({
275
+ entityClassName: entity.constructor.name,
276
+ action,
277
+ evaluationResult: EntityMetricsAuthorizationResult.DENY,
278
+ privacyPolicyEvaluationMode: privacyPolicyEvaluator.mode,
279
+ });
235
280
  throw e;
236
281
  }
237
282
  case EntityPrivacyPolicyEvaluationMode.DRY_RUN:
238
283
  try {
239
- return await this.authorizeForRulesetInnerAsync(
284
+ const result = await this.authorizeForRulesetInnerAsync(
240
285
  ruleset,
241
286
  viewerContext,
242
287
  queryContext,
243
288
  entity,
244
289
  action
245
290
  );
291
+ metricsAdapter.logAuthorizationEvent({
292
+ entityClassName: entity.constructor.name,
293
+ action,
294
+ evaluationResult: EntityMetricsAuthorizationResult.ALLOW,
295
+ privacyPolicyEvaluationMode: privacyPolicyEvaluator.mode,
296
+ });
297
+ return result;
246
298
  } catch (e) {
247
299
  if (!(e instanceof EntityNotAuthorizedError)) {
248
300
  throw e;
249
301
  }
250
302
  privacyPolicyEvaluator.denyHandler(e);
303
+ metricsAdapter.logAuthorizationEvent({
304
+ entityClassName: entity.constructor.name,
305
+ action,
306
+ evaluationResult: EntityMetricsAuthorizationResult.DENY,
307
+ privacyPolicyEvaluationMode: privacyPolicyEvaluator.mode,
308
+ });
251
309
  return entity;
252
310
  }
253
311
  }
@@ -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();
@@ -5,6 +5,7 @@ import ReadonlyEntity from './ReadonlyEntity';
5
5
  import ViewerContext from './ViewerContext';
6
6
  import ViewerScopedEntityLoaderFactory from './ViewerScopedEntityLoaderFactory';
7
7
  import ViewerScopedEntityMutatorFactory from './ViewerScopedEntityMutatorFactory';
8
+ import IEntityMetricsAdapter from './metrics/IEntityMetricsAdapter';
8
9
 
9
10
  /**
10
11
  * Provides a simpler API for loading and mutating entities by injecting the {@link ViewerContext}
@@ -76,4 +77,11 @@ export default class ViewerScopedEntityCompanion<
76
77
  getQueryContextProvider(): EntityQueryContextProvider {
77
78
  return this.entityCompanion.getQueryContextProvider();
78
79
  }
80
+
81
+ /**
82
+ * Get the {@link IEntityMetricsAdapter} for this companion.
83
+ */
84
+ getMetricsAdapter(): IEntityMetricsAdapter {
85
+ return this.entityCompanion.getMetricsAdapter();
86
+ }
79
87
  }
@@ -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
  });
@@ -118,6 +118,7 @@ export const testEntityCompanion = new EntityCompanionDefinition({
118
118
  describe(EntityLoader, () => {
119
119
  it('handles thrown errors and literals from constructor', async () => {
120
120
  const viewerContext = instance(mock(ViewerContext));
121
+ const metricsAdapter = instance(mock<IEntityMetricsAdapter>());
121
122
  const queryContext = StubQueryContextProvider.getQueryContext();
122
123
 
123
124
  const databaseAdapter = new StubDatabaseAdapter<TestFields>(
@@ -147,7 +148,7 @@ describe(EntityLoader, () => {
147
148
  databaseAdapter,
148
149
  entityCache,
149
150
  StubQueryContextProvider,
150
- instance(mock<IEntityMetricsAdapter>()),
151
+ metricsAdapter,
151
152
  TestEntity.name
152
153
  );
153
154
  const entityLoader = new EntityLoader(
@@ -156,7 +157,8 @@ describe(EntityLoader, () => {
156
157
  testEntityConfiguration,
157
158
  TestEntity,
158
159
  privacyPolicy,
159
- dataManager
160
+ dataManager,
161
+ metricsAdapter
160
162
  );
161
163
 
162
164
  let capturedThrownThing1: any;