@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.
- package/build/Entity.js +8 -2
- package/build/Entity.js.map +1 -1
- package/build/EntityCompanion.d.ts +5 -0
- package/build/EntityCompanion.js +8 -1
- package/build/EntityCompanion.js.map +1 -1
- package/build/EntityCompanionProvider.d.ts +1 -1
- package/build/EntityCompanionProvider.js.map +1 -1
- package/build/EntityLoader.d.ts +3 -1
- package/build/EntityLoader.js +5 -4
- package/build/EntityLoader.js.map +1 -1
- package/build/EntityLoaderFactory.d.ts +3 -1
- package/build/EntityLoaderFactory.js +3 -2
- package/build/EntityLoaderFactory.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 +48 -44
- package/build/EntityMutator.js.map +1 -1
- package/build/EntityPrivacyPolicy.d.ts +5 -4
- package/build/EntityPrivacyPolicy.js +60 -12
- package/build/EntityPrivacyPolicy.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/ViewerScopedEntityCompanion.d.ts +5 -0
- package/build/ViewerScopedEntityCompanion.js +6 -0
- package/build/ViewerScopedEntityCompanion.js.map +1 -1
- package/build/__tests__/EntityEdges-test.js +99 -3
- package/build/__tests__/EntityEdges-test.js.map +1 -1
- package/build/__tests__/EntityLoader-constructor-test.js +3 -2
- package/build/__tests__/EntityLoader-constructor-test.js.map +1 -1
- package/build/__tests__/EntityLoader-test.js +19 -11
- package/build/__tests__/EntityLoader-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 +16 -14
- package/build/__tests__/EntityMutator-test.js.map +1 -1
- package/build/__tests__/EntityPrivacyPolicy-test.js +119 -43
- package/build/__tests__/EntityPrivacyPolicy-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/__tests__/EntitySecondaryCacheLoader-test.js +1 -1
- package/build/__tests__/EntitySecondaryCacheLoader-test.js.map +1 -1
- package/build/index.d.ts +1 -0
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/metrics/IEntityMetricsAdapter.d.ts +16 -0
- package/build/metrics/IEntityMetricsAdapter.js +6 -1
- package/build/metrics/IEntityMetricsAdapter.js.map +1 -1
- package/build/metrics/NoOpEntityMetricsAdapter.d.ts +2 -1
- package/build/metrics/NoOpEntityMetricsAdapter.js +1 -0
- package/build/metrics/NoOpEntityMetricsAdapter.js.map +1 -1
- package/package.json +1 -1
- package/src/Entity.ts +10 -2
- package/src/EntityCompanion.ts +10 -2
- package/src/EntityCompanionProvider.ts +1 -1
- package/src/EntityLoader.ts +9 -4
- package/src/EntityLoaderFactory.ts +5 -2
- package/src/EntityMutationInfo.ts +47 -0
- package/src/EntityMutationTriggerConfiguration.ts +3 -3
- package/src/EntityMutationValidator.ts +8 -2
- package/src/EntityMutator.ts +91 -71
- package/src/EntityPrivacyPolicy.ts +76 -18
- package/src/EntityQueryContext.ts +54 -0
- package/src/EntityQueryContextProvider.ts +1 -0
- package/src/ViewerScopedEntityCompanion.ts +8 -0
- package/src/__tests__/EntityEdges-test.ts +163 -9
- package/src/__tests__/EntityLoader-constructor-test.ts +4 -2
- package/src/__tests__/EntityLoader-test.ts +39 -11
- package/src/__tests__/EntityMutator-MutationCacheConsistency-test.ts +105 -0
- package/src/__tests__/EntityMutator-test.ts +38 -13
- package/src/__tests__/EntityPrivacyPolicy-test.ts +189 -52
- package/src/__tests__/EntityQueryContext-test.ts +82 -0
- package/src/__tests__/EntitySecondaryCacheLoader-test.ts +1 -0
- package/src/index.ts +1 -0
- package/src/metrics/IEntityMetricsAdapter.ts +23 -0
- 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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 } =
|
|
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
|
});
|
|
@@ -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
|
-
|
|
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;
|