@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mock, instance, spy, verify, anyOfClass } from 'ts-mockito';
|
|
1
|
+
import { mock, instance, spy, verify, anyOfClass, anything, objectContaining } from 'ts-mockito';
|
|
2
2
|
|
|
3
3
|
import Entity from '../Entity';
|
|
4
4
|
import { EntityCompanionDefinition } from '../EntityCompanionProvider';
|
|
@@ -6,11 +6,15 @@ import EntityConfiguration from '../EntityConfiguration';
|
|
|
6
6
|
import { UUIDField } from '../EntityFields';
|
|
7
7
|
import EntityPrivacyPolicy, {
|
|
8
8
|
EntityPrivacyPolicyEvaluator,
|
|
9
|
+
EntityAuthorizationAction,
|
|
9
10
|
EntityPrivacyPolicyEvaluationMode,
|
|
10
11
|
} from '../EntityPrivacyPolicy';
|
|
11
12
|
import { EntityQueryContext } from '../EntityQueryContext';
|
|
12
13
|
import ViewerContext from '../ViewerContext';
|
|
13
14
|
import EntityNotAuthorizedError from '../errors/EntityNotAuthorizedError';
|
|
15
|
+
import IEntityMetricsAdapter, {
|
|
16
|
+
EntityMetricsAuthorizationResult,
|
|
17
|
+
} from '../metrics/IEntityMetricsAdapter';
|
|
14
18
|
import AlwaysAllowPrivacyPolicyRule from '../rules/AlwaysAllowPrivacyPolicyRule';
|
|
15
19
|
import AlwaysDenyPrivacyPolicyRule from '../rules/AlwaysDenyPrivacyPolicyRule';
|
|
16
20
|
import AlwaysSkipPrivacyPolicyRule from '../rules/AlwaysSkipPrivacyPolicyRule';
|
|
@@ -237,143 +241,276 @@ const blahEntityCompanionDefinition = new EntityCompanionDefinition({
|
|
|
237
241
|
});
|
|
238
242
|
|
|
239
243
|
describe(EntityPrivacyPolicy, () => {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
244
|
+
describe(EntityPrivacyPolicyEvaluationMode.ENFORCE.toString(), () => {
|
|
245
|
+
it('throws EntityNotAuthorizedError when deny', async () => {
|
|
246
|
+
const viewerContext = instance(mock(ViewerContext));
|
|
247
|
+
const queryContext = instance(mock(EntityQueryContext));
|
|
248
|
+
const metricsAdapterMock = mock<IEntityMetricsAdapter>();
|
|
249
|
+
const metricsAdapter = instance(metricsAdapterMock);
|
|
250
|
+
const entity = new BlahEntity(viewerContext, { id: '1' });
|
|
251
|
+
const policy = new AlwaysDenyPolicy();
|
|
252
|
+
await expect(
|
|
253
|
+
policy.authorizeCreateAsync(viewerContext, queryContext, entity, metricsAdapter)
|
|
254
|
+
).rejects.toBeInstanceOf(EntityNotAuthorizedError);
|
|
255
|
+
verify(
|
|
256
|
+
metricsAdapterMock.logAuthorizationEvent(
|
|
257
|
+
objectContaining({
|
|
258
|
+
entityClassName: entity.constructor.name,
|
|
259
|
+
action: EntityAuthorizationAction.CREATE,
|
|
260
|
+
evaluationResult: EntityMetricsAuthorizationResult.DENY,
|
|
261
|
+
privacyPolicyEvaluationMode: EntityPrivacyPolicyEvaluationMode.ENFORCE,
|
|
262
|
+
})
|
|
263
|
+
)
|
|
264
|
+
).once();
|
|
265
|
+
});
|
|
249
266
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
267
|
+
it('returns entity when allowed', async () => {
|
|
268
|
+
const viewerContext = instance(mock(ViewerContext));
|
|
269
|
+
const queryContext = instance(mock(EntityQueryContext));
|
|
270
|
+
const metricsAdapterMock = mock<IEntityMetricsAdapter>();
|
|
271
|
+
const metricsAdapter = instance(metricsAdapterMock);
|
|
272
|
+
const entity = new BlahEntity(viewerContext, { id: '1' });
|
|
273
|
+
const policy = new AlwaysAllowPolicy();
|
|
274
|
+
const approvedEntity = await policy.authorizeCreateAsync(
|
|
275
|
+
viewerContext,
|
|
276
|
+
queryContext,
|
|
277
|
+
entity,
|
|
278
|
+
metricsAdapter
|
|
279
|
+
);
|
|
280
|
+
expect(approvedEntity).toEqual(entity);
|
|
281
|
+
verify(
|
|
282
|
+
metricsAdapterMock.logAuthorizationEvent(
|
|
283
|
+
objectContaining({
|
|
284
|
+
entityClassName: entity.constructor.name,
|
|
285
|
+
action: EntityAuthorizationAction.CREATE,
|
|
286
|
+
evaluationResult: EntityMetricsAuthorizationResult.ALLOW,
|
|
287
|
+
privacyPolicyEvaluationMode: EntityPrivacyPolicyEvaluationMode.ENFORCE,
|
|
288
|
+
})
|
|
289
|
+
)
|
|
290
|
+
).once();
|
|
291
|
+
});
|
|
258
292
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
policy
|
|
266
|
-
|
|
267
|
-
|
|
293
|
+
it('throws EntityNotAuthorizedError when all skipped', async () => {
|
|
294
|
+
const viewerContext = instance(mock(ViewerContext));
|
|
295
|
+
const queryContext = instance(mock(EntityQueryContext));
|
|
296
|
+
const metricsAdapterMock = mock<IEntityMetricsAdapter>();
|
|
297
|
+
const metricsAdapter = instance(metricsAdapterMock);
|
|
298
|
+
const entity = new BlahEntity(viewerContext, { id: '1' });
|
|
299
|
+
const policy = new SkipAllPolicy();
|
|
300
|
+
await expect(
|
|
301
|
+
policy.authorizeCreateAsync(viewerContext, queryContext, entity, metricsAdapter)
|
|
302
|
+
).rejects.toBeInstanceOf(EntityNotAuthorizedError);
|
|
303
|
+
verify(
|
|
304
|
+
metricsAdapterMock.logAuthorizationEvent(
|
|
305
|
+
objectContaining({
|
|
306
|
+
entityClassName: entity.constructor.name,
|
|
307
|
+
action: EntityAuthorizationAction.CREATE,
|
|
308
|
+
evaluationResult: EntityMetricsAuthorizationResult.DENY,
|
|
309
|
+
privacyPolicyEvaluationMode: EntityPrivacyPolicyEvaluationMode.ENFORCE,
|
|
310
|
+
})
|
|
311
|
+
)
|
|
312
|
+
).once();
|
|
313
|
+
});
|
|
268
314
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
policy
|
|
276
|
-
|
|
277
|
-
|
|
315
|
+
it('throws EntityNotAuthorizedError when empty policy', async () => {
|
|
316
|
+
const viewerContext = instance(mock(ViewerContext));
|
|
317
|
+
const queryContext = instance(mock(EntityQueryContext));
|
|
318
|
+
const metricsAdapterMock = mock<IEntityMetricsAdapter>();
|
|
319
|
+
const metricsAdapter = instance(metricsAdapterMock);
|
|
320
|
+
const entity = new BlahEntity(viewerContext, { id: '1' });
|
|
321
|
+
const policy = new EmptyPolicy();
|
|
322
|
+
await expect(
|
|
323
|
+
policy.authorizeCreateAsync(viewerContext, queryContext, entity, metricsAdapter)
|
|
324
|
+
).rejects.toBeInstanceOf(EntityNotAuthorizedError);
|
|
325
|
+
verify(
|
|
326
|
+
metricsAdapterMock.logAuthorizationEvent(
|
|
327
|
+
objectContaining({
|
|
328
|
+
entityClassName: entity.constructor.name,
|
|
329
|
+
action: EntityAuthorizationAction.CREATE,
|
|
330
|
+
evaluationResult: EntityMetricsAuthorizationResult.DENY,
|
|
331
|
+
privacyPolicyEvaluationMode: EntityPrivacyPolicyEvaluationMode.ENFORCE,
|
|
332
|
+
})
|
|
333
|
+
)
|
|
334
|
+
).once();
|
|
335
|
+
});
|
|
278
336
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
policy
|
|
286
|
-
|
|
337
|
+
it('throws when rule throws', async () => {
|
|
338
|
+
const viewerContext = instance(mock(ViewerContext));
|
|
339
|
+
const queryContext = instance(mock(EntityQueryContext));
|
|
340
|
+
const metricsAdapterMock = mock<IEntityMetricsAdapter>();
|
|
341
|
+
const metricsAdapter = instance(metricsAdapterMock);
|
|
342
|
+
const entity = new BlahEntity(viewerContext, { id: '1' });
|
|
343
|
+
const policy = new ThrowAllPolicy();
|
|
344
|
+
await expect(
|
|
345
|
+
policy.authorizeCreateAsync(viewerContext, queryContext, entity, metricsAdapter)
|
|
346
|
+
).rejects.toThrowError('WooHoo!');
|
|
347
|
+
verify(metricsAdapterMock.logAuthorizationEvent(anything())).never();
|
|
348
|
+
});
|
|
287
349
|
});
|
|
288
350
|
|
|
289
|
-
describe(
|
|
351
|
+
describe(EntityPrivacyPolicyEvaluationMode.DRY_RUN.toString(), () => {
|
|
290
352
|
it('returns entity when denied but calls denialHandler', async () => {
|
|
291
353
|
const viewerContext = instance(mock(ViewerContext));
|
|
292
354
|
const queryContext = instance(mock(EntityQueryContext));
|
|
355
|
+
const metricsAdapterMock = mock<IEntityMetricsAdapter>();
|
|
356
|
+
const metricsAdapter = instance(metricsAdapterMock);
|
|
293
357
|
const entity = new BlahEntity(viewerContext, { id: '1' });
|
|
294
358
|
const policy = new DryRunAlwaysDenyPolicy();
|
|
295
359
|
|
|
296
360
|
const policySpy = spy(policy);
|
|
297
361
|
|
|
298
|
-
const approvedEntity = await policy.authorizeCreateAsync(
|
|
362
|
+
const approvedEntity = await policy.authorizeCreateAsync(
|
|
363
|
+
viewerContext,
|
|
364
|
+
queryContext,
|
|
365
|
+
entity,
|
|
366
|
+
metricsAdapter
|
|
367
|
+
);
|
|
299
368
|
expect(approvedEntity).toEqual(entity);
|
|
300
369
|
|
|
301
370
|
verify(policySpy.denyHandler(anyOfClass(EntityNotAuthorizedError))).once();
|
|
371
|
+
|
|
372
|
+
verify(
|
|
373
|
+
metricsAdapterMock.logAuthorizationEvent(
|
|
374
|
+
objectContaining({
|
|
375
|
+
entityClassName: entity.constructor.name,
|
|
376
|
+
action: EntityAuthorizationAction.CREATE,
|
|
377
|
+
evaluationResult: EntityMetricsAuthorizationResult.DENY,
|
|
378
|
+
privacyPolicyEvaluationMode: EntityPrivacyPolicyEvaluationMode.DRY_RUN,
|
|
379
|
+
})
|
|
380
|
+
)
|
|
381
|
+
).once();
|
|
302
382
|
});
|
|
303
383
|
|
|
304
384
|
it('does not log when not denied', async () => {
|
|
305
385
|
const viewerContext = instance(mock(ViewerContext));
|
|
306
386
|
const queryContext = instance(mock(EntityQueryContext));
|
|
387
|
+
const metricsAdapterMock = mock<IEntityMetricsAdapter>();
|
|
388
|
+
const metricsAdapter = instance(metricsAdapterMock);
|
|
307
389
|
const entity = new BlahEntity(viewerContext, { id: '1' });
|
|
308
390
|
const policy = new DryRunAlwaysAllowPolicy();
|
|
309
391
|
|
|
310
392
|
const policySpy = spy(policy);
|
|
311
393
|
|
|
312
|
-
const approvedEntity = await policy.authorizeCreateAsync(
|
|
394
|
+
const approvedEntity = await policy.authorizeCreateAsync(
|
|
395
|
+
viewerContext,
|
|
396
|
+
queryContext,
|
|
397
|
+
entity,
|
|
398
|
+
metricsAdapter
|
|
399
|
+
);
|
|
313
400
|
expect(approvedEntity).toEqual(entity);
|
|
314
401
|
|
|
315
402
|
verify(policySpy.denyHandler(anyOfClass(EntityNotAuthorizedError))).never();
|
|
403
|
+
|
|
404
|
+
verify(
|
|
405
|
+
metricsAdapterMock.logAuthorizationEvent(
|
|
406
|
+
objectContaining({
|
|
407
|
+
entityClassName: entity.constructor.name,
|
|
408
|
+
action: EntityAuthorizationAction.CREATE,
|
|
409
|
+
evaluationResult: EntityMetricsAuthorizationResult.ALLOW,
|
|
410
|
+
privacyPolicyEvaluationMode: EntityPrivacyPolicyEvaluationMode.DRY_RUN,
|
|
411
|
+
})
|
|
412
|
+
)
|
|
413
|
+
).once();
|
|
316
414
|
});
|
|
317
415
|
|
|
318
416
|
it('passes through other errors', async () => {
|
|
319
417
|
const viewerContext = instance(mock(ViewerContext));
|
|
320
418
|
const queryContext = instance(mock(EntityQueryContext));
|
|
419
|
+
const metricsAdapterMock = mock<IEntityMetricsAdapter>();
|
|
420
|
+
const metricsAdapter = instance(metricsAdapterMock);
|
|
321
421
|
const entity = new BlahEntity(viewerContext, { id: '1' });
|
|
322
422
|
const policy = new DryRunThrowAllPolicy();
|
|
323
423
|
|
|
324
424
|
const policySpy = spy(policy);
|
|
325
425
|
|
|
326
426
|
await expect(
|
|
327
|
-
policy.authorizeCreateAsync(viewerContext, queryContext, entity)
|
|
427
|
+
policy.authorizeCreateAsync(viewerContext, queryContext, entity, metricsAdapter)
|
|
328
428
|
).rejects.toThrowError('WooHoo!');
|
|
329
429
|
|
|
330
430
|
verify(policySpy.denyHandler(anyOfClass(EntityNotAuthorizedError))).never();
|
|
431
|
+
|
|
432
|
+
verify(metricsAdapterMock.logAuthorizationEvent(anything())).never();
|
|
331
433
|
});
|
|
332
434
|
});
|
|
333
435
|
|
|
334
|
-
describe(
|
|
436
|
+
describe(EntityPrivacyPolicyEvaluationMode.ENFORCE_AND_LOG.toString(), () => {
|
|
335
437
|
it('denies when denied but calls denialHandler', async () => {
|
|
336
438
|
const viewerContext = instance(mock(ViewerContext));
|
|
337
439
|
const queryContext = instance(mock(EntityQueryContext));
|
|
440
|
+
const metricsAdapterMock = mock<IEntityMetricsAdapter>();
|
|
441
|
+
const metricsAdapter = instance(metricsAdapterMock);
|
|
338
442
|
const entity = new BlahEntity(viewerContext, { id: '1' });
|
|
339
443
|
const policy = new LoggingEnforceAlwaysDenyPolicy();
|
|
340
444
|
|
|
341
445
|
const policySpy = spy(policy);
|
|
342
446
|
|
|
343
447
|
await expect(
|
|
344
|
-
policy.authorizeCreateAsync(viewerContext, queryContext, entity)
|
|
448
|
+
policy.authorizeCreateAsync(viewerContext, queryContext, entity, metricsAdapter)
|
|
345
449
|
).rejects.toBeInstanceOf(EntityNotAuthorizedError);
|
|
346
450
|
|
|
347
451
|
verify(policySpy.denyHandler(anyOfClass(EntityNotAuthorizedError))).once();
|
|
452
|
+
|
|
453
|
+
verify(
|
|
454
|
+
metricsAdapterMock.logAuthorizationEvent(
|
|
455
|
+
objectContaining({
|
|
456
|
+
entityClassName: entity.constructor.name,
|
|
457
|
+
action: EntityAuthorizationAction.CREATE,
|
|
458
|
+
evaluationResult: EntityMetricsAuthorizationResult.DENY,
|
|
459
|
+
privacyPolicyEvaluationMode: EntityPrivacyPolicyEvaluationMode.ENFORCE_AND_LOG,
|
|
460
|
+
})
|
|
461
|
+
)
|
|
462
|
+
).once();
|
|
348
463
|
});
|
|
349
464
|
|
|
350
465
|
it('does not log when not denied', async () => {
|
|
351
466
|
const viewerContext = instance(mock(ViewerContext));
|
|
352
467
|
const queryContext = instance(mock(EntityQueryContext));
|
|
468
|
+
const metricsAdapterMock = mock<IEntityMetricsAdapter>();
|
|
469
|
+
const metricsAdapter = instance(metricsAdapterMock);
|
|
353
470
|
const entity = new BlahEntity(viewerContext, { id: '1' });
|
|
354
471
|
const policy = new LoggingEnforceAlwaysAllowPolicy();
|
|
355
472
|
|
|
356
473
|
const policySpy = spy(policy);
|
|
357
474
|
|
|
358
|
-
const approvedEntity = await policy.authorizeCreateAsync(
|
|
475
|
+
const approvedEntity = await policy.authorizeCreateAsync(
|
|
476
|
+
viewerContext,
|
|
477
|
+
queryContext,
|
|
478
|
+
entity,
|
|
479
|
+
metricsAdapter
|
|
480
|
+
);
|
|
359
481
|
expect(approvedEntity).toEqual(entity);
|
|
360
482
|
|
|
361
483
|
verify(policySpy.denyHandler(anyOfClass(EntityNotAuthorizedError))).never();
|
|
484
|
+
|
|
485
|
+
verify(
|
|
486
|
+
metricsAdapterMock.logAuthorizationEvent(
|
|
487
|
+
objectContaining({
|
|
488
|
+
entityClassName: entity.constructor.name,
|
|
489
|
+
action: EntityAuthorizationAction.CREATE,
|
|
490
|
+
evaluationResult: EntityMetricsAuthorizationResult.ALLOW,
|
|
491
|
+
privacyPolicyEvaluationMode: EntityPrivacyPolicyEvaluationMode.ENFORCE_AND_LOG,
|
|
492
|
+
})
|
|
493
|
+
)
|
|
494
|
+
).once();
|
|
362
495
|
});
|
|
363
496
|
|
|
364
497
|
it('passes through other errors', async () => {
|
|
365
498
|
const viewerContext = instance(mock(ViewerContext));
|
|
366
499
|
const queryContext = instance(mock(EntityQueryContext));
|
|
500
|
+
const metricsAdapterMock = mock<IEntityMetricsAdapter>();
|
|
501
|
+
const metricsAdapter = instance(metricsAdapterMock);
|
|
367
502
|
const entity = new BlahEntity(viewerContext, { id: '1' });
|
|
368
503
|
const policy = new LoggingEnforceThrowAllPolicy();
|
|
369
504
|
|
|
370
505
|
const policySpy = spy(policy);
|
|
371
506
|
|
|
372
507
|
await expect(
|
|
373
|
-
policy.authorizeCreateAsync(viewerContext, queryContext, entity)
|
|
508
|
+
policy.authorizeCreateAsync(viewerContext, queryContext, entity, metricsAdapter)
|
|
374
509
|
).rejects.toThrowError('WooHoo!');
|
|
375
510
|
|
|
376
511
|
verify(policySpy.denyHandler(anyOfClass(EntityNotAuthorizedError))).never();
|
|
512
|
+
|
|
513
|
+
verify(metricsAdapterMock.logAuthorizationEvent(anything())).never();
|
|
377
514
|
});
|
|
378
515
|
});
|
|
379
516
|
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import invariant from 'invariant';
|
|
2
|
+
|
|
3
|
+
import { EntityQueryContext } from '../EntityQueryContext';
|
|
4
|
+
import ViewerContext from '../ViewerContext';
|
|
5
|
+
import { createUnitTestEntityCompanionProvider } from '../utils/testing/createUnitTestEntityCompanionProvider';
|
|
6
|
+
|
|
7
|
+
describe(EntityQueryContext, () => {
|
|
8
|
+
describe('callbacks', () => {
|
|
9
|
+
it('calls all callbacks, and calls invalidation first', async () => {
|
|
10
|
+
const companionProvider = createUnitTestEntityCompanionProvider();
|
|
11
|
+
const viewerContext = new ViewerContext(companionProvider);
|
|
12
|
+
|
|
13
|
+
const preCommitFirstCallback = jest.fn(async (): Promise<void> => {});
|
|
14
|
+
const preCommitSecondCallback = jest.fn(async (): Promise<void> => {});
|
|
15
|
+
const postCommitInvalidationCallback = jest.fn(async (): Promise<void> => {
|
|
16
|
+
invariant(
|
|
17
|
+
preCommitFirstCallback.mock.calls.length === 1,
|
|
18
|
+
'preCommit should be called before postCommitInvalidation'
|
|
19
|
+
);
|
|
20
|
+
invariant(
|
|
21
|
+
preCommitSecondCallback.mock.calls.length === 1,
|
|
22
|
+
'preCommit should be called before postCommitInvalidation'
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
const postCommitCallback = jest.fn(async (): Promise<void> => {
|
|
26
|
+
invariant(
|
|
27
|
+
preCommitFirstCallback.mock.calls.length === 1,
|
|
28
|
+
'preCommit should be called before postCommit'
|
|
29
|
+
);
|
|
30
|
+
invariant(
|
|
31
|
+
preCommitSecondCallback.mock.calls.length === 1,
|
|
32
|
+
'preCommit should be called before postCommit'
|
|
33
|
+
);
|
|
34
|
+
invariant(
|
|
35
|
+
postCommitInvalidationCallback.mock.calls.length === 1,
|
|
36
|
+
'postCommitInvalidation should be called before postCommit'
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
await viewerContext.runInTransactionForDatabaseAdaptorFlavorAsync(
|
|
41
|
+
'postgres',
|
|
42
|
+
async (queryContext) => {
|
|
43
|
+
queryContext.appendPostCommitCallback(postCommitCallback);
|
|
44
|
+
queryContext.appendPostCommitInvalidationCallback(postCommitInvalidationCallback);
|
|
45
|
+
queryContext.appendPreCommitCallback(preCommitSecondCallback, 2);
|
|
46
|
+
queryContext.appendPreCommitCallback(preCommitFirstCallback, 1);
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
expect(preCommitFirstCallback).toHaveBeenCalledTimes(1);
|
|
51
|
+
expect(preCommitSecondCallback).toHaveBeenCalledTimes(1);
|
|
52
|
+
expect(postCommitCallback).toHaveBeenCalledTimes(1);
|
|
53
|
+
expect(postCommitInvalidationCallback).toHaveBeenCalledTimes(1);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('prevents transaction from finishing when precommit throws (post commit callbacks are not called)', async () => {
|
|
57
|
+
const companionProvider = createUnitTestEntityCompanionProvider();
|
|
58
|
+
const viewerContext = new ViewerContext(companionProvider);
|
|
59
|
+
|
|
60
|
+
const preCommitCallback = jest.fn(async (): Promise<void> => {
|
|
61
|
+
throw new Error('wat');
|
|
62
|
+
});
|
|
63
|
+
const postCommitInvalidationCallback = jest.fn(async (): Promise<void> => {});
|
|
64
|
+
const postCommitCallback = jest.fn(async (): Promise<void> => {});
|
|
65
|
+
|
|
66
|
+
await expect(
|
|
67
|
+
viewerContext.runInTransactionForDatabaseAdaptorFlavorAsync(
|
|
68
|
+
'postgres',
|
|
69
|
+
async (queryContext) => {
|
|
70
|
+
queryContext.appendPostCommitCallback(postCommitCallback);
|
|
71
|
+
queryContext.appendPostCommitInvalidationCallback(postCommitInvalidationCallback);
|
|
72
|
+
queryContext.appendPreCommitCallback(preCommitCallback, 0);
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
).rejects.toThrowError('wat');
|
|
76
|
+
|
|
77
|
+
expect(preCommitCallback).toHaveBeenCalledTimes(1);
|
|
78
|
+
expect(postCommitCallback).toHaveBeenCalledTimes(0);
|
|
79
|
+
expect(postCommitInvalidationCallback).toHaveBeenCalledTimes(0);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -33,6 +33,7 @@ export { default as EntitySecondaryCacheLoader } from './EntitySecondaryCacheLoa
|
|
|
33
33
|
export * from './EntitySecondaryCacheLoader';
|
|
34
34
|
export * from './EntityMutator';
|
|
35
35
|
export { default as EntityMutationValidator } from './EntityMutationValidator';
|
|
36
|
+
export * from './EntityMutationInfo';
|
|
36
37
|
export * from './EntityMutationTriggerConfiguration';
|
|
37
38
|
export { default as EntityMutationTriggerConfiguration } from './EntityMutationTriggerConfiguration';
|
|
38
39
|
export { default as EntityMutatorFactory } from './EntityMutatorFactory';
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EntityAuthorizationAction,
|
|
3
|
+
EntityPrivacyPolicyEvaluationMode,
|
|
4
|
+
} from '../EntityPrivacyPolicy';
|
|
5
|
+
|
|
1
6
|
export enum EntityMetricsLoadType {
|
|
2
7
|
LOAD_MANY,
|
|
3
8
|
LOAD_MANY_EQUALITY_CONJUNCTION,
|
|
@@ -28,11 +33,29 @@ export interface IncrementLoadCountEvent {
|
|
|
28
33
|
entityClassName: string;
|
|
29
34
|
}
|
|
30
35
|
|
|
36
|
+
export enum EntityMetricsAuthorizationResult {
|
|
37
|
+
DENY,
|
|
38
|
+
ALLOW,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface EntityMetricsAuthorizationEvent {
|
|
42
|
+
entityClassName: string;
|
|
43
|
+
action: EntityAuthorizationAction;
|
|
44
|
+
evaluationResult: EntityMetricsAuthorizationResult;
|
|
45
|
+
privacyPolicyEvaluationMode: EntityPrivacyPolicyEvaluationMode;
|
|
46
|
+
}
|
|
47
|
+
|
|
31
48
|
/**
|
|
32
49
|
* An interface for gathering metrics about the Entity framework. Information about
|
|
33
50
|
* entity load and mutation operations is piped to an instance of this adapter.
|
|
34
51
|
*/
|
|
35
52
|
export default interface IEntityMetricsAdapter {
|
|
53
|
+
/**
|
|
54
|
+
* Called when a {@link EntityPrivacyPolicy} authorization succeeds or fails.
|
|
55
|
+
* @param authorizationEvent - info about the authorization event
|
|
56
|
+
*/
|
|
57
|
+
logAuthorizationEvent(authorizationEvent: EntityMetricsAuthorizationEvent): void;
|
|
58
|
+
|
|
36
59
|
/**
|
|
37
60
|
* Called when any load occurs.
|
|
38
61
|
* @param loadEvent - info about the load event
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import IEntityMetricsAdapter, {
|
|
2
|
+
EntityMetricsAuthorizationEvent,
|
|
2
3
|
EntityMetricsLoadEvent,
|
|
3
4
|
EntityMetricsMutationEvent,
|
|
4
5
|
IncrementLoadCountEvent,
|
|
5
6
|
} from './IEntityMetricsAdapter';
|
|
6
7
|
|
|
7
8
|
export default class NoOpEntityMetricsAdapter implements IEntityMetricsAdapter {
|
|
9
|
+
logAuthorizationEvent(_authorizationEvent: EntityMetricsAuthorizationEvent): void {}
|
|
8
10
|
logDataManagerLoadEvent(_loadEvent: EntityMetricsLoadEvent): void {}
|
|
9
11
|
logMutatorMutationEvent(_mutationEvent: EntityMetricsMutationEvent): void {}
|
|
10
12
|
incrementDataManagerDataloaderLoadCount(
|