@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
@@ -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
- it('throws EntityNotAuthorizedError when deny', async () => {
241
- const viewerContext = instance(mock(ViewerContext));
242
- const queryContext = instance(mock(EntityQueryContext));
243
- const entity = new BlahEntity(viewerContext, { id: '1' });
244
- const policy = new AlwaysDenyPolicy();
245
- await expect(
246
- policy.authorizeCreateAsync(viewerContext, queryContext, entity)
247
- ).rejects.toBeInstanceOf(EntityNotAuthorizedError);
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
- it('returns entity when allowed', async () => {
251
- const viewerContext = instance(mock(ViewerContext));
252
- const queryContext = instance(mock(EntityQueryContext));
253
- const entity = new BlahEntity(viewerContext, { id: '1' });
254
- const policy = new AlwaysAllowPolicy();
255
- const approvedEntity = await policy.authorizeCreateAsync(viewerContext, queryContext, entity);
256
- expect(approvedEntity).toEqual(entity);
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
- it('throws EntityNotAuthorizedError when all skipped', async () => {
260
- const viewerContext = instance(mock(ViewerContext));
261
- const queryContext = instance(mock(EntityQueryContext));
262
- const entity = new BlahEntity(viewerContext, { id: '1' });
263
- const policy = new SkipAllPolicy();
264
- await expect(
265
- policy.authorizeCreateAsync(viewerContext, queryContext, entity)
266
- ).rejects.toBeInstanceOf(EntityNotAuthorizedError);
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
- it('throws EntityNotAuthorizedError when empty policy', async () => {
270
- const viewerContext = instance(mock(ViewerContext));
271
- const queryContext = instance(mock(EntityQueryContext));
272
- const entity = new BlahEntity(viewerContext, { id: '1' });
273
- const policy = new EmptyPolicy();
274
- await expect(
275
- policy.authorizeCreateAsync(viewerContext, queryContext, entity)
276
- ).rejects.toBeInstanceOf(EntityNotAuthorizedError);
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
- it('throws when rule throws', async () => {
280
- const viewerContext = instance(mock(ViewerContext));
281
- const queryContext = instance(mock(EntityQueryContext));
282
- const entity = new BlahEntity(viewerContext, { id: '1' });
283
- const policy = new ThrowAllPolicy();
284
- await expect(
285
- policy.authorizeCreateAsync(viewerContext, queryContext, entity)
286
- ).rejects.toThrowError('WooHoo!');
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('dry run', () => {
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(viewerContext, queryContext, entity);
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(viewerContext, queryContext, entity);
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('logging enforce', () => {
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(viewerContext, queryContext, entity);
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
+ });
@@ -78,6 +78,7 @@ describe(EntitySecondaryCacheLoader, () => {
78
78
  spiedPrivacyPolicy.authorizeReadAsync(
79
79
  vc1,
80
80
  anyOfClass(EntityNonTransactionalQueryContext),
81
+ anything(),
81
82
  anything()
82
83
  )
83
84
  ).once();
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(